aboutsummaryrefslogtreecommitdiff
path: root/Hotline
diff options
context:
space:
mode:
authorDustin Mierau <dustin@mierau.me>2024-01-09 10:30:44 -0800
committerDustin Mierau <dustin@mierau.me>2024-01-09 10:30:44 -0800
commit382121de2e79303845331d699adcdd777f8f062a (patch)
tree83bab877069d3a0daddcb6c2a031400b99c60756 /Hotline
parent0910d3380755d38a93e5b87a31ecf31f17d0bc58 (diff)
Split view in News saves position and hiding when an article isn't selected. Some code cleanup. Some plumbing for news posting though it's not hooked up yet.
Diffstat (limited to 'Hotline')
-rw-r--r--Hotline/Application.swift10
-rw-r--r--Hotline/Hotline/HotlineClient.swift116
-rw-r--r--Hotline/Models/Hotline.swift2
-rw-r--r--Hotline/Models/NewsArticle.swift21
-rw-r--r--Hotline/Shared/URLAdditions.swift16
-rw-r--r--Hotline/macOS/FilePreviewTextView.swift2
-rw-r--r--Hotline/macOS/NewsEditorView.swift100
-rw-r--r--Hotline/macOS/NewsView.swift358
-rw-r--r--Hotline/macOS/TrackerView.swift2
9 files changed, 381 insertions, 246 deletions
diff --git a/Hotline/Application.swift b/Hotline/Application.swift
index 4541ea3..e7445dc 100644
--- a/Hotline/Application.swift
+++ b/Hotline/Application.swift
@@ -55,6 +55,16 @@ struct Application: App {
.environment(preferences)
}
+ // MARK: News Editor Window
+// WindowGroup(id: "news-editor", for: NewsArticle.self) { $article in
+// NewsEditorView(article: $article)
+// }
+// .windowResizability(.contentSize)
+// .windowStyle(.titleBar)
+// .windowToolbarStyle(.unifiedCompact(showsTitle: true))
+// .defaultSize(width: 450, height: 550)
+// .defaultPosition(.center)
+
// MARK: Image Preview Window
WindowGroup(id: "preview-image", for: PreviewFileInfo.self) { $info in
FilePreviewImageView(info: $info)
diff --git a/Hotline/Hotline/HotlineClient.swift b/Hotline/Hotline/HotlineClient.swift
index c33cb75..c1b13ff 100644
--- a/Hotline/Hotline/HotlineClient.swift
+++ b/Hotline/Hotline/HotlineClient.swift
@@ -131,82 +131,46 @@ class HotlineClient {
}
switch newState {
- case .preparing:
- print("HotlineClient: connection preparing...")
- case .setup:
- print("HotlineClient: connection setup")
- case .waiting(let err):
- print("HotlineClient: connection waiting \(err)...")
- switch err {
- case .posix(let errCode):
- print("HotlineClient: posix error code \(errCode)")
- self.disconnect()
-// switch errCode {
-// case .ETIMEDOUT:
-// self.disconnect()
-// case .ECONNREFUSED:
-// self.disconnect()
-// default:
-// self.disconnect()
-// break
-// }
- case .tls(let errStatus):
- print("HotlineClient: tls error code \(errStatus)")
- self.disconnect()
- case .dns(let errType):
- print("HotlineClient: DNS Error code \(errType)")
- self.disconnect()
- default:
- print("HotlineClient: error code \(err)")
- }
case .ready:
print("HotlineClient: connection ready!")
self.updateConnectionStatus(.connected)
self.connectCallback?(true)
self.connectCallback = nil
-// if self.connectionContinuation != nil {
-// let continuation = self.connectionContinuation!
-// self.connectionContinuation = nil
-// callback?(true)
-// continuation.resume(returning: true)
+// case .waiting(let err):
+// print("HotlineClient: connection waiting \(err)...")
+// switch err {
+// case .posix(let errCode):
+// print("HotlineClient: posix error code \(errCode)")
+// self.disconnect()
+// case .tls(let errStatus):
+// print("HotlineClient: tls error code \(errStatus)")
+// self.disconnect()
+// case .dns(let errType):
+// print("HotlineClient: DNS Error code \(errType)")
+// self.disconnect()
+// default:
+// print("HotlineClient: error code \(err)")
// }
-// callback?(true)
- case .failed(let err):
- print("HotlineClient: connection error \(err)")
+ case .cancelled:
+ print("HotlineClient: connection cancelled")
self.updateConnectionStatus(.disconnected)
self.reset()
self.connectCallback?(false)
self.connectCallback = nil
-// callback?(false)
-// if self.connectionContinuation != nil {
-// let continuation = self.connectionContinuation!
-// self.connectionContinuation = nil
-// continuation.resume(returning: false)
-// }
- case .cancelled:
- print("HotlineClient: connection cancelled")
+ case .failed(let err):
+ print("HotlineClient: connection error \(err)")
self.updateConnectionStatus(.disconnected)
self.reset()
self.connectCallback?(false)
self.connectCallback = nil
-// callback?(false)
-// if self.connectionContinuation != nil {
-// let continuation = self.connectionContinuation!
-// self.connectionContinuation = nil
-// continuation.resume(returning: false)
-// }
default:
+ print("HotlineClient: hmm", newState)
break
}
}
self.updateConnectionStatus(.connecting)
self.connection?.start(queue: .global())
-
-// return await withCheckedContinuation { [weak self] continuation in
-// self?.connectionContinuation = continuation
-// self?.connection?.start(queue: .global())
-// }
}
private func reset() {
@@ -214,6 +178,7 @@ class HotlineClient {
}
func disconnect() {
+ print("DISCONNECT?")
for (_, replyInfo) in self.transactionLog {
let replyCallback = replyInfo.1
replyCallback?(HotlineTransaction(type: replyInfo.0), .networkFailure)
@@ -590,6 +555,47 @@ class HotlineClient {
})
}
+ func postNewsArticle(title: String, text: String, path: [String] = [], parentID: UInt32? = nil, sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsArticle]) -> Void)? = nil) {
+ var t = HotlineTransaction(type: .postNewsArticle)
+ if !path.isEmpty {
+ t.setFieldPath(type: .newsPath, val: path)
+ }
+ if let parentID = parentID {
+ t.setFieldUInt32(type: .newsArticleID, val: parentID)
+ }
+ t.setFieldString(type: .newsArticleTitle, val: title)
+ t.setFieldString(type: .newsArticleDataFlavor, val: "text/plain")
+ t.setFieldUInt32(type: .newsArticleFlags, val: 0)
+ t.setFieldString(type: .newsArticleData, val: text)
+
+ self.sendTransaction(t, sent: sent, reply: { r, err in
+ if err != nil {
+ reply?([])
+ return
+ }
+
+ var articles: [HotlineNewsArticle] = []
+// let articleData = r.getField(type: .newsArticleListData)
+
+// print("ARTICLE DATA?", articleData)
+
+ if let articleData = r.getField(type: .newsArticleListData) {
+ let newsList = articleData.getNewsList()
+ for art in newsList.articles {
+ var blah = art
+ blah.path = path
+ articles.append(blah)
+
+ print(blah.title)
+ }
+ }
+
+ DispatchQueue.main.async {
+ reply?(articles)
+ }
+ })
+ }
+
func sendGetNewsArticles(path: [String] = [], sent: ((Bool) -> Void)? = nil, reply: (([HotlineNewsArticle]) -> Void)? = nil) {
var t = HotlineTransaction(type: .getNewsArticleNameList)
if !path.isEmpty {
diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift
index e88c267..c018e55 100644
--- a/Hotline/Models/Hotline.swift
+++ b/Hotline/Models/Hotline.swift
@@ -246,7 +246,7 @@ final class Hotline: HotlineClientDelegate, HotlineFileClientDelegate {
self.username = username
self.iconID = iconID
- print("CLIENT LOGIN: '\(server.login)' '\(server.password == nil)'")
+ print("CLIENT LOGIN: '\(server.login)' '\(server.password)'")
self.client.login(server.address, port: UInt16(server.port), login: server.login, password: server.password, username: username, iconID: UInt16(iconID)) { [weak self] err, serverName, serverVersion in
self?.serverVersion = serverVersion
diff --git a/Hotline/Models/NewsArticle.swift b/Hotline/Models/NewsArticle.swift
new file mode 100644
index 0000000..067d6b6
--- /dev/null
+++ b/Hotline/Models/NewsArticle.swift
@@ -0,0 +1,21 @@
+import Foundation
+
+struct NewsArticle: Identifiable, Codable {
+ var id: UUID = UUID()
+ var parentID: UInt32?
+ var path: [String]
+ var title: String
+ var body: String
+}
+
+extension NewsArticle: Equatable {
+ static func == (lhs: NewsArticle, rhs: NewsArticle) -> Bool {
+ return lhs.id == rhs.id && lhs.parentID == rhs.parentID
+ }
+}
+
+extension NewsArticle: Hashable {
+ func hash(into hasher: inout Hasher) {
+ hasher.combine(self.id)
+ }
+}
diff --git a/Hotline/Shared/URLAdditions.swift b/Hotline/Shared/URLAdditions.swift
index 7eebb46..1f25541 100644
--- a/Hotline/Shared/URLAdditions.swift
+++ b/Hotline/Shared/URLAdditions.swift
@@ -23,20 +23,4 @@ extension URL {
return filePath
}
-
-// private func prepareDownloadFile(name: String) -> Bool {
-// let folderURL = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask)[0]
-// let filePath = findUniqueFilePath(base: name, at: folderURL)
-//
-// if FileManager.default.createFile(atPath: filePath, contents: nil) {
-// if let h = FileHandle(forWritingAtPath: filePath) {
-// self.filePath = filePath
-// self.fileHandle = h
-// self.fileProgress?.fileURL = URL(filePath: filePath).resolvingSymlinksInPath()
-// return true
-// }
-// }
-//
-// return false
-// }
}
diff --git a/Hotline/macOS/FilePreviewTextView.swift b/Hotline/macOS/FilePreviewTextView.swift
index 4c81e6f..8b5b4f3 100644
--- a/Hotline/macOS/FilePreviewTextView.swift
+++ b/Hotline/macOS/FilePreviewTextView.swift
@@ -43,8 +43,8 @@ struct FilePreviewTextView: View {
.contentMargins(.top, -16.0, for: .scrollIndicators)
.contentMargins(.bottom, -16.0, for: .scrollIndicators)
.contentMargins(.trailing, -16.0, for: .scrollIndicators)
- .frame(maxWidth: .infinity, maxHeight: .infinity)
.scrollClipDisabled()
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
}
else {
VStack(alignment: .center, spacing: 0) {
diff --git a/Hotline/macOS/NewsEditorView.swift b/Hotline/macOS/NewsEditorView.swift
new file mode 100644
index 0000000..18c4a0d
--- /dev/null
+++ b/Hotline/macOS/NewsEditorView.swift
@@ -0,0 +1,100 @@
+import SwiftUI
+import UniformTypeIdentifiers
+
+struct NewsEditorView: View {
+ @Environment(\.controlActiveState) private var controlActiveState
+ @Environment(\.colorScheme) private var colorScheme
+ @Environment(\.dismiss) private var dismiss
+ @Environment(Hotline.self) private var model: Hotline
+
+// @Binding var article: NewsArticle?
+ @State var title: String = ""
+ @State var text: String = ""
+
+ var body: some View {
+ NavigationStack {
+ VStack(alignment: .leading, spacing: 0) {
+ HStack {
+ Button {
+ dismiss()
+ } label: {
+ Image(systemName: "xmark.circle.fill")
+ .resizable()
+ .scaledToFit()
+ }
+ .buttonStyle(.plain)
+ .frame(width: 16, height: 16)
+ .padding()
+
+ Spacer()
+
+ Button {
+ } label: {
+ Image(systemName: "paperplane")
+ .resizable()
+ .scaledToFit()
+ }
+ .buttonStyle(.plain)
+ .frame(width: 16, height: 16)
+ .padding()
+ }
+ .frame(maxWidth: .infinity)
+ TextField("Title", text: $title, axis: .vertical)
+ .textFieldStyle(.plain)
+ .padding()
+ .focusEffectDisabled()
+ .font(.title)
+ .frame(maxWidth: .infinity)
+ .border(Color.pink, width: 0)
+ Divider()
+ TextEditor(text: $text)
+ .textEditorStyle(.plain)
+ .font(.system(size: 14))
+ .lineSpacing(3)
+ .padding(16)
+ .contentMargins(.top, -16.0, for: .scrollIndicators)
+ .contentMargins(.bottom, -16.0, for: .scrollIndicators)
+ .contentMargins(.trailing, -16.0, for: .scrollIndicators)
+ .scrollClipDisabled()
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ }
+ }
+ .frame(minWidth: 300, idealWidth: 450, maxWidth: .infinity, minHeight: 300, idealHeight: 500, maxHeight: .infinity)
+ .background(Color(nsColor: .textBackgroundColor))
+ .presentationCompactAdaptation(.sheet)
+ .toolbarTitleDisplayMode(.inlineLarge)
+// .toolbar {
+// ToolbarItem(placement: .navigation) {
+// Button("Post", action: {})
+// }
+// ToolbarItem(placement: .automatic) {
+// Button("Delete", action: {
+// dismiss()
+// })
+// }
+// }
+ .task {
+// if let info = info {
+// preview = FilePreview(info: info)
+// preview?.download()
+// }
+ }
+ .onAppear {
+// if info == nil {
+// Task {
+// dismiss()
+// }
+// return
+// }
+ }
+ .onDisappear {
+// preview?.cancel()
+ dismiss()
+ }
+// .onChange(of: preview?.state) {
+// if preview?.state == .failed {
+// dismiss()
+// }
+// }
+ }
+}
diff --git a/Hotline/macOS/NewsView.swift b/Hotline/macOS/NewsView.swift
index daeb45b..d7e40f2 100644
--- a/Hotline/macOS/NewsView.swift
+++ b/Hotline/macOS/NewsView.swift
@@ -1,5 +1,190 @@
import SwiftUI
import MarkdownUI
+import SplitView
+
+struct NewsView: View {
+ @Environment(Hotline.self) private var model: Hotline
+ @Environment(\.openWindow) private var openWindow
+
+ @State private var selection: NewsInfo?
+ @State private var articleText: String?
+ @State private var splitHidden = SideHolder(.bottom)
+ @State private var splitFraction = FractionHolder.usingUserDefaults(0.25, key: "News Split Fraction")
+ @State private var editorOpen: Bool = false
+
+ var body: some View {
+ NavigationStack {
+ VSplit(
+ top: {
+ newsBrowser
+ },
+ bottom: {
+ articleViewer
+ }
+ )
+ .fraction(splitFraction)
+ .constraints(minPFraction: 0.1, minSFraction: 0.3)
+ .hide(splitHidden)
+ .styling(inset: 0, visibleThickness: 0.5, invisibleThickness: 5, hideSplitter: true)
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .background(Color(nsColor: .textBackgroundColor))
+ }
+// .sheet(isPresented: $editorOpen) {
+// print("Sheet dismissed!")
+// } content: {
+// NewsEditorView()
+// }
+ .toolbar {
+ ToolbarItem(placement: .primaryAction) {
+ Button {
+
+ } label: {
+ Image(systemName: "trash")
+ }
+ }
+
+ ToolbarItem(placement: .primaryAction) {
+ Button {
+// if let selection = selection {
+// editorOpen = true
+//// openWindow(id: "news-editor", value: NewsArticle(parentID: nil, path: selection.path, title: "", body: ""))
+// }
+ } label: {
+ Image(systemName: "square.and.pencil")
+ }
+ }
+
+ ToolbarItem(placement: .primaryAction) {
+ Button {
+
+ } label: {
+ Image(systemName: "arrowshape.turn.up.left")
+ }
+ }
+ }
+ }
+
+ var newsBrowser: some View {
+ List(model.news, id: \.self, selection: $selection) { newsItem in
+ NewsItemView(news: newsItem, depth: 0).tag(newsItem.id)
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .environment(\.defaultMinListRowHeight, 34)
+ .listStyle(.inset)
+ .alternatingRowBackgrounds(.enabled)
+ .task {
+ if !model.newsLoaded {
+ let _ = await model.getNewsList()
+ }
+ }
+ .contextMenu(forSelectionType: NewsInfo.self) { items in
+ // ...
+ } primaryAction: { items in
+ guard let clickedNews = items.first else {
+ return
+ }
+
+ self.selection = clickedNews
+ if clickedNews.type == .bundle || clickedNews.type == .category {
+ clickedNews.expanded.toggle()
+ }
+ }
+ .onChange(of: selection) {
+ self.articleText = nil
+ if let article = selection, article.type == .article {
+ if let articleFlavor = article.articleFlavors?.first,
+ let articleID = article.articleID {
+ Task {
+ if let articleText = await self.model.getNewsArticle(id: articleID, at: article.path, flavor: articleFlavor) {
+ self.articleText = articleText
+ }
+ }
+ if self.splitHidden.side != nil {
+ withAnimation(.easeOut(duration: 0.15)) {
+ self.splitHidden.side = nil
+ }
+ }
+
+ }
+ }
+ else {
+ if self.splitHidden.side != .bottom {
+ withAnimation(.easeOut(duration: 0.25)) {
+ self.splitHidden.side = .bottom
+ }
+ }
+ }
+ }
+ .onKeyPress(.rightArrow) {
+ if let s = selection, s.type == .bundle || s.type == .category {
+ s.expanded = true
+ return .handled
+ }
+ return .ignored
+ }
+ .onKeyPress(.leftArrow) {
+ if let s = selection, s.type == .bundle || s.type == .category {
+ s.expanded = false
+ return .handled
+ }
+ return .ignored
+ }
+ .overlay {
+ if !model.newsLoaded {
+ VStack {
+ ProgressView()
+ .controlSize(.regular)
+ }
+ .frame(maxWidth: .infinity)
+ }
+ }
+ }
+
+ var articleViewer: some View {
+ ScrollView {
+ if let selection = selection {
+ VStack(alignment: .leading, spacing: 0) {
+ if let poster = selection.articleUsername, let postDate = selection.articleDate {
+ HStack(alignment: .firstTextBaseline) {
+ Text(poster)
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .textSelection(.enabled)
+ .padding(.bottom, 16)
+ Spacer()
+ Text("\(NewsItemView.dateFormatter.string(from: postDate))")
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .textSelection(.enabled)
+ .padding(.bottom, 16)
+ }
+ }
+
+ Divider()
+
+ Text(selection.name).font(.title)
+ .textSelection(.enabled)
+ .padding(.bottom, 8)
+ .padding(.top, 16)
+
+ if let newsText = self.articleText {
+ Markdown(newsText)
+ .markdownTheme(.basic)
+ .textSelection(.enabled)
+ .lineSpacing(6)
+ .padding(.top, 16)
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .leading)
+ .padding()
+ }
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .transition(.move(edge: .bottom))
+ }
+}
struct NewsItemView: View {
@Environment(Hotline.self) private var model: Hotline
@@ -47,6 +232,7 @@ struct NewsItemView: View {
.scaledToFit()
.frame(width: 10)
.foregroundStyle(.secondary)
+ .opacity(0.5)
}
}
Text(news.name)
@@ -93,178 +279,6 @@ struct NewsItemView: View {
}
}
-struct NewsView: View {
- @Environment(Hotline.self) private var model: Hotline
-
- @State private var selection: NewsInfo?
- @State private var articleText: String?
-
- var body: some View {
- NavigationStack {
- VSplitView {
-
- // MARK: News Browser
- List(model.news, id: \.self, selection: $selection) { newsItem in
- NewsItemView(news: newsItem, depth: 0).tag(newsItem.id)
- }
-
- .frame(maxWidth: .infinity, minHeight: 100, idealHeight: 100)
- .environment(\.defaultMinListRowHeight, 34)
- .listStyle(.inset)
- .alternatingRowBackgrounds(.enabled)
- .task {
- if !model.newsLoaded {
- let _ = await model.getNewsList()
- }
- }
- .contextMenu(forSelectionType: NewsInfo.self) { items in
- // ...
- } primaryAction: { items in
- print("ITEMS?", items)
- guard let clickedNews = items.first else {
- return
- }
-
- self.selection = clickedNews
- if clickedNews.type == .bundle || clickedNews.type == .category {
- clickedNews.expanded.toggle()
- }
- }
- .onChange(of: selection) {
- if
- let article = selection,
- article.type == .article {
- self.articleText = nil
- if
-// let article = self.articleSelection.selectedArticle,
- let articleFlavor = article.articleFlavors?.first,
- let articleID = article.articleID {
- Task {
- if let articleText = await self.model.getNewsArticle(id: articleID, at: article.path, flavor: articleFlavor) {
- self.articleText = articleText
- }
- }
- }
- }
- }
- .onKeyPress(.rightArrow) {
- if let s = selection, s.type == .bundle || s.type == .category {
- s.expanded = true
- return .handled
- }
- return .ignored
- }
- .onKeyPress(.leftArrow) {
- if let s = selection, s.type == .bundle || s.type == .category {
- s.expanded = false
- return .handled
- }
- return .ignored
- }
- .overlay {
- if !model.newsLoaded {
- VStack {
- ProgressView()
- .controlSize(.large)
- }
- .frame(maxWidth: .infinity)
- }
- }
-
- // MARK: Article Viewer
- ScrollView {
- VStack(alignment: .leading, spacing: 0) {
- if let news = selection {
- if news.type == .article {
-
-// Text(news.name).font(.title)
-// .textSelection(.enabled)
-// .padding(.bottom, 8)
-
- if let poster = news.articleUsername, let postDate = news.articleDate {
- HStack(alignment: .firstTextBaseline) {
- Text(poster)
- .foregroundStyle(.secondary)
- .lineLimit(1)
- .truncationMode(.tail)
- .textSelection(.enabled)
- .padding(.bottom, 16)
- Spacer()
- Text("\(NewsItemView.dateFormatter.string(from: postDate))")
- .foregroundStyle(.secondary)
- .lineLimit(1)
- .truncationMode(.tail)
- .textSelection(.enabled)
- .padding(.bottom, 16)
- }
- }
-
- Divider()
-
- if let newsText = self.articleText {
- Markdown(newsText)
-// Text(newsText)
- .textSelection(.enabled)
- .lineSpacing(4)
- .padding(.top, 16)
- }
- }
- }
- }
- .frame(maxWidth: .infinity, alignment: .leading)
- .padding()
- }
- .frame(maxWidth: .infinity, minHeight: 200)
- }
- .frame(maxWidth: .infinity, maxHeight: .infinity)
- .background(Color(nsColor: .textBackgroundColor))
- }
- .toolbar {
- ToolbarItem(placement: .primaryAction) {
- Button {
-
- } label: {
- Image(systemName: "square.and.pencil")
- }
- }
-
- ToolbarItem(placement: .primaryAction) {
- Button {
-
- } label: {
- Image(systemName: "arrowshape.turn.up.left")
- }
- }
-
-// if let bannerImage = model.bannerImage {
-// ToolbarItem(placement: .primaryAction) {
-//// HStack {
-// bannerImage
-// .resizable()
-// .aspectRatio(contentMode: .fit)
-// .clipped()
-// .frame(minHeight: 60, alignment: .topLeading
-// )
-// .clipped()
-// }
-// .frame(maxWidth: .infinity)
-// .tag(MenuItem(name: "title", image: "", type: .banner))
-// .padding(.bottom, 16)
-
- // VStack(spacing: 0) {
- // bannerImage
- // .resizable()
- // .aspectRatio(contentMode: .fit)
- // .frame(maxWidth: .infinity, minHeight: 60, alignment: .topLeading)
- // .clipped()
- // }
- // .frame(maxWidth: .infinity)
-// }
-// }
- }
- }
-}
-
#Preview {
NewsView()
.environment(Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient()))
diff --git a/Hotline/macOS/TrackerView.swift b/Hotline/macOS/TrackerView.swift
index 268cd3e..23e6da2 100644
--- a/Hotline/macOS/TrackerView.swift
+++ b/Hotline/macOS/TrackerView.swift
@@ -237,7 +237,7 @@ struct TrackerView: View {
var bookmarks: [TrackerBookmark] = [
TrackerBookmark(type: .server, name: "RetroMac", address: "82.32.130.173"),
- TrackerBookmark(type: .server, name: "System 7 Today", address: "158.174.146.146"),
+ TrackerBookmark(type: .server, name: "System 7 Today", address: "62.116.228.147"),
TrackerBookmark(type: .tracker, name: "Featured Servers", address: "hltracker.com"),
TrackerBookmark(type: .tracker, name: "Agent79", address: "tracked.agent79.org"),
TrackerBookmark(type: .tracker, name: "Preterhuman", address: "tracker.preterhuman.net"),