aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Hotline.xcodeproj/project.pbxproj14
-rw-r--r--Hotline/Application-macOS.swift9
-rw-r--r--Hotline/Assets.xcassets/User.imageset/User.pngbin475 -> 459 bytes
-rw-r--r--Hotline/Assets.xcassets/User.imageset/User@2x.pngbin800 -> 838 bytes
-rw-r--r--Hotline/Assets.xcassets/User.imageset/User@3x.pngbin1160 -> 1329 bytes
-rw-r--r--Hotline/Models/Bookmark.swift269
-rw-r--r--Hotline/Models/BookmarksOld.swift (renamed from Hotline/Models/Bookmarks.swift)36
-rw-r--r--Hotline/Models/Server.swift6
-rw-r--r--Hotline/Sounds/Application-iOS.swift5
-rw-r--r--Hotline/macOS/NewsView.swift3
-rw-r--r--Hotline/macOS/ServerView.swift5
-rw-r--r--Hotline/macOS/TrackerView.swift607
12 files changed, 550 insertions, 404 deletions
diff --git a/Hotline.xcodeproj/project.pbxproj b/Hotline.xcodeproj/project.pbxproj
index e6d50a4..9237db3 100644
--- a/Hotline.xcodeproj/project.pbxproj
+++ b/Hotline.xcodeproj/project.pbxproj
@@ -12,6 +12,7 @@
DA0D698D2B1E7CF700C71DF5 /* UsersView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0D698C2B1E7CF700C71DF5 /* UsersView.swift */; platformFilter = ios; };
DA0D698F2B1E841600C71DF5 /* MessageBoardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0D698E2B1E841600C71DF5 /* MessageBoardView.swift */; platformFilter = ios; };
DA0D69912B1E894800C71DF5 /* FilesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA0D69902B1E894800C71DF5 /* FilesView.swift */; platformFilter = ios; };
+ DA20BBE12BF5237600B94E7C /* Bookmark.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA20BBE02BF5237600B94E7C /* Bookmark.swift */; };
DA2863D82B37AD1C00A7D050 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2863D72B37AD1C00A7D050 /* SettingsView.swift */; platformFilters = (macos, ); };
DA2863DA2B37BF6E00A7D050 /* Preferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2863D92B37BF6E00A7D050 /* Preferences.swift */; };
DA2863DD2B3E8B7000A7D050 /* FilePreview.swift in Sources */ = {isa = PBXBuildFile; fileRef = DA2863DC2B3E8B7000A7D050 /* FilePreview.swift */; };
@@ -64,7 +65,7 @@
DABE8C002B55E69800884D28 /* server-message.aiff in Resources */ = {isa = PBXBuildFile; fileRef = DABE8BFF2B55E69800884D28 /* server-message.aiff */; };
DABE8C022B55E69D00884D28 /* new-news.aiff in Resources */ = {isa = PBXBuildFile; fileRef = DABE8C012B55E69D00884D28 /* new-news.aiff */; };
DABE8C042B57940B00884D28 /* DAKeychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABE8C032B57940A00884D28 /* DAKeychain.swift */; };
- DABE8C062B57A06100884D28 /* Bookmarks.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABE8C052B57A06100884D28 /* Bookmarks.swift */; };
+ DABE8C062B57A06100884D28 /* BookmarksOld.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABE8C052B57A06100884D28 /* BookmarksOld.swift */; };
DABFCC292B1530DC009F40D2 /* FoundationExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DABFCC282B1530DC009F40D2 /* FoundationExtensions.swift */; };
DAC002192B21630900A6C290 /* SwiftUIExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC002182B21630900A6C290 /* SwiftUIExtensions.swift */; };
DAC3D97E2BC0F1ED00A727C9 /* Application-iOS.swift in Sources */ = {isa = PBXBuildFile; fileRef = DAC3D97D2BC0F1ED00A727C9 /* Application-iOS.swift */; platformFilter = ios; };
@@ -89,6 +90,7 @@
DA0D698C2B1E7CF700C71DF5 /* UsersView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsersView.swift; sourceTree = "<group>"; };
DA0D698E2B1E841600C71DF5 /* MessageBoardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MessageBoardView.swift; sourceTree = "<group>"; };
DA0D69902B1E894800C71DF5 /* FilesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FilesView.swift; sourceTree = "<group>"; };
+ DA20BBE02BF5237600B94E7C /* Bookmark.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bookmark.swift; sourceTree = "<group>"; };
DA2863D72B37AD1C00A7D050 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
DA2863D92B37BF6E00A7D050 /* Preferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Preferences.swift; sourceTree = "<group>"; };
DA2863DB2B37DBE000A7D050 /* Hotline.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Hotline.entitlements; sourceTree = "<group>"; };
@@ -142,7 +144,7 @@
DABE8BFF2B55E69800884D28 /* server-message.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = "server-message.aiff"; sourceTree = "<group>"; };
DABE8C012B55E69D00884D28 /* new-news.aiff */ = {isa = PBXFileReference; lastKnownFileType = audio.aiff; path = "new-news.aiff"; sourceTree = "<group>"; };
DABE8C032B57940A00884D28 /* DAKeychain.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DAKeychain.swift; sourceTree = "<group>"; };
- DABE8C052B57A06100884D28 /* Bookmarks.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Bookmarks.swift; sourceTree = "<group>"; };
+ DABE8C052B57A06100884D28 /* BookmarksOld.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksOld.swift; sourceTree = "<group>"; };
DABFCC282B1530DC009F40D2 /* FoundationExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FoundationExtensions.swift; sourceTree = "<group>"; };
DAC002182B21630900A6C290 /* SwiftUIExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftUIExtensions.swift; sourceTree = "<group>"; };
DAC3D97D2BC0F1ED00A727C9 /* Application-iOS.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Application-iOS.swift"; path = "Sounds/Application-iOS.swift"; sourceTree = "<group>"; };
@@ -294,7 +296,7 @@
DADDB28E2B238D850024040D /* Hotline.swift */,
DADDB28A2B22B31F0024040D /* Tracker.swift */,
DAC3D9822BC33FD000A727C9 /* ApplicationState.swift */,
- DABE8C052B57A06100884D28 /* Bookmarks.swift */,
+ DABE8C052B57A06100884D28 /* BookmarksOld.swift */,
DA2863D92B37BF6E00A7D050 /* Preferences.swift */,
DADDB28C2B22B5920024040D /* Server.swift */,
DA32CD482B2931640053B98B /* User.swift */,
@@ -307,6 +309,7 @@
DA32CD4E2B2931CC0053B98B /* NewsInfo.swift */,
DA72A0E12B4DAA4000A0F48A /* NewsArticle.swift */,
DAAEE66C2B475F1400A5BA07 /* PreviewFileInfo.swift */,
+ DA20BBE02BF5237600B94E7C /* Bookmark.swift */,
);
path = Models;
sourceTree = "<group>";
@@ -318,6 +321,7 @@
DAE734F82B2E4185000C56F6 /* ServerView.swift */,
DAE734FE2B2E6750000C56F6 /* ChatView.swift */,
DAE735042B3218D8000C56F6 /* NewsView.swift */,
+ DA2D98112BF29D5F0027E4BD /* NewsItemView.swift */,
DA72A0DC2B4CD0BF00A0F48A /* NewsEditorView.swift */,
DAE735022B30C0BB000C56F6 /* MessageView.swift */,
DAE734FC2B2E65E9000C56F6 /* MessageBoardView.swift */,
@@ -330,7 +334,6 @@
DA55AC762BE589F700034857 /* AboutView.swift */,
DA65499B2BEC3FBD00EDB697 /* ServerAgreementView.swift */,
DA6549992BEC280E00EDB697 /* ServerMessageView.swift */,
- DA2D98112BF29D5F0027E4BD /* NewsItemView.swift */,
);
path = macOS;
sourceTree = "<group>";
@@ -448,8 +451,9 @@
DA55AC732BE42AF000034857 /* AsyncLinkPreview.swift in Sources */,
DA65499E2BEC438A00EDB697 /* NSWindowBridge.swift in Sources */,
DAE735032B30C0BB000C56F6 /* MessageView.swift in Sources */,
- DABE8C062B57A06100884D28 /* Bookmarks.swift in Sources */,
+ DABE8C062B57A06100884D28 /* BookmarksOld.swift in Sources */,
DA32CD4B2B29318E0053B98B /* FileInfo.swift in Sources */,
+ DA20BBE12BF5237600B94E7C /* Bookmark.swift in Sources */,
DA9CAFCB2B126E3300CDA197 /* HotlineTrackerClient.swift in Sources */,
DA2863DA2B37BF6E00A7D050 /* Preferences.swift in Sources */,
DA32CD4F2B2931CC0053B98B /* NewsInfo.swift in Sources */,
diff --git a/Hotline/Application-macOS.swift b/Hotline/Application-macOS.swift
index 3ddf903..f6b5bc5 100644
--- a/Hotline/Application-macOS.swift
+++ b/Hotline/Application-macOS.swift
@@ -2,8 +2,6 @@ import SwiftUI
import SwiftData
import UniformTypeIdentifiers
-#if os(macOS)
-
@Observable
final class AppLaunchState {
static let shared = AppLaunchState()
@@ -35,8 +33,7 @@ struct Application: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
- @State private var soundEffects = SoundEffectPlayer()
- @State private var bookmarks = Bookmarks()
+ @State private var bookmarks = BookmarksOld()
@State private var hotlinePanel: HotlinePanel? = nil
@FocusedValue(\.activeHotlineModel) private var activeHotline: Hotline?
@@ -49,6 +46,7 @@ struct Application: App {
.frame(minWidth: 250, minHeight: 250)
.environment(bookmarks)
}
+ .modelContainer(for: Bookmark.self, isAutosaveEnabled: true, isUndoEnabled: true)
.defaultSize(width: 700, height: 550)
.defaultPosition(.center)
.keyboardShortcut(.init("R"), modifiers: .command)
@@ -82,7 +80,6 @@ struct Application: App {
WindowGroup(id: "server", for: Server.self) { server in
ServerView(server: server)
.frame(minWidth: 430, minHeight: 300)
- .environment(soundEffects)
.environment(bookmarks)
} defaultValue: {
Server(name: nil, description: nil, address: "")
@@ -241,5 +238,3 @@ struct Application: App {
}
}
}
-
-#endif
diff --git a/Hotline/Assets.xcassets/User.imageset/User.png b/Hotline/Assets.xcassets/User.imageset/User.png
index 0bf463b..0a5c57e 100644
--- a/Hotline/Assets.xcassets/User.imageset/User.png
+++ b/Hotline/Assets.xcassets/User.imageset/User.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/User.imageset/User@2x.png b/Hotline/Assets.xcassets/User.imageset/User@2x.png
index 27712aa..b64c6a5 100644
--- a/Hotline/Assets.xcassets/User.imageset/User@2x.png
+++ b/Hotline/Assets.xcassets/User.imageset/User@2x.png
Binary files differ
diff --git a/Hotline/Assets.xcassets/User.imageset/User@3x.png b/Hotline/Assets.xcassets/User.imageset/User@3x.png
index aeda6cf..cb593c9 100644
--- a/Hotline/Assets.xcassets/User.imageset/User@3x.png
+++ b/Hotline/Assets.xcassets/User.imageset/User@3x.png
Binary files differ
diff --git a/Hotline/Models/Bookmark.swift b/Hotline/Models/Bookmark.swift
new file mode 100644
index 0000000..160de30
--- /dev/null
+++ b/Hotline/Models/Bookmark.swift
@@ -0,0 +1,269 @@
+import Foundation
+import SwiftData
+
+enum BookmarkType: Codable {
+ case tracker
+ case server
+ case temporary
+}
+
+@Model
+final class Bookmark {
+ var type: BookmarkType
+ var order: Int = 0
+
+ var name: String
+ var address: String
+ var port: Int
+
+ @Attribute(.allowsCloudEncryption)
+ var login: String?
+
+ @Attribute(.allowsCloudEncryption)
+ var password: String?
+
+ @Attribute(.ephemeral)
+ var expanded: Bool = false
+
+ @Attribute(.ephemeral)
+ var loading: Bool = false
+
+ @Attribute(.ephemeral)
+ var serverDescription: String? = nil
+
+ @Transient
+ var servers: [Bookmark] = []
+
+ @Transient
+ var displayAddress: String {
+ switch self.type {
+ case .tracker:
+ if self.port == HotlinePorts.DefaultTrackerPort {
+ return self.address
+ }
+ else {
+ return "\(self.address):\(String(self.port))"
+ }
+
+ case .server, .temporary:
+ if self.port == HotlinePorts.DefaultServerPort {
+ return self.address
+ }
+ else {
+ return "\(self.address):\(String(self.port))"
+ }
+ }
+
+// if let s = server {
+// if s.port == HotlinePorts.DefaultServerPort {
+// return s.address
+// }
+// else {
+// return "\(s.address):\(s.port)"
+// }
+// }
+// else if let b = bookmark {
+// if b.port == HotlinePorts.DefaultServerPort {
+// return b.address
+// }
+// else {
+// return "\(b.address):\(b.port)"
+// }
+// }
+
+// return nil
+ }
+
+ @Transient
+ var server: Server? {
+ switch self.type {
+ case .tracker:
+ return nil
+
+ case .server, .temporary:
+ return Server(name: self.name, description: self.serverDescription, address: self.address, port: self.port, login: self.login, password: self.password)
+ }
+ }
+
+ static let DefaultBookmarks: [Bookmark] = [
+ Bookmark(type: .server, name: "The Mobius Strip", address: "67.174.208.111", port: HotlinePorts.DefaultServerPort),
+ Bookmark(type: .server, name: "System 7 Today", address: "hotline.system7today.com", port: HotlinePorts.DefaultServerPort),
+ Bookmark(type: .tracker, name: "Featured Servers", address: "hltracker.com", port: HotlinePorts.DefaultTrackerPort)
+ ]
+
+ init(type: BookmarkType, name: String, address: String, port: Int, login: String? = nil, password: String? = nil) {
+ self.type = type
+ self.name = name
+ self.address = address
+ self.port = port
+
+ self.login = login
+ self.password = password
+ }
+
+ init(temporaryServer server: Server) {
+ self.type = .temporary
+
+ self.name = server.name ?? server.address
+ self.address = server.address
+ self.port = server.port
+
+ self.serverDescription = server.description
+ }
+
+ static func populateDefaults(force: Bool = false, context: ModelContext) {
+ if force || Bookmark.fetchCount(context: context) == 0 {
+ Bookmark.add(Bookmark.DefaultBookmarks, context: context)
+ }
+ }
+
+ static func fetchAll(context: ModelContext) -> [Bookmark] {
+ let fetchDescriptor = FetchDescriptor<Bookmark>(sortBy: [.init(\.order)])
+ do {
+ let bookmarks: [Bookmark] = try context.fetch(fetchDescriptor)
+ return bookmarks
+ }
+ catch {
+ return []
+ }
+ }
+
+ static func fetchCount(context: ModelContext) -> Int {
+ let descriptor = FetchDescriptor<Bookmark>()
+ return (try? context.fetchCount(descriptor)) ?? 0
+ }
+
+ static func deleteAll(context: ModelContext) {
+ try? context.delete(model: Bookmark.self)
+ }
+
+ static func add(_ bookmark: Bookmark, context: ModelContext) {
+ guard bookmark.type != .temporary else {
+ print("Bookmark: Attempting to add temporary bookmark to store. Aborting.")
+ return
+ }
+
+ let existingBookmarks = Bookmark.fetchAll(context: context)
+
+ // Reindex bookmarks before insert.
+ for existingBookmark in existingBookmarks {
+ existingBookmark.order += 1
+ }
+
+ // Insert new bookmark at start.
+ bookmark.order = 0
+ context.insert(bookmark)
+ }
+
+ static func add(_ bookmarks: [Bookmark], context: ModelContext) {
+ let existingBookmarks = Bookmark.fetchAll(context: context)
+
+ // Reindex bookmarks before insert.
+ for existingBookmark in existingBookmarks {
+ existingBookmark.order += bookmarks.count
+ }
+
+ // Insert new bookmarks at start.
+ var bookmarkIndex = 0
+ for newBookmark in bookmarks {
+ newBookmark.order = bookmarkIndex
+ context.insert(newBookmark)
+ bookmarkIndex += 1
+
+ print("Bookmark: added \(newBookmark.name)")
+ }
+ }
+
+ static func delete(_ bookmark: Bookmark, context: ModelContext) {
+ // Delete bookmark
+ context.delete(bookmark)
+
+ // Reindex bookmarks
+ let existingBookmarks = Bookmark.fetchAll(context: context)
+ var index = 0
+ for existingBookmark in existingBookmarks {
+ existingBookmark.order = index
+ index += 1
+ }
+ }
+
+ static func delete(at indexes: IndexSet, context: ModelContext) {
+ var existingBookmarks = Bookmark.fetchAll(context: context)
+// existingBookmarks.remove(atOffsets: indexes)
+ let bookmarksToDelete = indexes.map { existingBookmarks[$0] }
+
+ // Delete bookmark
+ for bookmark in bookmarksToDelete {
+ context.delete(bookmark)
+ }
+
+ // Reindex bookmarks
+ var index = 0
+ existingBookmarks.remove(atOffsets: indexes)
+ for existingBookmark in existingBookmarks {
+ existingBookmark.order = index
+ index += 1
+ }
+
+ do {
+ try context.save()
+ }
+ catch {
+ print("Bookmark: Failed to save bookmark deletions")
+ }
+ }
+
+ static func move(_ indexes: IndexSet, to newIndex: Int, context: ModelContext) {
+ guard Bookmark.fetchCount(context: context) >= indexes.count else {
+ print("Bookmark: Not enough bookmarks to move requested set")
+ return
+ }
+
+ // Perform move
+ var existingBookmarks = Bookmark.fetchAll(context: context)
+ existingBookmarks.move(fromOffsets: indexes, toOffset: newIndex)
+
+ // Reindex bookmarks
+ var index = 0
+ for existingBookmark in existingBookmarks {
+ existingBookmark.order = index
+ index += 1
+ }
+
+ do {
+ try context.save()
+ }
+ catch {
+ print("Bookmark: Failed to save bookmark reordering")
+ }
+ }
+
+ func fetchServers() async {
+ guard self.type == .tracker else {
+ // self.loading = false
+ return
+ }
+
+ DispatchQueue.main.sync {
+ self.loading = true
+ }
+
+ var fetchedBookmarks: [Bookmark] = []
+
+ let client = HotlineTrackerClient()
+ if let fetchedServers: [HotlineServer] = try? await client.fetchServers(address: self.address, port: self.port) {
+ for fetchedServer in fetchedServers {
+ if let serverName = fetchedServer.name {
+ let server = Server(name: serverName, description: fetchedServer.description, address: fetchedServer.address, port: Int(fetchedServer.port), users: Int(fetchedServer.users))
+ fetchedBookmarks.append(Bookmark(temporaryServer: server))
+ }
+ }
+ }
+
+ let newServers = fetchedBookmarks
+ DispatchQueue.main.sync {
+ self.servers = newServers
+ self.loading = false
+ }
+ }
+}
diff --git a/Hotline/Models/Bookmarks.swift b/Hotline/Models/BookmarksOld.swift
index 3f4971c..0a7bb16 100644
--- a/Hotline/Models/Bookmarks.swift
+++ b/Hotline/Models/BookmarksOld.swift
@@ -5,20 +5,20 @@ extension NSNotification {
static let BookmarkRemoved = Notification.Name("BookmarkRemoved")
}
-enum BookmarkType: String, Codable {
+enum BookmarkOldType: String, Codable {
case tracker = "tracker"
case server = "server"
}
-struct Bookmark: Codable, Equatable {
- let type: BookmarkType
+struct BookmarkOld: Codable, Equatable {
+ let type: BookmarkOldType
let name: String
let address: String
let port: Int
let login: String?
let password: String?
- init(type: BookmarkType, name: String, address: String, port: Int = HotlinePorts.DefaultServerPort, login: String? = nil, password: String? = nil) {
+ init(type: BookmarkOldType, name: String, address: String, port: Int = HotlinePorts.DefaultServerPort, login: String? = nil, password: String? = nil) {
self.type = type
self.name = name
self.address = address
@@ -28,20 +28,20 @@ struct Bookmark: Codable, Equatable {
}
}
-@Observable final class Bookmarks {
- var bookmarks: [Bookmark]? = nil
+@Observable final class BookmarksOld {
+ var bookmarks: [BookmarkOld]? = nil
- static let DefaultBookmarks: [Bookmark] = [
- Bookmark(type: .server, name: "System 7 Today", address: "hotline.system7today.com"),
- Bookmark(type: .server, name: "The Mobius Strip", address: "67.174.208.111"),
- Bookmark(type: .tracker, name: "Featured Servers", address: "hltracker.com"),
+ static let DefaultBookmarks: [BookmarkOld] = [
+ BookmarkOld(type: .server, name: "System 7 Today", address: "hotline.system7today.com"),
+ BookmarkOld(type: .server, name: "The Mobius Strip", address: "67.174.208.111"),
+ BookmarkOld(type: .tracker, name: "Featured Servers", address: "hltracker.com"),
]
init() {
self.load()
}
- func apply(_ newBookmarks: [Bookmark], save shouldSave: Bool = true) {
+ func apply(_ newBookmarks: [BookmarkOld], save shouldSave: Bool = true) {
self.bookmarks = newBookmarks
if shouldSave {
self.save()
@@ -53,21 +53,21 @@ struct Bookmark: Codable, Equatable {
let jsonData: Data? = jsonString?.data(using: .utf8, allowLossyConversion: false)
let decoder = JSONDecoder()
- var decodedBookmarks = try? decoder.decode([Bookmark].self, from: jsonData ?? Data())
+ var decodedBookmarks = try? decoder.decode([BookmarkOld].self, from: jsonData ?? Data())
if decodedBookmarks == nil || decodedBookmarks?.isEmpty == true {
print("Bookmarks: using default bookmarks")
- decodedBookmarks = Bookmarks.DefaultBookmarks
+ decodedBookmarks = BookmarksOld.DefaultBookmarks
}
else {
print("Bookmarks: using saved bookmarks")
}
- self.bookmarks = [Bookmark](decodedBookmarks!)
+ self.bookmarks = [BookmarkOld](decodedBookmarks!)
}
func save() {
var bookmarksToSave = self.bookmarks
- if bookmarksToSave == Bookmarks.DefaultBookmarks {
+ if bookmarksToSave == BookmarksOld.DefaultBookmarks {
print("Bookmarks: skipping saving default bookmarks")
bookmarksToSave = []
}
@@ -83,7 +83,7 @@ struct Bookmark: Codable, Equatable {
// MARK: -
- func add(_ bookmark: Bookmark, save shouldSave: Bool = true) {
+ func add(_ bookmark: BookmarkOld, save shouldSave: Bool = true) {
self.bookmarks?.insert(bookmark, at: 0)
if shouldSave {
@@ -95,7 +95,7 @@ struct Bookmark: Codable, Equatable {
}
}
- func delete(_ bookmark: Bookmark, save shouldSave: Bool = true) -> Bool {
+ func delete(_ bookmark: BookmarkOld, save shouldSave: Bool = true) -> Bool {
if let i = self.bookmarks?.firstIndex(where: { b in b.address.lowercased() == bookmark.address.lowercased() && b.port == bookmark.port }) {
self.bookmarks?.remove(at: i)
@@ -112,7 +112,7 @@ struct Bookmark: Codable, Equatable {
return false
}
- func update(_ bookmark: Bookmark, save shouldSave: Bool = true) -> Bool {
+ func update(_ bookmark: BookmarkOld, save shouldSave: Bool = true) -> Bool {
if let i = self.bookmarks?.firstIndex(where: { b in b.address.lowercased() == bookmark.address.lowercased() && b.port == bookmark.port }) {
self.bookmarks?[i] = bookmark
if shouldSave {
diff --git a/Hotline/Models/Server.swift b/Hotline/Models/Server.swift
index 27d45c6..e0fe38b 100644
--- a/Hotline/Models/Server.swift
+++ b/Hotline/Models/Server.swift
@@ -10,14 +10,14 @@ struct Server: Codable {
var login: String
var password: String
- init(name: String?, description: String?, address: String, port: Int = HotlinePorts.DefaultServerPort, users: Int = 0, login: String = "", password: String = "") {
+ init(name: String?, description: String?, address: String, port: Int = HotlinePorts.DefaultServerPort, users: Int = 0, login: String? = nil, password: String? = nil) {
self.name = name
self.description = description
self.address = address.lowercased()
self.port = port
self.users = users
- self.login = login
- self.password = password
+ self.login = login ?? ""
+ self.password = password ?? ""
}
init?(url: URL) {
diff --git a/Hotline/Sounds/Application-iOS.swift b/Hotline/Sounds/Application-iOS.swift
index 055c8f1..dc3a9d0 100644
--- a/Hotline/Sounds/Application-iOS.swift
+++ b/Hotline/Sounds/Application-iOS.swift
@@ -2,13 +2,10 @@ import SwiftUI
import SwiftData
import UniformTypeIdentifiers
-#if os(iOS)
-
@main
struct Application: App {
private var model = Hotline(trackerClient: HotlineTrackerClient(), client: HotlineClient())
- @State private var soundEffects = SoundEffectPlayer()
@State private var bookmarks = Bookmarks()
@FocusedValue(\.activeHotlineModel) private var activeHotline: Hotline?
@@ -21,5 +18,3 @@ struct Application: App {
}
}
}
-
-#endif
diff --git a/Hotline/macOS/NewsView.swift b/Hotline/macOS/NewsView.swift
index 6f24193..8e10b19 100644
--- a/Hotline/macOS/NewsView.swift
+++ b/Hotline/macOS/NewsView.swift
@@ -13,7 +13,7 @@ struct NewsView: View {
@State private var splitFraction = FractionHolder.usingUserDefaults(0.25, key: "News Split Fraction")
@State private var editorOpen: Bool = false
@State private var replyOpen: Bool = false
- @State private var loading: Bool = true
+ @State private var loading: Bool = false
var body: some View {
Group {
@@ -65,6 +65,7 @@ struct NewsView: View {
}
.task {
if !model.newsLoaded {
+ loading = true
await model.getNewsList()
loading = false
}
diff --git a/Hotline/macOS/ServerView.swift b/Hotline/macOS/ServerView.swift
index acd3ac2..64c3fbd 100644
--- a/Hotline/macOS/ServerView.swift
+++ b/Hotline/macOS/ServerView.swift
@@ -127,8 +127,7 @@ enum ServerNavigationType: Identifiable, Hashable, Equatable {
}
struct ServerView: View {
- @Environment(SoundEffectPlayer.self) private var soundEffects: SoundEffectPlayer
- @Environment(Bookmarks.self) private var bookmarks: Bookmarks
+ @Environment(BookmarksOld.self) private var bookmarks: BookmarksOld
@Environment(\.dismiss) var dismiss
@Environment(\.colorScheme) private var colorScheme
@Environment(\.controlActiveState) private var controlActiveState
@@ -335,7 +334,7 @@ struct ServerView: View {
let password: String? = connectPassword.isEmpty ? nil : connectPassword
if !host.isEmpty {
- let _ = bookmarks.add(Bookmark(type: .server, name: name, address: host, port: port, login: login, password: password))
+ let _ = bookmarks.add(BookmarkOld(type: .server, name: name, address: host, port: port, login: login, password: password))
}
}
}
diff --git a/Hotline/macOS/TrackerView.swift b/Hotline/macOS/TrackerView.swift
index f9755f8..ab70389 100644
--- a/Hotline/macOS/TrackerView.swift
+++ b/Hotline/macOS/TrackerView.swift
@@ -1,146 +1,92 @@
import SwiftUI
+import SwiftData
struct TrackerView: View {
- @Environment(Bookmarks.self) private var bookmarks: Bookmarks
+// @Environment(BookmarksOld.self) private var bookmarks: BookmarksOld
@Environment(\.colorScheme) private var colorScheme
@Environment(\.openWindow) private var openWindow
@Environment(\.controlActiveState) private var controlActiveState
+ @Environment(\.modelContext) private var modelContext
- @MainActor
- func refresh() async {
-
- // When a tracker is selected, refresh only that tracker.
- if
- let selectedItem = selection,
- let bookmark = selectedItem.bookmark,
- bookmark.type == .tracker {
- if !selectedItem.expanded {
- selectedItem.expanded = true
- }
- else {
- await selectedItem.loadServers()
- }
- return
- }
-
- // Otherwise refresh/expand all trackers.
- for server in self.servers {
- if
- let bookmark = server.bookmark,
- bookmark.type == .tracker {
- if !server.expanded {
- server.expanded = true
- }
- else {
- Task {
- await server.loadServers()
- }
- }
- }
- }
- }
-
- @State private var servers: [TrackerItem] = []
- @State private var selection: TrackerItem? = nil
-
- @State private var scrollOffset: CGFloat = CGFloat.zero
- @State private var initialLoadComplete = false
@State private var refreshing = false
- @State private var topBarOpacity: Double = 1.0
- @State private var connectVisible = false
- @State private var connectDismissed = true
- @State private var serverVisible = false
-
@State private var trackerSheetPresented: Bool = false
- @State private var trackerName: String = ""
- @State private var trackerAddress: String = ""
+ @State private var trackerSheetBookmark: Bookmark? = nil
- var trackerBookmarkSheet: some View {
- VStack(alignment: .leading) {
- Text("Type the address and name of a Hotline Tracker:")
- .foregroundStyle(.secondary)
- .padding(.bottom, 8)
- Form {
- Group {
- TextField(text: $trackerAddress) {
- Text("Address:")
- }
- TextField(text: $trackerName, prompt: Text("Optional")) {
- Text("Name:")
+ @Query(sort: \Bookmark.order) private var bookmarks: [Bookmark]
+ @State private var selection: Bookmark? = nil
+
+ var body: some View {
+ List(selection: $selection) {
+ ForEach(bookmarks, id: \.self) { bookmark in
+ TrackerItemView(bookmark: bookmark)
+ .tag(bookmark)
+
+ if bookmark.type == .tracker && bookmark.expanded {
+ ForEach(bookmark.servers, id: \.self) { trackedServer in
+ TrackerItemView(bookmark: trackedServer)
+ .moveDisabled(true)
+ .deleteDisabled(true)
+ .tag(trackedServer)
}
}
- .textFieldStyle(.roundedBorder)
- .controlSize(.large)
}
- }
- .frame(width: 300)
- .fixedSize(horizontal: true, vertical: true)
- .padding()
- .toolbar {
- ToolbarItem(placement: .confirmationAction) {
- Button("Add Tracker") {
- var displayName = trackerName.trimmingCharacters(in: .whitespacesAndNewlines)
- let (host, port) = Tracker.parseTrackerAddressAndPort(trackerAddress)
-
- if displayName.isEmpty {
- displayName = host
- }
-
- if !displayName.isEmpty && !host.isEmpty {
- if !host.isEmpty {
- let _ = bookmarks.add(Bookmark(type: .tracker, name: displayName, address: host, port: port))
- trackerSheetPresented = false
- trackerName = ""
- trackerAddress = ""
- }
- }
- }
+ .onMove { movedIndexes, destinationIndex in
+ Bookmark.move(movedIndexes, to: destinationIndex, context: modelContext)
}
- ToolbarItem(placement: .cancellationAction) {
- Button("Cancel") {
- trackerSheetPresented = false
- trackerName = ""
- trackerAddress = ""
- }
+ .onDelete { deletedIndexes in
+ Bookmark.delete(at: deletedIndexes, context: modelContext)
}
}
- }
-
- var body: some View {
- List($servers, id: \.self, editActions: [.move], selection: $selection) { $item in
- TrackerItemView(item: item, depth: 0)
- .deleteDisabled(!item.editable)
- .moveDisabled(!item.editable)
- .tag(item)
+ .onDeleteCommand {
+ if let bookmark = selection,
+ bookmark.type != .temporary {
+ Bookmark.delete(bookmark, context: modelContext)
+ }
}
.environment(\.defaultMinListRowHeight, 34)
.listStyle(.inset)
.alternatingRowBackgrounds(.enabled)
- .onDeleteCommand {
- if let sel = selection, let bookmark = sel.bookmark {
- let _ = bookmarks.delete(bookmark)
- if let i = self.servers.firstIndex(where: { $0.id == sel.id }) {
- self.servers.remove(at: i)
- }
- selection = nil
- }
+ .onAppear {
+// Bookmark.deleteAll(context: modelContext)
+
+ // Make sure default bookmarks are there when empty.
+ Bookmark.populateDefaults(context: modelContext)
}
- .contextMenu(forSelectionType: TrackerItem.self) { items in
+ .contextMenu(forSelectionType: Bookmark.self) { items in
if let item = items.first {
- if let server = item.server {
+ if item.type == .temporary {
Button {
- let _ = bookmarks.add(Bookmark(type: .server, name: server.name ?? server.address, address: server.address, port: server.port))
+ let newBookmark = Bookmark(type: .server, name: item.name, address: item.address, port: item.port, login: item.login, password: item.password)
+ Bookmark.add(newBookmark, context: modelContext)
} label: {
Label("Bookmark", systemImage: "bookmark")
}
+
+ Divider()
+ }
+
+ Button {
+ NSPasteboard.general.clearContents()
+ NSPasteboard.general.setString(item.displayAddress, forType: .string)
+ } label: {
+ Label("Copy Address", systemImage: "doc.on.doc")
}
- if let address = item.displayAddress {
+ if item.type == .tracker || item.type == .server {
+ Divider()
+
+ if item.type == .tracker {
+ Button {
+ trackerSheetBookmark = item
+ } label: {
+ Label("Edit Tracker...", systemImage: "pencil")
+ }
+ }
+
Button {
- NSPasteboard.general.clearContents()
- NSPasteboard.general.setString(address, forType: .string)
+ Bookmark.delete(item, context: modelContext)
} label: {
- Label("Copy Address", systemImage: "doc.on.doc")
+ Label(item.type == .tracker ? "Delete Tracker" : "Delete Bookmark", systemImage: "trash")
}
}
}
@@ -149,41 +95,42 @@ struct TrackerView: View {
return
}
- if
- let bookmark = clickedItem.bookmark,
- bookmark.type == .tracker {
- clickedItem.expanded.toggle()
+ if clickedItem.type == .tracker {
+ if let event = NSApp.currentEvent,
+ event.modifierFlags.contains(.option) {
+ trackerSheetBookmark = clickedItem
+ }
+ else {
+ clickedItem.expanded.toggle()
+ }
}
else if let server = clickedItem.server {
openWindow(id: "server", value: server)
}
- else if let bookmark = clickedItem.bookmark, bookmark.type == .server {
- let server = Server(name: bookmark.name, description: nil, address: bookmark.address, port: bookmark.port, login: bookmark.login ?? "", password: bookmark.password ?? "")
- openWindow(id: "server", value: server)
- }
}
.onKeyPress(.rightArrow) {
if
- let selectedItem = selection,
- let bookmark = selectedItem.bookmark,
+ let bookmark = selection,
bookmark.type == .tracker {
- selectedItem.expanded = true
+ bookmark.expanded = true
return .handled
}
return .ignored
}
.onKeyPress(.leftArrow) {
if
- let selectedItem = selection,
- let bookmark = selectedItem.bookmark,
+ let bookmark = selection,
bookmark.type == .tracker {
- selectedItem.expanded = false
+ bookmark.expanded = false
return .handled
}
return .ignored
}
+ .sheet(item: $trackerSheetBookmark) { item in
+ TrackerBookmarkSheet(item)
+ }
.sheet(isPresented: $trackerSheetPresented) {
- trackerBookmarkSheet
+ TrackerBookmarkSheet()
}
.navigationTitle("Servers")
.toolbar {
@@ -196,16 +143,16 @@ struct TrackerView: View {
.frame(width: 9)
.opacity(controlActiveState == .inactive ? 0.5 : 1.0)
}
+
ToolbarItem(placement: .primaryAction) {
Button {
- Task {
- initialLoadComplete = false
- await refresh()
- initialLoadComplete = true
- }
+ refreshing = true
+ refresh()
+ refreshing = false
} label: {
Label("Refresh", systemImage: "arrow.clockwise")
}
+ .disabled(refreshing)
.help("Refresh Trackers")
}
@@ -227,31 +174,15 @@ struct TrackerView: View {
.help("Connect to Server")
}
}
- .task {
- guard let bookmarks = bookmarks.bookmarks else {
- return
- }
- // Add initial items to tracker list.
- var items: [TrackerItem] = []
- for bookmark in bookmarks {
- items.append(TrackerItem(bookmark: bookmark))
- }
- self.servers = items
- }
- .onChange(of: servers) {
- Task {
- saveBookmarks()
- }
- }
- .onReceive(NotificationCenter.default.publisher(for: NSNotification.BookmarkAdded)) { notification in
- guard let bookmarks = bookmarks.bookmarks, let userInfo = notification.userInfo else {
- return
- }
-
- if let i = userInfo["index"] as? Int, bookmarks.count > i {
- self.servers.insert(TrackerItem(bookmark: bookmarks[i]), at: i)
- }
- }
+// .onReceive(NotificationCenter.default.publisher(for: NSNotification.BookmarkAdded)) { notification in
+// guard let bookmarks = bookmarks.bookmarks, let userInfo = notification.userInfo else {
+// return
+// }
+//
+// if let i = userInfo["index"] as? Int, bookmarks.count > i {
+// self.servers.insert(TrackerItem(bookmark: bookmarks[i]), at: i)
+// }
+// }
.onOpenURL(perform: { url in
if let s = Server(url: url) {
openWindow(id: "server", value: s)
@@ -259,34 +190,131 @@ struct TrackerView: View {
})
}
- private func saveBookmarks() {
- var newBookmarks: [Bookmark] = []
- for server in self.servers {
- if let b = server.bookmark {
- newBookmarks.append(b)
+ func refresh() {
+ // When a tracker is selected, refresh only that tracker.
+ if
+ let selectedBookmark = selection,
+ selectedBookmark.type == .tracker {
+ if !selectedBookmark.expanded {
+ selectedBookmark.expanded = true
+ }
+ else {
+ Task {
+ await selectedBookmark.fetchServers()
+ }
}
+ return
}
- bookmarks.apply(newBookmarks)
+ // Otherwise refresh/expand all trackers.
+ for bookmark in self.bookmarks {
+ if bookmark.type == .tracker {
+ if !bookmark.expanded {
+ bookmark.expanded = true
+ }
+ else {
+ Task {
+ await bookmark.fetchServers()
+ }
+ }
+ }
+ }
}
}
-struct TrackerItemView: View {
- @State var expanded = false
- @State var loading = false
+struct TrackerBookmarkSheet: View {
+ @Environment(\.dismiss) private var dismiss
+ @Environment(\.modelContext) private var modelContext
+
+ @State private var bookmark: Bookmark? = nil
+ @State private var trackerAddress: String = ""
+ @State private var trackerName: String = ""
- var item: TrackerItem
- let depth: Int
+ init() {
+
+ }
+
+ init(_ editingBookmark: Bookmark) {
+ _bookmark = .init(initialValue: editingBookmark)
+ _trackerAddress = .init(initialValue: editingBookmark.displayAddress)
+ _trackerName = .init(initialValue: editingBookmark.name)
+ }
var body: some View {
- HStack {
- if
- let bookmark = item.bookmark,
- bookmark.type == .tracker {
+ VStack(alignment: .leading) {
+ Text("Type the address and name of a Hotline Tracker:")
+ .foregroundStyle(.secondary)
+ .padding(.bottom, 8)
+ Form {
+ Group {
+ TextField(text: $trackerAddress) {
+ Text("Address:")
+ }
+ TextField(text: $trackerName, prompt: Text("Optional")) {
+ Text("Name:")
+ }
+ }
+ .textFieldStyle(.roundedBorder)
+ .controlSize(.large)
+ }
+ }
+ .frame(width: 300)
+ .fixedSize(horizontal: true, vertical: true)
+ .padding()
+ .toolbar {
+ ToolbarItem(placement: .confirmationAction) {
+ Button(self.bookmark != nil ? "Save Tracker" : "Add Tracker") {
+ var displayName = trackerName.trimmingCharacters(in: .whitespacesAndNewlines)
+ let (host, port) = Tracker.parseTrackerAddressAndPort(trackerAddress)
+
+ if displayName.isEmpty {
+ displayName = host
+ }
+
+ if !displayName.isEmpty && !host.isEmpty {
+ if !host.isEmpty {
+ if self.bookmark != nil {
+ // We're editing an existing bookmark.
+ self.bookmark?.name = displayName
+ self.bookmark?.address = host
+ self.bookmark?.port = port
+ }
+ else {
+ // We're creating a new bookmark.
+ let newBookmark = Bookmark(type: .tracker, name: displayName, address: host, port: port)
+ Bookmark.add(newBookmark, context: modelContext)
+ }
+
+ self.trackerName = ""
+ self.trackerAddress = ""
+
+ dismiss()
+ }
+ }
+ }
+ }
+ ToolbarItem(placement: .cancellationAction) {
+ Button("Cancel") {
+ self.trackerName = ""
+ self.trackerAddress = ""
+
+ dismiss()
+ }
+ }
+ }
+ }
+}
+
+struct TrackerItemView: View {
+ let bookmark: Bookmark
+
+ var body: some View {
+ HStack(alignment: .center, spacing: 8) {
+ if bookmark.type == .tracker {
Button {
- item.expanded.toggle()
+ bookmark.expanded.toggle()
} label: {
- Text(Image(systemName: item.expanded ? "chevron.down" : "chevron.right"))
+ Text(Image(systemName: bookmark.expanded ? "chevron.down" : "chevron.right"))
.bold()
.font(.system(size: 10))
.opacity(0.5)
@@ -296,208 +324,63 @@ struct TrackerItemView: View {
.frame(width: 10)
.padding(.leading, 4)
}
- else {
- HStack {
- if let bookmark = item.bookmark, bookmark.type == .server {
- Image(systemName: "bookmark.fill")
- .resizable()
- .renderingMode(.template)
- .aspectRatio(contentMode: .fit)
- .frame(width: 11, height: 11, alignment: .center)
- .opacity(0.5)
- }
- }
- .frame(width: 10)
- .padding(.leading, 4)
- }
-
- HStack(alignment: .center, spacing: 0) {
- if let bookmark = item.bookmark {
- switch bookmark.type {
- case .tracker:
-// Image(systemName: "point.3.filled.connected.trianglepath.dotted")
- Image("Tracker")
- .resizable()
- .scaledToFit()
- .frame(width: 16, height: 16)
- case .server:
- Image("Server")
- .resizable()
- .scaledToFit()
- .frame(width: 16, height: 16)
-// Image(systemName: "globe.americas.fill")
- }
- }
- else if let _ = item.server {
- Image("Server")
- .resizable()
- .scaledToFit()
- .frame(width: 16, height: 16)
- }
- }
- .frame(width: 15)
- if let bookmark = item.bookmark {
- switch bookmark.type {
- case .tracker:
- Text(bookmark.name).bold().lineLimit(1).truncationMode(.tail)
- case .server:
- Text(bookmark.name).lineLimit(1).truncationMode(.tail)
- }
- }
- else if let server = item.server {
- Text(server.name ?? server.address).lineLimit(1)
-
- if let description = server.description, !description.isEmpty {
- Text(description).foregroundStyle(.secondary).lineLimit(1).truncationMode(.tail)
- }
- }
-
- if
- let server = item.server,
- server.users > 0 {
- Spacer()
- Text("\(server.users) \(Image(systemName: "person.fill"))")
- .lineLimit(1)
- .foregroundStyle(.secondary)
- .padding([.leading, .trailing], 4)
- .padding([.top, .bottom], 2)
-// .background(Capsule(style: .circular).stroke(.secondary, lineWidth: 1))
-// .opacity(0.5)
- }
- else if
- let bookmark = item.bookmark,
- bookmark.type == .tracker {
- if item.loading {
+ switch bookmark.type {
+ case .tracker:
+ Image("Tracker")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 16, height: 16, alignment: .center)
+ Text(bookmark.name).bold().lineLimit(1).truncationMode(.tail)
+ if bookmark.loading {
ProgressView()
.padding([.leading, .trailing], 2)
.controlSize(.small)
}
+ case .server:
+ Image(systemName: "bookmark.fill")
+ .resizable()
+ .renderingMode(.template)
+ .aspectRatio(contentMode: .fit)
+ .frame(width: 11, height: 11, alignment: .center)
+ .opacity(0.5)
+ .padding(.leading, 3)
+ Image("Server")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 16, height: 16, alignment: .center)
+ Text(bookmark.name).lineLimit(1).truncationMode(.tail)
+ case .temporary:
Spacer()
+ .frame(width: 14 + 8 + 16)
+ Image("Server")
+ .resizable()
+ .scaledToFit()
+ .frame(width: 16, height: 16, alignment: .center)
+ Text(bookmark.name).lineLimit(1).truncationMode(.tail)
+ if let serverDescription = bookmark.serverDescription {
+ Text(serverDescription)
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ }
}
- else {
- Spacer()
- }
+
+ Spacer()
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
- .padding(.leading, CGFloat(depth * (12 + 10)))
- .onChange(of: item.expanded) {
- loading = false
+ .onChange(of: bookmark.expanded) {
+ guard bookmark.type == .tracker else {
+ return
+ }
- if
- item.expanded,
- let bookmark = item.bookmark,
- bookmark.type == .tracker {
+ if bookmark.expanded {
Task {
- await item.loadServers()
- }
- }
- }
-
- if
- item.expanded,
- let servers = item.servers {
- ForEach(servers, id: \.self) { serverItem in
- TrackerItemView(item: serverItem, depth: self.depth + 1)
- .tag(serverItem)
- .deleteDisabled(!serverItem.editable)
- .moveDisabled(!serverItem.editable)
- }
- }
- }
-}
-
-@Observable
-class TrackerItem: Identifiable, Hashable {
- let id: UUID = UUID()
- let bookmark: Bookmark?
- let server: Server?
-
- var editable: Bool = true
-
- var servers: [TrackerItem]?
-
- var expanded: Bool = false
- var loading: Bool = false
-
- var displayAddress: String? {
- if let s = server {
- if s.port == HotlinePorts.DefaultServerPort {
- return s.address
- }
- else {
- return "\(s.address):\(s.port)"
- }
- }
- else if let b = bookmark {
- if b.port == HotlinePorts.DefaultServerPort {
- return b.address
- }
- else {
- return "\(b.address):\(b.port)"
- }
- }
-
- return nil
- }
-
- init(bookmark: Bookmark) {
- self.bookmark = bookmark
- self.server = nil
- self.servers = nil
-
- if bookmark.type == .tracker {
- self.servers = []
- }
- }
-
- init(server: Server) {
- self.server = server
- self.servers = nil
- self.bookmark = nil
- }
-
- static func == (lhs: TrackerItem, rhs: TrackerItem) -> Bool {
- return lhs.id == rhs.id
- }
-
- func hash(into hasher: inout Hasher) {
- hasher.combine(self.id)
- }
-
-
- @MainActor
- func loadServers() async {
- guard
- let bookmark = self.bookmark,
- bookmark.type == .tracker
- else {
- self.loading = false
- return
- }
-
- let client = HotlineTrackerClient()
-
- self.loading = true
-
- if let fetchedServers: [HotlineServer] = try? await client.fetchServers(address: bookmark.address, port: HotlinePorts.DefaultTrackerPort) {
- var newItems: [TrackerItem] = []
-
- for s in fetchedServers {
- if let serverName = s.name {
- let server = Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users))
- let item = TrackerItem(server: server)
- item.editable = false
- newItems.append(item)
+ await bookmark.fetchServers()
}
}
-
- self.servers = newItems
}
-
- self.loading = false
}
-
}
#Preview {