From 34fef2f5eca64586aace2c83723135c1eca346f7 Mon Sep 17 00:00:00 2001 From: Dustin Mierau Date: Thu, 16 May 2024 17:40:27 -0700 Subject: Move tracker items and bookmarks to SwiftData for iCloud sync. --- Hotline/Models/Bookmark.swift | 269 ++++++++++++++++++++++++++++++++++++++ Hotline/Models/Bookmarks.swift | 126 ------------------ Hotline/Models/BookmarksOld.swift | 126 ++++++++++++++++++ Hotline/Models/Server.swift | 6 +- 4 files changed, 398 insertions(+), 129 deletions(-) create mode 100644 Hotline/Models/Bookmark.swift delete mode 100644 Hotline/Models/Bookmarks.swift create mode 100644 Hotline/Models/BookmarksOld.swift (limited to 'Hotline/Models') 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(sortBy: [.init(\.order)]) + do { + let bookmarks: [Bookmark] = try context.fetch(fetchDescriptor) + return bookmarks + } + catch { + return [] + } + } + + static func fetchCount(context: ModelContext) -> Int { + let descriptor = FetchDescriptor() + 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/Bookmarks.swift deleted file mode 100644 index 3f4971c..0000000 --- a/Hotline/Models/Bookmarks.swift +++ /dev/null @@ -1,126 +0,0 @@ -import SwiftUI - -extension NSNotification { - static let BookmarkAdded = Notification.Name("BookmarkAdded") - static let BookmarkRemoved = Notification.Name("BookmarkRemoved") -} - -enum BookmarkType: String, Codable { - case tracker = "tracker" - case server = "server" -} - -struct Bookmark: Codable, Equatable { - let type: BookmarkType - 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) { - self.type = type - self.name = name - self.address = address - self.port = port - self.login = login - self.password = password - } -} - -@Observable final class Bookmarks { - var bookmarks: [Bookmark]? = 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"), - ] - - init() { - self.load() - } - - func apply(_ newBookmarks: [Bookmark], save shouldSave: Bool = true) { - self.bookmarks = newBookmarks - if shouldSave { - self.save() - } - } - - func load() { - let jsonString: String? = DAKeychain.shared["Hotline Bookmarks"] - let jsonData: Data? = jsonString?.data(using: .utf8, allowLossyConversion: false) - - let decoder = JSONDecoder() - var decodedBookmarks = try? decoder.decode([Bookmark].self, from: jsonData ?? Data()) - if decodedBookmarks == nil || decodedBookmarks?.isEmpty == true { - print("Bookmarks: using default bookmarks") - decodedBookmarks = Bookmarks.DefaultBookmarks - } - else { - print("Bookmarks: using saved bookmarks") - } - - self.bookmarks = [Bookmark](decodedBookmarks!) - } - - func save() { - var bookmarksToSave = self.bookmarks - if bookmarksToSave == Bookmarks.DefaultBookmarks { - print("Bookmarks: skipping saving default bookmarks") - bookmarksToSave = [] - } - - let encoder = JSONEncoder() - encoder.outputFormatting = .prettyPrinted - if let jsonData = try? encoder.encode(bookmarksToSave) { - if let jsonString = String(data: jsonData, encoding: .utf8) { - DAKeychain.shared["Hotline Bookmarks"] = jsonString - } - } - } - - // MARK: - - - func add(_ bookmark: Bookmark, save shouldSave: Bool = true) { - self.bookmarks?.insert(bookmark, at: 0) - - if shouldSave { - self.save() - } - - DispatchQueue.main.async { - NotificationCenter.default.post(name: NSNotification.BookmarkAdded, object: nil, userInfo: ["index": 0]) - } - } - - func delete(_ bookmark: Bookmark, 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) - - if shouldSave { - self.save() - } - - DispatchQueue.main.async { - NotificationCenter.default.post(name: NSNotification.BookmarkRemoved, object: nil, userInfo: ["index": i]) - } - return true - } - - return false - } - - func update(_ bookmark: Bookmark, 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 { - self.save() - } - return true - } - - return false - } -} diff --git a/Hotline/Models/BookmarksOld.swift b/Hotline/Models/BookmarksOld.swift new file mode 100644 index 0000000..0a7bb16 --- /dev/null +++ b/Hotline/Models/BookmarksOld.swift @@ -0,0 +1,126 @@ +import SwiftUI + +extension NSNotification { + static let BookmarkAdded = Notification.Name("BookmarkAdded") + static let BookmarkRemoved = Notification.Name("BookmarkRemoved") +} + +enum BookmarkOldType: String, Codable { + case tracker = "tracker" + case server = "server" +} + +struct BookmarkOld: Codable, Equatable { + let type: BookmarkOldType + let name: String + let address: String + let port: Int + let login: String? + let password: String? + + 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 + self.port = port + self.login = login + self.password = password + } +} + +@Observable final class BookmarksOld { + var bookmarks: [BookmarkOld]? = nil + + 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: [BookmarkOld], save shouldSave: Bool = true) { + self.bookmarks = newBookmarks + if shouldSave { + self.save() + } + } + + func load() { + let jsonString: String? = DAKeychain.shared["Hotline Bookmarks"] + let jsonData: Data? = jsonString?.data(using: .utf8, allowLossyConversion: false) + + let decoder = JSONDecoder() + var decodedBookmarks = try? decoder.decode([BookmarkOld].self, from: jsonData ?? Data()) + if decodedBookmarks == nil || decodedBookmarks?.isEmpty == true { + print("Bookmarks: using default bookmarks") + decodedBookmarks = BookmarksOld.DefaultBookmarks + } + else { + print("Bookmarks: using saved bookmarks") + } + + self.bookmarks = [BookmarkOld](decodedBookmarks!) + } + + func save() { + var bookmarksToSave = self.bookmarks + if bookmarksToSave == BookmarksOld.DefaultBookmarks { + print("Bookmarks: skipping saving default bookmarks") + bookmarksToSave = [] + } + + let encoder = JSONEncoder() + encoder.outputFormatting = .prettyPrinted + if let jsonData = try? encoder.encode(bookmarksToSave) { + if let jsonString = String(data: jsonData, encoding: .utf8) { + DAKeychain.shared["Hotline Bookmarks"] = jsonString + } + } + } + + // MARK: - + + func add(_ bookmark: BookmarkOld, save shouldSave: Bool = true) { + self.bookmarks?.insert(bookmark, at: 0) + + if shouldSave { + self.save() + } + + DispatchQueue.main.async { + NotificationCenter.default.post(name: NSNotification.BookmarkAdded, object: nil, userInfo: ["index": 0]) + } + } + + 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) + + if shouldSave { + self.save() + } + + DispatchQueue.main.async { + NotificationCenter.default.post(name: NSNotification.BookmarkRemoved, object: nil, userInfo: ["index": i]) + } + return true + } + + return false + } + + 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 { + self.save() + } + return true + } + + return false + } +} 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) { -- cgit