diff options
Diffstat (limited to 'Hotline/Models/Bookmark.swift')
| -rw-r--r-- | Hotline/Models/Bookmark.swift | 284 |
1 files changed, 149 insertions, 135 deletions
diff --git a/Hotline/Models/Bookmark.swift b/Hotline/Models/Bookmark.swift index b410f75..5afc2c0 100644 --- a/Hotline/Models/Bookmark.swift +++ b/Hotline/Models/Bookmark.swift @@ -11,131 +11,140 @@ enum BookmarkType: String, Codable { final class Bookmark { var type: BookmarkType = BookmarkType.server var order: Int = 0 - + var name: String = "" var address: String = "" var port: Int = HotlinePorts.DefaultServerPort - + @Attribute(.allowsCloudEncryption) var login: String? - + @Attribute(.allowsCloudEncryption) var password: String? - + @Attribute var autoconnect: Bool = false - + @Attribute(.ephemeral) var expanded: Bool = false - + @Attribute(.ephemeral) var loading: Bool = false - + @Attribute(.ephemeral) var serverDescription: String? = nil - + @Attribute(.ephemeral) var serverUserCount: Int? = nil - + @Transient var servers: [Bookmark] = [] - + func hash(into hasher: inout Hasher) { - + } - + @Transient var displayAddress: String { switch self.type { case .tracker: if self.port == HotlinePorts.DefaultTrackerPort { return self.address - } - else { + } else { return "\(self.address):\(String(self.port))" } - + case .server, .temporary: if self.port == HotlinePorts.DefaultServerPort { return self.address - } - else { + } else { return "\(self.address):\(String(self.port))" } } } - + @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, autoconnect: self.autoconnect) + return Server( + name: self.name, description: self.serverDescription, address: self.address, + port: self.port, login: self.login, password: self.password, autoconnect: self.autoconnect) } } - + 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) + 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, autoconnect: Bool = false) { + + init( + type: BookmarkType, name: String, address: String, port: Int, login: String? = nil, + password: String? = nil, autoconnect: Bool = false + ) { self.type = type self.name = name self.address = address self.port = port - + self.login = login self.password = password - + self.autoconnect = autoconnect } - + 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 self.serverUserCount = server.users } - + init?(fileData: Data, name: String? = nil) { guard fileData.count <= 2000 else { return nil } - + var fileDataArray: [UInt8] = [UInt8](fileData) - + guard let headerValue = fileDataArray.consumeUInt32(), - headerValue.fourCharCode() == "HTsc", - let versionNumber = fileDataArray.consumeUInt16(), - versionNumber == 1, - fileDataArray.consume(128), // Skip 128 reserved bytes. - let loginLength = fileDataArray.consumeUInt16(), - let loginData: Data = fileDataArray.consumeBytes(32), - let passwordLength = fileDataArray.consumeUInt16(), - let passwordData: Data = fileDataArray.consumeBytes(32), - let addressLength = fileDataArray.consumeUInt16(), - let addressData: Data = fileDataArray.consumeBytes(256), - let addressString = String(data: addressData[0..<Int(addressLength)], encoding: .ascii), - let loginString = String(data: loginData[0..<Int(loginLength)], encoding: .ascii), - let passwordString = String(data: passwordData[0..<Int(passwordLength)], encoding: .ascii) else { + headerValue.fourCharCode() == "HTsc", + let versionNumber = fileDataArray.consumeUInt16(), + versionNumber == 1, + fileDataArray.consume(128), // Skip 128 reserved bytes. + let loginLength = fileDataArray.consumeUInt16(), + let loginData: Data = fileDataArray.consumeBytes(32), + let passwordLength = fileDataArray.consumeUInt16(), + let passwordData: Data = fileDataArray.consumeBytes(32), + let addressLength = fileDataArray.consumeUInt16(), + let addressData: Data = fileDataArray.consumeBytes(256), + let addressString = String(data: addressData[0..<Int(addressLength)], encoding: .ascii), + let loginString = String(data: loginData[0..<Int(loginLength)], encoding: .ascii), + let passwordString = String(data: passwordData[0..<Int(passwordLength)], encoding: .ascii) + else { return nil } - + let (addressHost, addressPort) = Server.parseServerAddressAndPort(addressString) - + self.type = .server if let name = name, !name.isEmpty { self.name = name - } - else { + } else { self.name = addressHost } self.address = addressHost @@ -143,156 +152,158 @@ final class Bookmark { self.login = loginString.isEmpty ? nil : loginString self.password = passwordString.isEmpty ? nil : passwordString } - + convenience init?(fileURL bookmarkFileURL: URL) { guard bookmarkFileURL.isFileURL, - let fileAttributes = try? FileManager.default.attributesOfItem(atPath: bookmarkFileURL.path(percentEncoded: false)), - let fileSize = fileAttributes[FileAttributeKey.size] as? UInt64, - fileSize <= 2000, - let fileData = try? Data(contentsOf: bookmarkFileURL) else { + let fileAttributes = try? FileManager.default.attributesOfItem( + atPath: bookmarkFileURL.path(percentEncoded: false)), + let fileSize = fileAttributes[FileAttributeKey.size] as? UInt64, + fileSize <= 2000, + let fileData = try? Data(contentsOf: bookmarkFileURL) + else { return nil } - + print("Bookmark: Parsing Hotline bookmark file...") - - let fileName = bookmarkFileURL.deletingPathExtension().lastPathComponent.trimmingCharacters(in: .whitespacesAndNewlines) - + + let fileName = bookmarkFileURL.deletingPathExtension().lastPathComponent.trimmingCharacters( + in: .whitespacesAndNewlines) + self.init(fileData: fileData, name: fileName) - -// var fileDataArray: [UInt8] = [UInt8](fileData) -// -// guard let headerValue = fileDataArray.consumeUInt32(), -// headerValue.fourCharCode() == "HTsc", -// let versionNumber = fileDataArray.consumeUInt16(), -// versionNumber == 1, -// fileDataArray.consume(128), // Skip 128 reserved bytes. -// let loginLength = fileDataArray.consumeUInt16(), -// let loginData: Data = fileDataArray.consumeBytes(32), -// let passwordLength = fileDataArray.consumeUInt16(), -// let passwordData: Data = fileDataArray.consumeBytes(32), -// let addressLength = fileDataArray.consumeUInt16(), -// let addressData: Data = fileDataArray.consumeBytes(256), -// let addressString = String(data: addressData[0..<Int(addressLength)], encoding: .ascii), -// let loginString = String(data: loginData[0..<Int(loginLength)], encoding: .ascii), -// let passwordString = String(data: passwordData[0..<Int(passwordLength)], encoding: .ascii) else { -// return nil -// } -// -// let (addressHost, addressPort) = Server.parseServerAddressAndPort(addressString) -// -// self.type = .server -// self.name = fileName.isEmpty ? addressHost : fileName -// self.address = addressHost -// self.port = addressPort -// self.login = loginString.isEmpty ? nil : loginString -// self.password = passwordString.isEmpty ? nil : passwordString + + // var fileDataArray: [UInt8] = [UInt8](fileData) + // + // guard let headerValue = fileDataArray.consumeUInt32(), + // headerValue.fourCharCode() == "HTsc", + // let versionNumber = fileDataArray.consumeUInt16(), + // versionNumber == 1, + // fileDataArray.consume(128), // Skip 128 reserved bytes. + // let loginLength = fileDataArray.consumeUInt16(), + // let loginData: Data = fileDataArray.consumeBytes(32), + // let passwordLength = fileDataArray.consumeUInt16(), + // let passwordData: Data = fileDataArray.consumeBytes(32), + // let addressLength = fileDataArray.consumeUInt16(), + // let addressData: Data = fileDataArray.consumeBytes(256), + // let addressString = String(data: addressData[0..<Int(addressLength)], encoding: .ascii), + // let loginString = String(data: loginData[0..<Int(loginLength)], encoding: .ascii), + // let passwordString = String(data: passwordData[0..<Int(passwordLength)], encoding: .ascii) else { + // return nil + // } + // + // let (addressHost, addressPort) = Server.parseServerAddressAndPort(addressString) + // + // self.type = .server + // self.name = fileName.isEmpty ? addressHost : fileName + // self.address = addressHost + // self.port = addressPort + // self.login = loginString.isEmpty ? nil : loginString + // self.password = passwordString.isEmpty ? nil : passwordString } - + func bookmarkFileData() -> Data? { guard let addressData = self.displayAddress.data(using: .ascii) else { return nil } - + let loginData: Data = self.login?.data(using: .ascii) ?? Data() let passwordData: Data = self.password?.data(using: .ascii) ?? Data() - + var fileData: Data = Data() - - fileData.appendUInt32("HTsc".fourCharCode()) // magic - fileData.appendUInt16(0x0001) // version - fileData.append(Data(repeating: 0x00, count: 128)) // reserved - + + fileData.appendUInt32("HTsc".fourCharCode()) // magic + fileData.appendUInt16(0x0001) // version + fileData.append(Data(repeating: 0x00, count: 128)) // reserved + fileData.appendUInt16(UInt16(loginData.count)) fileData.append(loginData) if loginData.count < 32 { // Pad login data to 32 bytes fileData.append(Data(repeating: 0x00, count: 32 - loginData.count)) } - + fileData.appendUInt16(UInt16(passwordData.count)) fileData.append(passwordData) if passwordData.count < 32 { // Pad password data to 32 bytes fileData.append(Data(repeating: 0x00, count: 32 - passwordData.count)) } - + fileData.appendUInt16(UInt16(addressData.count)) fileData.append(addressData) if passwordData.count < 256 { // Pad address data to 256 bytes fileData.append(Data(repeating: 0x00, count: 256 - addressData.count)) } - + return fileData } - + 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 { + } 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 @@ -301,17 +312,17 @@ final class Bookmark { index += 1 } } - + static func delete(at indexes: IndexSet, context: ModelContext) { var existingBookmarks = Bookmark.fetchAll(context: context) -// existingBookmarks.remove(atOffsets: indexes) + // 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) @@ -319,62 +330,65 @@ final class Bookmark { existingBookmark.order = index index += 1 } - + do { try context.save() - } - catch { + } 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 { + } 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) { + 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)) + 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 |