diff options
Diffstat (limited to 'Hotline/Models')
| -rw-r--r-- | Hotline/Models/ApplicationState.swift | 6 | ||||
| -rw-r--r-- | Hotline/Models/Bookmark.swift | 284 | ||||
| -rw-r--r-- | Hotline/Models/ChatMessage.swift | 17 | ||||
| -rw-r--r-- | Hotline/Models/FileDetails.swift | 2 | ||||
| -rw-r--r-- | Hotline/Models/FileInfo.swift | 30 | ||||
| -rw-r--r-- | Hotline/Models/FilePreview.swift | 51 | ||||
| -rw-r--r-- | Hotline/Models/Hotline.swift | 699 | ||||
| -rw-r--r-- | Hotline/Models/NewsInfo.swift | 47 | ||||
| -rw-r--r-- | Hotline/Models/Preferences.swift | 134 | ||||
| -rw-r--r-- | Hotline/Models/PreviewFileInfo.swift | 5 | ||||
| -rw-r--r-- | Hotline/Models/Server.swift | 25 | ||||
| -rw-r--r-- | Hotline/Models/Tracker.swift | 4 | ||||
| -rw-r--r-- | Hotline/Models/TransferInfo.swift | 12 | ||||
| -rw-r--r-- | Hotline/Models/User.swift | 8 |
14 files changed, 719 insertions, 605 deletions
diff --git a/Hotline/Models/ApplicationState.swift b/Hotline/Models/ApplicationState.swift index 6e077c5..3e90ed9 100644 --- a/Hotline/Models/ApplicationState.swift +++ b/Hotline/Models/ApplicationState.swift @@ -3,14 +3,14 @@ import SwiftUI @Observable final class ApplicationState { static let shared = ApplicationState() - + var activeHotline: Hotline? = nil var activeServerState: ServerState? = nil - + // Frontmost server window information var activeServerID: UUID? = nil var activeServerBanner: NSImage? = nil var activeServerName: String? = nil - + var cloudKitReady: Bool = false } 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 diff --git a/Hotline/Models/ChatMessage.swift b/Hotline/Models/ChatMessage.swift index e585cdf..36648d1 100644 --- a/Hotline/Models/ChatMessage.swift +++ b/Hotline/Models/ChatMessage.swift @@ -9,25 +9,24 @@ enum ChatMessageType { struct ChatMessage: Identifiable { let id = UUID() - + let text: String let type: ChatMessageType let date: Date let username: String? - + static let parser = /^\s*([^\:]+):\s*([\s\S]+)$/ - + init(text: String, type: ChatMessageType, date: Date) { self.type = type self.date = date - - if - type == .message, - let match = text.firstMatch(of: ChatMessage.parser) { + + if type == .message, + let match = text.firstMatch(of: ChatMessage.parser) + { self.username = String(match.1) self.text = String(match.2) - } - else { + } else { self.username = nil self.text = text } diff --git a/Hotline/Models/FileDetails.swift b/Hotline/Models/FileDetails.swift index 20b1ee2..dc302e8 100644 --- a/Hotline/Models/FileDetails.swift +++ b/Hotline/Models/FileDetails.swift @@ -1,6 +1,6 @@ import UniformTypeIdentifiers -struct FileDetails:Identifiable { +struct FileDetails: Identifiable { let id = UUID() var name: String var path: [String] diff --git a/Hotline/Models/FileInfo.swift b/Hotline/Models/FileInfo.swift index 2da7378..0eadd10 100644 --- a/Hotline/Models/FileInfo.swift +++ b/Hotline/Models/FileInfo.swift @@ -3,46 +3,46 @@ import UniformTypeIdentifiers @Observable class FileInfo: Identifiable, Hashable { let id: UUID - + let path: [String] let name: String - + let type: String let creator: String let fileSize: UInt - + let isFolder: Bool let isUnavailable: Bool - + var isDropboxFolder: Bool { guard self.isFolder, - (self.name.range(of: "upload", options: [.caseInsensitive]) != nil) || (self.name.range(of: "drop box", options: [.caseInsensitive]) != nil) + (self.name.range(of: "upload", options: [.caseInsensitive]) != nil) + || (self.name.range(of: "drop box", options: [.caseInsensitive]) != nil) else { return false } return true } - + var isAdminDropboxFolder: Bool { self.isDropboxFolder && (self.name.range(of: "admin", options: [.caseInsensitive]) != nil) } - + var expanded: Bool = false var children: [FileInfo]? = nil - + var isPreviewable: Bool { let fileExtension = (self.name as NSString).pathExtension if let fileType = UTType(filenameExtension: fileExtension) { if fileType.isSubtype(of: .image) { return true - } - else if fileType.isSubtype(of: .text) { + } else if fileType.isSubtype(of: .text) { return true } } return false } - + var isImage: Bool { let fileExtension = (self.name as NSString).pathExtension if let fileType = UTType(filenameExtension: fileExtension) { @@ -52,7 +52,7 @@ import UniformTypeIdentifiers } return false } - + init(hotlineFile: HotlineFile) { self.id = UUID() self.path = hotlineFile.path @@ -62,17 +62,17 @@ import UniformTypeIdentifiers self.fileSize = UInt(hotlineFile.fileSize) self.isFolder = hotlineFile.isFolder self.isUnavailable = (!self.isFolder && (self.fileSize == 0)) - + print(self.name, self.type, self.creator, self.isUnavailable) if self.isFolder { self.children = [] } } - + static func == (lhs: FileInfo, rhs: FileInfo) -> Bool { return lhs.id == rhs.id } - + func hash(into hasher: inout Hasher) { hasher.combine(self.id) } diff --git a/Hotline/Models/FilePreview.swift b/Hotline/Models/FilePreview.swift index e5f49a3..bba29fa 100644 --- a/Hotline/Models/FilePreview.swift +++ b/Hotline/Models/FilePreview.swift @@ -18,52 +18,55 @@ enum FilePreviewType: Equatable { final class FilePreview: HotlineFilePreviewClientDelegate { @ObservationIgnored let info: PreviewFileInfo @ObservationIgnored var client: HotlineFilePreviewClient? = nil - + var state: FilePreviewState = .unloaded var progress: Double = 0.0 - + var data: Data? = nil - + #if os(iOS) - var image: UIImage? = nil + var image: UIImage? = nil #elseif os(macOS) - var image: NSImage? = nil + var image: NSImage? = nil #endif - + var text: String? = nil var styledText: NSAttributedString? = nil - + var previewType: FilePreviewType { let fileExtension = (info.name as NSString).pathExtension if let fileType = UTType(filenameExtension: fileExtension) { if fileType.isSubtype(of: .image) { return .image - } - else if fileType.isSubtype(of: .text) { + } else if fileType.isSubtype(of: .text) { return .text } } return .unknown } - + init(info: PreviewFileInfo) { self.info = info - - self.client = HotlineFilePreviewClient(address: info.address, port: UInt16(info.port), reference: info.id, size: UInt32(info.size)) + + self.client = HotlineFilePreviewClient( + address: info.address, port: UInt16(info.port), reference: info.id, size: UInt32(info.size)) self.client?.delegate = self } - + func download() { self.client?.start() } - + func cancel() { self.client?.cancel() } - - func hotlineTransferStatusChanged(client: any HotlineTransferClient, reference: UInt32, status: HotlineTransferStatus, timeRemaining: TimeInterval) { + + func hotlineTransferStatusChanged( + client: any HotlineTransferClient, reference: UInt32, status: HotlineTransferStatus, + timeRemaining: TimeInterval + ) { print("FilePreview: Download status changed:", status) - + switch status { case .unconnected: state = .unloaded @@ -88,24 +91,24 @@ final class FilePreview: HotlineFilePreviewClientDelegate { progress = 1.0 } } - + func hotlineFilePreviewComplete(client: HotlineFilePreviewClient, reference: UInt32, data: Data) { self.state = .loaded self.data = data - + switch self.previewType { case .image: #if os(iOS) - self.image = UIImage(data: data) + self.image = UIImage(data: data) #elseif os(macOS) - self.image = NSImage(data: data) + self.image = NSImage(data: data) #endif case .text: - let encoding: UInt = NSString.stringEncoding(for: data, convertedString: nil, usedLossyConversion: nil) + let encoding: UInt = NSString.stringEncoding( + for: data, convertedString: nil, usedLossyConversion: nil) if encoding != 0 { self.text = String(data: data, encoding: String.Encoding(rawValue: encoding)) - } - else { + } else { self.text = String(data: data, encoding: .utf8) } case .unknown: diff --git a/Hotline/Models/Hotline.swift b/Hotline/Models/Hotline.swift index 9ea29c5..1989d46 100644 --- a/Hotline/Models/Hotline.swift +++ b/Hotline/Models/Hotline.swift @@ -1,25 +1,27 @@ import SwiftUI @Observable -class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelegate, HotlineFilePreviewClientDelegate, HotlineFileUploadClientDelegate { +class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelegate, + HotlineFilePreviewClientDelegate, HotlineFileUploadClientDelegate +{ let id: UUID = UUID() let trackerClient: HotlineTrackerClient let client: HotlineClient - + static func == (lhs: Hotline, rhs: Hotline) -> Bool { return lhs.id == rhs.id } - + #if os(macOS) - static func getClassicIcon(_ index: Int) -> NSImage? { - return NSImage(named: "Classic/\(index)") - } + static func getClassicIcon(_ index: Int) -> NSImage? { + return NSImage(named: "Classic/\(index)") + } #elseif os(iOS) - static func getClassicIcon(_ index: Int) -> UIImage? { - return UIImage(named: "Classic/\(index)") - } + static func getClassicIcon(_ index: Int) -> UIImage? { + return UIImage(named: "Classic/\(index)") + } #endif - + // The icon ordering here was painsakenly pulled manually // from the original Hotline client to display the classic icons // in the same order as the original client. @@ -104,11 +106,11 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega 6001, 6002, 6003, 6004, 6005, 6008, 6009, 6010, 6011, 6012, 6013, 6014, 6015, 6016, 6017, 6018, 6023, 6025, 6026, 6027, 6028, - 6029, 6030, 6031, 6032, 6033, 6034, 6035 + 6029, 6030, 6031, 6032, 6033, 6034, 6035, ] - + var status: HotlineClientStatus = .disconnected - var server: Server? { + var server: Server? { didSet { self.updateServerTitle() } @@ -132,258 +134,271 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega var files: [FileInfo] = [] var filesLoaded: Bool = false var news: [NewsInfo] = [] - private var newsLookup: [String:NewsInfo] = [:] + private var newsLookup: [String: NewsInfo] = [:] var newsLoaded: Bool = false var accountsLoaded: Bool = false - var instantMessages: [UInt16:[InstantMessage]] = [:] + var instantMessages: [UInt16: [InstantMessage]] = [:] var transfers: [TransferInfo] = [] var downloads: [HotlineTransferClient] = [] - var unreadInstantMessages: [UInt16:UInt16] = [:] + var unreadInstantMessages: [UInt16: UInt16] = [:] var unreadPublicChat: Bool = false var errorDisplayed: Bool = false var errorMessage: String? = nil - + @ObservationIgnored var bannerClient: HotlineFilePreviewClient? #if os(macOS) - var bannerImage: NSImage? = nil + var bannerImage: NSImage? = nil #elseif os(iOS) - var bannerImage: UIImage? = nil + var bannerImage: UIImage? = nil #endif - - + // MARK: - - + init(trackerClient: HotlineTrackerClient, client: HotlineClient) { self.trackerClient = trackerClient self.client = client self.client.delegate = self } - + // MARK: - - - @MainActor func getServerList(tracker: String, port: Int = HotlinePorts.DefaultTrackerPort) async -> [Server] { + + @MainActor func getServerList(tracker: String, port: Int = HotlinePorts.DefaultTrackerPort) async + -> [Server] + { var servers: [Server] = [] - - if let fetchedServers: [HotlineServer] = try? await self.trackerClient.fetchServers(address: tracker, port: port) { + + if let fetchedServers: [HotlineServer] = try? await self.trackerClient.fetchServers( + address: tracker, port: port) + { for s in fetchedServers { if let serverName = s.name { - servers.append(Server(name: serverName, description: s.description, address: s.address, port: Int(s.port), users: Int(s.users))) + servers.append( + Server( + name: serverName, description: s.description, address: s.address, port: Int(s.port), + users: Int(s.users))) } } } - + return servers } - + @MainActor func disconnectTracker() { self.trackerClient.close() } - - @MainActor func login(server: Server, username: String, iconID: Int, callback: ((Bool) -> Void)? = nil) { + + @MainActor func login( + server: Server, username: String, iconID: Int, callback: ((Bool) -> Void)? = nil + ) { self.server = server self.serverName = server.name self.username = username self.iconID = iconID - - self.client.login(address: server.address, port: server.port, login: server.login, password: server.password, username: username, iconID: UInt16(iconID)) { [weak self] err, serverName, serverVersion in + + self.client.login( + address: server.address, port: server.port, login: server.login, password: server.password, + username: username, iconID: UInt16(iconID) + ) { [weak self] err, serverName, serverVersion in self?.serverVersion = serverVersion ?? 123 if serverName != nil { self?.serverName = serverName } - + callback?(err == nil) } } - - @MainActor func sendUserInfo(username: String, iconID: Int, options: HotlineUserOptions = [], autoresponse: String? = nil) { + + @MainActor func sendUserInfo( + username: String, iconID: Int, options: HotlineUserOptions = [], autoresponse: String? = nil + ) { self.username = username self.iconID = iconID - - self.client.sendSetClientUserInfo(username: username, iconID: UInt16(iconID), options: options, autoresponse: autoresponse) + + self.client.sendSetClientUserInfo( + username: username, iconID: UInt16(iconID), options: options, autoresponse: autoresponse) } - + @MainActor func getUserList() { self.client.sendGetUserList() } - + @MainActor func disconnect() { self.client.disconnect() self.bannerClient?.cancel() } - + @MainActor func sendAgree() { self.client.sendAgree(username: self.username, iconID: UInt16(self.iconID), options: .none) } - + @MainActor func sendInstantMessage(_ text: String, userID: UInt16) { - let message = InstantMessage(direction: .outgoing, text: text.convertingLinksToMarkdown(), type: .message, date: Date()) - + let message = InstantMessage( + direction: .outgoing, text: text.convertingLinksToMarkdown(), type: .message, date: Date()) + if self.instantMessages[userID] == nil { self.instantMessages[userID] = [message] - } - else { + } else { self.instantMessages[userID]!.append(message) } - + self.client.sendInstantMessage(message: text, userID: userID) - + if Prefs.shared.playPrivateMessageSound && Prefs.shared.playPrivateMessageSound { SoundEffectPlayer.shared.playSoundEffect(.chatMessage) } } - + func markPublicChatAsRead() { self.unreadPublicChat = false } - + func hasUnreadInstantMessages(userID: UInt16) -> Bool { return self.unreadInstantMessages[userID] != nil } - + func markInstantMessagesAsRead(userID: UInt16) { self.unreadInstantMessages.removeValue(forKey: userID) } - + @MainActor func sendChat(_ text: String, announce: Bool = false) { self.client.sendChat(message: text, announce: announce) } - + @MainActor func getMessageBoard() async -> [String] { self.messageBoard = await withCheckedContinuation { [weak self] continuation in - self?.client.sendGetMessageBoard() { err, messages in + self?.client.sendGetMessageBoard { err, messages in continuation.resume(returning: (err != nil ? [] : messages)) } } - + self.messageBoardLoaded = true - + return self.messageBoard } - + @MainActor func postToMessageBoard(text: String) { self.client.sendPostMessageBoard(text: text) } - + @MainActor func getFileList(path: [String] = []) async -> [FileInfo] { return await withCheckedContinuation { [weak self] continuation in self?.client.sendGetFileList(path: path) { [weak self] files in let parentFile = self?.findFile(in: self?.files ?? [], at: path) - + var newFiles: [FileInfo] = [] for f in files { newFiles.append(FileInfo(hotlineFile: f)) } - + DispatchQueue.main.async { if let parent = parentFile { parent.children = newFiles - } - else if path.isEmpty { + } else if path.isEmpty { self?.filesLoaded = true self?.files = newFiles } - + continuation.resume(returning: newFiles) } } } } - - @MainActor func getNewsArticle(id articleID: UInt, at path: [String], flavor: String) async -> String? { + + @MainActor func getNewsArticle(id articleID: UInt, at path: [String], flavor: String) async + -> String? + { return await withCheckedContinuation { [weak self] continuation in - self?.client.sendGetNewsArticle(id: UInt32(articleID), path: path, flavor: flavor) { articleText in -// let parentNews = self?.findNews(in: self?.news ?? [], at: path) - -// var newCategories: [NewsInfo] = [] -// for category in categories { -// newCategories.append(NewsInfo(hotlineNewsCategory: category)) -// } -// -// if let parent = existingNewsItem { -// parent.children = newCategories -// } -// else if path.isEmpty { -// self?.news = newCategories -// } - + self?.client.sendGetNewsArticle(id: UInt32(articleID), path: path, flavor: flavor) { + articleText in + // let parentNews = self?.findNews(in: self?.news ?? [], at: path) + + // var newCategories: [NewsInfo] = [] + // for category in categories { + // newCategories.append(NewsInfo(hotlineNewsCategory: category)) + // } + // + // if let parent = existingNewsItem { + // parent.children = newCategories + // } + // else if path.isEmpty { + // self?.news = newCategories + // } + continuation.resume(returning: articleText) } } - + } - + @MainActor func getAccounts() async -> [HotlineAccount] { return await withCheckedContinuation { [weak self] continuation in - self?.client.sendGetAccounts() { articles in + self?.client.sendGetAccounts { articles in continuation.resume(returning: articles) } } } - + @MainActor func getNewsList(at path: [String] = []) async { return await withCheckedContinuation { [weak self] continuation in let parentNewsGroup = self?.findNews(in: self?.news ?? [], at: path) - + // Send a categories request for bundle paths or root (empty path) if path.isEmpty || parentNewsGroup?.type == .bundle { print("Hotline: Requesting categories at: /\(path.joined(separator: "/"))") - + self?.client.sendGetNewsCategories(path: path) { @MainActor [weak self] categories in // Create info for each category returned. var newCategoryInfos: [NewsInfo] = [] - + // Transform hotline categories into NewsInfo objects. for category in categories { var newsCategoryInfo = NewsInfo(hotlineNewsCategory: category) - + if let lookupPath = newsCategoryInfo.lookupPath { // Merge returned category info with existing category info. if let existingCategoryInfo = self?.newsLookup[lookupPath] { print("Hotline: Merging category into existing category at \(lookupPath)") - + existingCategoryInfo.count = newsCategoryInfo.count existingCategoryInfo.name = newsCategoryInfo.name existingCategoryInfo.path = newsCategoryInfo.path existingCategoryInfo.categoryID = newsCategoryInfo.categoryID newsCategoryInfo = existingCategoryInfo - } - else { + } else { print("Hotline: New category added at \(lookupPath)") self?.newsLookup[lookupPath] = newsCategoryInfo } } - + newCategoryInfos.append(newsCategoryInfo) } - + if let parent = parentNewsGroup { parent.children = newCategoryInfos - } - else if path.isEmpty { + } else if path.isEmpty { self?.newsLoaded = true self?.news = newCategoryInfos } - + continuation.resume() } - } - else { + } else { print("Hotline: Requesting articles at: /\(path.joined(separator: "/"))") - + self?.client.sendGetNewsArticles(path: path) { @MainActor [weak self] articles in print("Hotline: Organizing news at \(path.joined(separator: "/"))") // Create info for each article returned. var newArticleInfos: [NewsInfo] = [] - + for article in articles { var newsArticleInfo = NewsInfo(hotlineNewsArticle: article) - + if let lookupPath = newsArticleInfo.lookupPath { // Merge returned category info with existing category info. if let existingArticleInfo = self?.newsLookup[lookupPath] { print("Hotline: Merging article into existing article at \(lookupPath)") - + existingArticleInfo.count = newsArticleInfo.count existingArticleInfo.name = newsArticleInfo.name existingArticleInfo.path = newsArticleInfo.path @@ -392,94 +407,96 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega existingArticleInfo.articleFlavors = newsArticleInfo.articleFlavors existingArticleInfo.articleID = newsArticleInfo.articleID newsArticleInfo = existingArticleInfo - } - else { + } else { print("Hotline: New article added at \(lookupPath)") self?.newsLookup[lookupPath] = newsArticleInfo } } - + newArticleInfos.append(newsArticleInfo) } - + let organizedNewsArticles: [NewsInfo] = self?.organizeNewsArticles(newArticleInfos) ?? [] if let parent = parentNewsGroup { parent.children = organizedNewsArticles } - + continuation.resume() } } } } - + func organizeNewsArticles(_ flatArticles: [NewsInfo]) -> [NewsInfo] { // Place articles under their parent. var organized: [NewsInfo] = [] for article in flatArticles { if let parentLookupPath = article.parentArticleLookupPath, - let parentArticle = self.newsLookup[parentLookupPath] { -// article.expanded = true + let parentArticle = self.newsLookup[parentLookupPath] + { + // article.expanded = true if parentArticle.children.firstIndex(of: article) == nil { article.expanded = true parentArticle.children.append(article) } - } - else { + } else { organized.append(article) } } - + return organized } - - @MainActor func postNewsArticle(title: String, body: String, at path: [String], parentID: UInt32 = 0) async -> Bool { - - + + @MainActor func postNewsArticle( + title: String, body: String, at path: [String], parentID: UInt32 = 0 + ) async -> Bool { + return await withCheckedContinuation { [weak self] continuation in guard let client = self?.client else { continuation.resume(returning: false) return } - - client.postNewsArticle(title: title, text: body, path: path, parentID: parentID, callback: { success in - print("Hotline: News article posted? \(success)") - continuation.resume(returning: success) - }) + + client.postNewsArticle( + title: title, text: body, path: path, parentID: parentID, + callback: { success in + print("Hotline: News article posted? \(success)") + continuation.resume(returning: success) + }) } } - -// @MainActor func getNewsCategories(at path: [String] = []) async -> [NewsInfo] { -// return await withCheckedContinuation { [weak self] continuation in -// guard let client = self?.client else { -// continuation.resume(returning: []) -// return -// } -// -// client.sendGetNewsCategories(path: path) { [weak self] categories in -// let parentNews = self?.findNews(in: self?.news ?? [], at: path) -// -// var newCategories: [NewsInfo] = [] -// for category in categories { -// let categoryInfo: NewsInfo = NewsInfo(hotlineNewsCategory: category) -// newCategories.append(categoryInfo) -// self?.newsLookup[categoryInfo.path.joined(separator: "/")] = categoryInfo -// } -// -// DispatchQueue.main.async { -// if let parent = parentNews { -// parent.children = newCategories -// } -// else if path.isEmpty { -// self?.news = newCategories -// } -// -// continuation.resume(returning: newCategories) -// } -// } -// } -// } - + + // @MainActor func getNewsCategories(at path: [String] = []) async -> [NewsInfo] { + // return await withCheckedContinuation { [weak self] continuation in + // guard let client = self?.client else { + // continuation.resume(returning: []) + // return + // } + // + // client.sendGetNewsCategories(path: path) { [weak self] categories in + // let parentNews = self?.findNews(in: self?.news ?? [], at: path) + // + // var newCategories: [NewsInfo] = [] + // for category in categories { + // let categoryInfo: NewsInfo = NewsInfo(hotlineNewsCategory: category) + // newCategories.append(categoryInfo) + // self?.newsLookup[categoryInfo.path.joined(separator: "/")] = categoryInfo + // } + // + // DispatchQueue.main.async { + // if let parent = parentNews { + // parent.children = newCategories + // } + // else if path.isEmpty { + // self?.news = newCategories + // } + // + // continuation.resume(returning: newCategories) + // } + // } + // } + // } + @MainActor func getArticles(at path: [String]) async -> [NewsInfo] { return await withCheckedContinuation { [weak self] continuation in self?.client.sendGetNewsArticles(path: path) { articles in @@ -487,284 +504,315 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega } } } - - @MainActor func downloadFile(_ fileName: String, path: [String], complete callback: ((TransferInfo, URL) -> Void)? = nil) { + + @MainActor func downloadFile( + _ fileName: String, path: [String], complete callback: ((TransferInfo, URL) -> Void)? = nil + ) { var fullPath: [String] = [] if path.count > 1 { - fullPath = Array(path[0..<path.count-1]) + fullPath = Array(path[0..<path.count - 1]) } - - self.client.sendDownloadFile(name: fileName, path: fullPath) { [weak self] success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount in + + self.client.sendDownloadFile(name: fileName, path: fullPath) { + [weak self] + success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount + in print("GOT DOWNLOAD REPLY:") print("\tSUCCESS?", success) print("\tTRANSFER SIZE: \(downloadTransferSize.debugDescription)") print("\tFILE SIZE: \(downloadFileSize.debugDescription)") print("\tREFERENCE NUM: \(downloadReferenceNumber.debugDescription)") print("\tWAITING COUNT: \(downloadWaitingCount.debugDescription)") - - if - let self = self, -// let server = self.server, + + if let self = self, + // let server = self.server, let address = self.server?.address, let port = self.server?.port, let referenceNumber = downloadReferenceNumber, - let transferSize = downloadTransferSize { - - let fileClient = HotlineFileDownloadClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize)) -// let previewClient = HotlineFilePreviewClient(server: self.server, reference: referenceNumber, size: UInt32(transferSize), type: .fileDownload) -// let fileClient = HotlineFileClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize), type: .fileDownload) + let transferSize = downloadTransferSize + { + + let fileClient = HotlineFileDownloadClient( + address: address, port: UInt16(port), reference: referenceNumber, + size: UInt32(transferSize)) + // let previewClient = HotlineFilePreviewClient(server: self.server, reference: referenceNumber, size: UInt32(transferSize), type: .fileDownload) + // let fileClient = HotlineFileClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize), type: .fileDownload) fileClient.delegate = self self.downloads.append(fileClient) - + let transfer = TransferInfo(id: referenceNumber, title: fileName, size: UInt(transferSize)) transfer.downloadCallback = callback self.transfers.append(transfer) - + fileClient.start() } } } - - @MainActor func downloadFileTo(url fileURL: URL, fileName: String, path: [String], progress progressCallback: ((TransferInfo, Double) -> Void)? = nil, complete callback: ((TransferInfo, URL) -> Void)? = nil) { + + @MainActor func downloadFileTo( + url fileURL: URL, fileName: String, path: [String], + progress progressCallback: ((TransferInfo, Double) -> Void)? = nil, + complete callback: ((TransferInfo, URL) -> Void)? = nil + ) { var fullPath: [String] = [] if path.count > 1 { - fullPath = Array(path[0..<path.count-1]) + fullPath = Array(path[0..<path.count - 1]) } - - self.client.sendDownloadFile(name: fileName, path: fullPath) { [weak self] success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount in + + self.client.sendDownloadFile(name: fileName, path: fullPath) { + [weak self] + success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount + in print("GOT DOWNLOAD REPLY:") print("\tSUCCESS?", success) print("\tTRANSFER SIZE: \(downloadTransferSize.debugDescription)") print("\tFILE SIZE: \(downloadFileSize.debugDescription)") print("\tREFERENCE NUM: \(downloadReferenceNumber.debugDescription)") print("\tWAITING COUNT: \(downloadWaitingCount.debugDescription)") - - if - let self = self, + + if let self = self, let address = self.server?.address, let port = self.server?.port, let referenceNumber = downloadReferenceNumber, - let transferSize = downloadTransferSize { - - let fileClient = HotlineFileDownloadClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize)) + let transferSize = downloadTransferSize + { + + let fileClient = HotlineFileDownloadClient( + address: address, port: UInt16(port), reference: referenceNumber, + size: UInt32(transferSize)) fileClient.delegate = self self.downloads.append(fileClient) - + let transfer = TransferInfo(id: referenceNumber, title: fileName, size: UInt(transferSize)) transfer.downloadCallback = callback transfer.progressCallback = progressCallback self.transfers.append(transfer) - + fileClient.start(to: fileURL) } } } - - @MainActor func uploadFile(url fileURL: URL, path: [String], complete callback: ((TransferInfo) -> Void)? = nil) { + + @MainActor func uploadFile( + url fileURL: URL, path: [String], complete callback: ((TransferInfo) -> Void)? = nil + ) { let fileName = fileURL.lastPathComponent - + guard fileURL.isFileURL, !fileName.isEmpty else { print("NOT A FILE URL?") return } - + let filePath = fileURL.path(percentEncoded: false) - + var fileIsDirectory: ObjCBool = false guard FileManager.default.fileExists(atPath: filePath, isDirectory: &fileIsDirectory), - fileIsDirectory.boolValue == false else { + fileIsDirectory.boolValue == false + else { print("FILE IS A DIRECTORY?") return } - + var fileSize: UInt = 0 - + // Data size let fileAttributes = try? FileManager.default.attributesOfItem(atPath: filePath) if let sizeAttribute = fileAttributes?[.size] as? NSNumber { print("DATA SIZE \(sizeAttribute.uintValue)") fileSize += sizeAttribute.uintValue } - + // Resource size let resourceURL = fileURL.appendingPathComponent("..namedfork/rsrc") - - + print("RESOURCE PATH \(resourceURL)") - let resourceAttributes = try? FileManager.default.attributesOfItem(atPath: resourceURL.path(percentEncoded: false)) + let resourceAttributes = try? FileManager.default.attributesOfItem( + atPath: resourceURL.path(percentEncoded: false)) if let sizeAttribute = resourceAttributes?[.size] as? NSNumber { print("RESOURCE SIZE \(sizeAttribute.uintValue)") fileSize += sizeAttribute.uintValue } - + print("FILE SIZE? \(fileSize)") - + guard fileSize > 0 else { print("FILE IS EMPTY??") return } - + print("FILE SIZE: \(fileSize) NAME: \(fileName) PATH: \(path)") - - self.client.sendUploadFile(name: fileName, path: path) { [weak self] success, uploadReferenceNumber in + + self.client.sendUploadFile(name: fileName, path: path) { + [weak self] success, uploadReferenceNumber in print("UPLOAD REFERENCE: \(String(describing: uploadReferenceNumber))") - + if let self = self, - let address = self.server?.address, - let port = self.server?.port, - let referenceNumber = uploadReferenceNumber, - let fileClient = HotlineFileUploadClient(upload: fileURL, address: address, port: UInt16(port), reference: referenceNumber) { - + let address = self.server?.address, + let port = self.server?.port, + let referenceNumber = uploadReferenceNumber, + let fileClient = HotlineFileUploadClient( + upload: fileURL, address: address, port: UInt16(port), reference: referenceNumber) + { + print("GOING TO UPLOAD") - + fileClient.delegate = self self.downloads.append(fileClient) - + let transfer = TransferInfo(id: referenceNumber, title: fileName, size: fileSize) transfer.uploadCallback = callback self.transfers.append(transfer) - + fileClient.start() } } - + } - + @MainActor func getFileDetails(_ fileName: String, path: [String]) async -> FileDetails? { var fullPath: [String] = [] if path.count > 1 { - fullPath = Array(path[0..<path.count-1]) + fullPath = Array(path[0..<path.count - 1]) } - + return await withCheckedContinuation { [weak self] continuation in self?.client.sendGetFileInfo(name: fileName, path: fullPath) { info in continuation.resume(returning: info) } } } - + @MainActor func deleteFile(_ fileName: String, path: [String]) async -> Bool { var fullPath: [String] = [] if path.count > 1 { - fullPath = Array(path[0..<path.count-1]) + fullPath = Array(path[0..<path.count - 1]) } - + return await withCheckedContinuation { [weak self] continuation in self?.client.sendDeleteFile(name: fileName, path: fullPath) { success in continuation.resume(returning: success) } } } - - @MainActor func previewFile(_ fileName: String, path: [String], complete callback: ((PreviewFileInfo?) -> Void)? = nil) { + + @MainActor func previewFile( + _ fileName: String, path: [String], complete callback: ((PreviewFileInfo?) -> Void)? = nil + ) { var fullPath: [String] = [] if path.count > 1 { - fullPath = Array(path[0..<path.count-1]) + fullPath = Array(path[0..<path.count - 1]) } - - self.client.sendDownloadFile(name: fileName, path: fullPath, preview: true) { [weak self] success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount in + + self.client.sendDownloadFile(name: fileName, path: fullPath, preview: true) { + [weak self] + success, downloadReferenceNumber, downloadTransferSize, downloadFileSize, downloadWaitingCount + in guard success else { callback?(nil) return } - + print("GOT DOWNLOAD REPLY:") print("SUCCESS?", success) print("TRANSFER SIZE: \(downloadTransferSize.debugDescription)") print("FILE SIZE: \(downloadFileSize.debugDescription)") print("REFERENCE NUM: \(downloadReferenceNumber.debugDescription)") print("WAITING COUNT: \(downloadWaitingCount.debugDescription)") - + var info: PreviewFileInfo? = nil - - if - let address = self?.server?.address, + + if let address = self?.server?.address, let port = self?.server?.port, let referenceNumber = downloadReferenceNumber, - let transferSize = downloadTransferSize { - - info = PreviewFileInfo(id: referenceNumber, address: address, port: port, size: transferSize, name: fileName) + let transferSize = downloadTransferSize + { + + info = PreviewFileInfo( + id: referenceNumber, address: address, port: port, size: transferSize, name: fileName) } - + callback?(info) } } - + @MainActor func deleteTransfer(id: UInt32) { if let b = self.bannerClient, b.referenceNumber == id { b.cancel() self.bannerClient = nil return } - + if let i = self.transfers.firstIndex(where: { $0.id == id }) { self.transfers.remove(at: i) } - + if let i = self.downloads.firstIndex(where: { $0.referenceNumber == id }) { let fileClient = self.downloads.remove(at: i) fileClient.cancel() } } - + @MainActor func deleteAllTransfers() { self.transfers = [] - + let downloads = self.downloads self.downloads = [] - + for fileClient in downloads { fileClient.cancel() } } - + @MainActor func downloadBanner(force: Bool = false) { guard self.serverVersion >= 150 else { return } - + if self.bannerClient != nil || force { self.bannerClient?.delegate = nil self.bannerClient?.cancel() self.bannerClient = nil - + if force { self.bannerImage = nil } } - + if self.bannerImage != nil { return } - - self.client.sendDownloadBanner { [weak self] success, downloadReferenceNumber, downloadTransferSize in + + self.client.sendDownloadBanner { + [weak self] success, downloadReferenceNumber, downloadTransferSize in if !success { return } - - if - let self = self, + + if let self = self, let address = self.server?.address, let port = self.server?.port, let referenceNumber = downloadReferenceNumber, - let transferSize = downloadTransferSize { - self.bannerClient = HotlineFilePreviewClient(address: address, port: UInt16(port), reference: referenceNumber, size: UInt32(transferSize)) + let transferSize = downloadTransferSize + { + self.bannerClient = HotlineFilePreviewClient( + address: address, port: UInt16(port), reference: referenceNumber, + size: UInt32(transferSize)) self.bannerClient?.delegate = self self.bannerClient?.start() } } } - + // MARK: - Hotline Delegate - + @MainActor func hotlineStatusChanged(status: HotlineClientStatus) { print("Hotline: Connection status changed to: \(status)") - + if status == .disconnected { self.serverVersion = 123 self.serverName = nil self.access = nil - + self.users = [] self.chat = [] self.messageBoard = [] @@ -773,32 +821,31 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega self.filesLoaded = false self.news = [] self.newsLoaded = false - + self.bannerImage = nil if let b = self.bannerClient { b.cancel() self.bannerClient = nil } - + self.deleteAllTransfers() - } - else if status == .loggedIn { + } else if status == .loggedIn { if Prefs.shared.playSounds && Prefs.shared.playLoggedInSound { SoundEffectPlayer.shared.playSoundEffect(.loggedIn) } } - + self.status = status } - + func hotlineGetUserInfo() -> (String, UInt16) { return (self.username, UInt16(self.iconID)) } - + func hotlineReceivedAgreement(text: String) { self.chat.append(ChatMessage(text: text, type: .agreement, date: Date())) } - + func hotlineReceivedNewsPost(message: String) { let messageBoardRegex = /([\s\r\n]*[_\-]+[\s\r\n]+)/ let matches = message.matches(of: messageBoardRegex) @@ -809,44 +856,44 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega } else { self.messageBoard.insert(message, at: 0) } - + SoundEffectPlayer.shared.playSoundEffect(.newNews) } - + func hotlineReceivedServerMessage(message: String) { if Prefs.shared.playChatSound && Prefs.shared.playChatSound { SoundEffectPlayer.shared.playSoundEffect(.serverMessage) } - + print("Hotline: received server message:\n\(message)") self.chat.append(ChatMessage(text: message, type: .server, date: Date())) } - + func hotlineReceivedPrivateMessage(userID: UInt16, message: String) { if let existingUserIndex = self.users.firstIndex(where: { $0.id == UInt(userID) }) { let user = self.users[existingUserIndex] print("Hotline: received private message from \(user.name): \(message)") - + if Prefs.shared.playPrivateMessageSound && Prefs.shared.playPrivateMessageSound { if self.unreadInstantMessages[userID] == nil { SoundEffectPlayer.shared.playSoundEffect(.serverMessage) - } - else { + } else { SoundEffectPlayer.shared.playSoundEffect(.chatMessage) } } - - let instantMessage = InstantMessage(direction: .incoming, text: message.convertingLinksToMarkdown(), type: .message, date: Date()) + + let instantMessage = InstantMessage( + direction: .incoming, text: message.convertingLinksToMarkdown(), type: .message, + date: Date()) if self.instantMessages[userID] == nil { self.instantMessages[userID] = [instantMessage] - } - else { + } else { self.instantMessages[userID]!.append(instantMessage) } self.unreadInstantMessages[userID] = userID } } - + func hotlineReceivedChatMessage(message: String) { if Prefs.shared.playSounds && Prefs.shared.playChatSound { SoundEffectPlayer.shared.playSoundEffect(.chatMessage) @@ -854,11 +901,11 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega self.chat.append(ChatMessage(text: message, type: .message, date: Date())) self.unreadPublicChat = true } - + func hotlineReceivedUserList(users: [HotlineUser]) { var existingUserIDs: [UInt16] = [] var userList: [User] = [] - + for u in users { if let i = self.users.firstIndex(where: { $0.id == u.id }) { // If a user is already in the user list we have to assume @@ -866,59 +913,62 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega // which means let's keep their existing info. existingUserIDs.append(u.id) userList.append(self.users[i]) - } - else { + } else { userList.append(User(hotlineUser: u)) } } - + if !existingUserIDs.isEmpty { self.users = self.users.filter { !existingUserIDs.contains($0.id) } } - + self.users = userList + self.users } - + func hotlineUserChanged(user: HotlineUser) { self.addOrUpdateHotlineUser(user) } - + func hotlineUserDisconnected(userID: UInt16) { if let existingUserIndex = self.users.firstIndex(where: { $0.id == UInt(userID) }) { let user = self.users.remove(at: existingUserIndex) - + if Prefs.shared.showJoinLeaveMessages { self.chat.append(ChatMessage(text: "\(user.name) left", type: .status, date: Date())) } - + if Prefs.shared.playSounds && Prefs.shared.playLeaveSound { SoundEffectPlayer.shared.playSoundEffect(.userLogout) } } } - + func hotlineReceivedUserAccess(options: HotlineUserAccessOptions) { print("Hotline: got access options") HotlineUserAccessOptions.printAccessOptions(options) - + self.access = options } - + func hotlineReceivedErrorMessage(code: UInt32, message: String?) { print("Hotline: received error message \(code)", message.debugDescription) - - self.errorDisplayed = (message != nil) // Show error if there is a message to display. + + self.errorDisplayed = (message != nil) // Show error if there is a message to display. self.errorMessage = message - + if self.errorDisplayed, - Prefs.shared.playSounds && Prefs.shared.playErrorSound { + Prefs.shared.playSounds && Prefs.shared.playErrorSound + { SoundEffectPlayer.shared.playSoundEffect(.error) } } - + // MARK: - Hotline Transfer Delegate - - func hotlineTransferStatusChanged(client: HotlineTransferClient, reference: UInt32, status: HotlineTransferStatus, timeRemaining: TimeInterval) { + + func hotlineTransferStatusChanged( + client: HotlineTransferClient, reference: UInt32, status: HotlineTransferStatus, + timeRemaining: TimeInterval + ) { switch status { case .unconnected: break @@ -953,34 +1003,34 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega } } } - - func hotlineFileDownloadReceivedInfo(client: HotlineFileDownloadClient, reference: UInt32, info: HotlineFileInfoFork) { + + func hotlineFileDownloadReceivedInfo( + client: HotlineFileDownloadClient, reference: UInt32, info: HotlineFileInfoFork + ) { if let transfer = self.transfers.first(where: { $0.id == reference }) { transfer.title = info.name } } - + func hotlineFilePreviewComplete(client: HotlineFilePreviewClient, reference: UInt32, data: Data) { if let b = self.bannerClient, b.referenceNumber == reference { #if os(macOS) - self.bannerImage = NSImage(data: data) + self.bannerImage = NSImage(data: data) #elseif os(iOS) - self.bannerImage = UIImage(data: data) + self.bannerImage = UIImage(data: data) #endif - } - else - if let i = self.transfers.firstIndex(where: { $0.id == reference }) { + } else if let i = self.transfers.firstIndex(where: { $0.id == reference }) { let transfer = self.transfers[i] transfer.previewCallback?(transfer, data) self.transfers.remove(at: i) } - + if let i = self.downloads.firstIndex(where: { $0.referenceNumber == reference }) { self.downloads.remove(at: i) } } - - func hotlineFileDownloadComplete(client: HotlineFileDownloadClient, reference: UInt32, at: URL) { + + func hotlineFileDownloadComplete(client: HotlineFileDownloadClient, reference: UInt32, at: URL) { if let i = self.transfers.firstIndex(where: { $0.id == reference }) { let transfer = self.transfers[i] transfer.fileURL = at @@ -989,12 +1039,12 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega SoundEffectPlayer.shared.playSoundEffect(.transferComplete) } } - + if let i = self.downloads.firstIndex(where: { $0.referenceNumber == reference }) { self.downloads.remove(at: i) } } - + func hotlineFileUploadComplete(client: HotlineFileUploadClient, reference: UInt32) { if let i = self.transfers.firstIndex(where: { $0.id == reference }) { let transfer = self.transfers[i] @@ -1003,31 +1053,30 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega SoundEffectPlayer.shared.playSoundEffect(.transferComplete) } } - + if let i = self.downloads.firstIndex(where: { $0.referenceNumber == reference }) { self.downloads.remove(at: i) } } - + // MARK: - Utilities - + func updateServerTitle() { self.serverTitle = self.serverName ?? self.server?.name ?? server?.address ?? "Server" } - + private func addOrUpdateHotlineUser(_ user: HotlineUser) { print("Hotline: users: \n\(self.users)") if let i = self.users.firstIndex(where: { $0.id == user.id }) { print("Hotline: updating user \(self.users[i].name)") self.users[i] = User(hotlineUser: user) - } - else { + } else { if !self.users.isEmpty { if Prefs.shared.playSounds && Prefs.shared.playJoinSound { SoundEffectPlayer.shared.playSoundEffect(.userLogin) } } - + print("Hotline: added user: \(user.name)") self.users.append(User(hotlineUser: user)) if Prefs.shared.showJoinLeaveMessages { @@ -1035,55 +1084,53 @@ class Hotline: Equatable, HotlineClientDelegate, HotlineFileDownloadClientDelega } } } - + private func findFile(in filesToSearch: [FileInfo], at path: [String]) -> FileInfo? { guard !path.isEmpty, !filesToSearch.isEmpty else { return nil } - + let currentName = path[0] - + for file in filesToSearch { if file.name == currentName { if path.count == 1 { return file - } - else if let subfiles = file.children { + } else if let subfiles = file.children { let remainingPath = Array(path[1...]) return self.findFile(in: subfiles, at: remainingPath) } } } - + return nil } - + private func findNews(in newsToSearch: [NewsInfo], at path: [String]) -> NewsInfo? { guard !path.isEmpty, !newsToSearch.isEmpty, let currentName = path.first else { return nil } - + for news in newsToSearch { if news.name == currentName { if path.count == 1 { return news - } - else if !news.children.isEmpty { + } else if !news.children.isEmpty { let remainingPath = Array(path[1...]) return self.findNews(in: news.children, at: remainingPath) } } } - + return nil } - + private func findNewsArticle(id articleID: UInt32, at path: [String]) -> NewsInfo? { guard let parent = self.findNews(in: self.news, at: path), !parent.children.isEmpty else { return nil } - + return parent.children.first { child in guard let childArticleID = child.articleID else { return false } - + return child.type == .article && child.articleID == childArticleID } } diff --git a/Hotline/Models/NewsInfo.swift b/Hotline/Models/NewsInfo.swift index 6ee28b8..caf532f 100644 --- a/Hotline/Models/NewsInfo.swift +++ b/Hotline/Models/NewsInfo.swift @@ -8,28 +8,28 @@ enum NewsInfoType { @Observable class NewsInfo: Identifiable, Hashable { let id: UUID = UUID() - + var name: String var count: UInt let type: NewsInfoType - + var categoryID: UUID? var articleID: UInt? var parentID: UInt? - + var path: [String] var expanded: Bool = false var children: [NewsInfo] = [] - + var articleFlavors: [String]? var articleUsername: String? var articleDate: Date? var read: Bool = false - + var expandable: Bool { self.type == .bundle || self.type == .category || self.children.count > 0 } - + var lookupPath: String? { switch self.type { case .bundle, .category: @@ -38,21 +38,21 @@ enum NewsInfoType { guard let aid = self.articleID else { return nil } -// if let pid = self.parentID, pid != 0 { -// return "/\(self.path.joined(separator: "/"))/\(pid)/\(aid)" -// } + // if let pid = self.parentID, pid != 0 { + // return "/\(self.path.joined(separator: "/"))/\(pid)/\(aid)" + // } return "/\(self.path.joined(separator: "/"))/\(aid)" } } - + var parentArticleLookupPath: String? { switch self.type { case .bundle, .category: -// if self.path.count <= 1 { -// return "/" -// } -// let parentPath = self.path[0..<self.path.count-1] -// return "/\(parentPath.joined(separator: "/"))" + // if self.path.count <= 1 { + // return "/" + // } + // let parentPath = self.path[0..<self.path.count-1] + // return "/\(parentPath.joined(separator: "/"))" return nil case .article: guard let pid = self.parentID, pid != 0 else { @@ -61,7 +61,7 @@ enum NewsInfoType { return "/\(self.path.joined(separator: "/"))/\(pid)" } } - + init(hotlineNewsCategory: HotlineNewsCategory) { self.categoryID = hotlineNewsCategory.id self.articleID = nil @@ -69,15 +69,14 @@ enum NewsInfoType { self.name = hotlineNewsCategory.name self.count = UInt(hotlineNewsCategory.count) self.path = hotlineNewsCategory.path - + if hotlineNewsCategory.type == 2 { self.type = .bundle - } - else { + } else { self.type = .category } } - + init(hotlineNewsArticle: HotlineNewsArticle) { self.articleID = UInt(hotlineNewsArticle.id) self.parentID = hotlineNewsArticle.parentID == 0 ? nil : UInt(hotlineNewsArticle.parentID) @@ -85,19 +84,19 @@ enum NewsInfoType { self.categoryID = nil self.name = hotlineNewsArticle.title self.count = 0 -// self.count = UInt(hotlineNewsArticle.count) + // self.count = UInt(hotlineNewsArticle.count) self.path = hotlineNewsArticle.path self.type = .article - + self.articleFlavors = hotlineNewsArticle.flavors.map { $0.0 } self.articleUsername = hotlineNewsArticle.username self.articleDate = hotlineNewsArticle.date } - + func hash(into hasher: inout Hasher) { hasher.combine(self.id) } - + static func == (lhs: NewsInfo, rhs: NewsInfo) -> Bool { return lhs.id == rhs.id } diff --git a/Hotline/Models/Preferences.swift b/Hotline/Models/Preferences.swift index 207ce91..bcceec5 100644 --- a/Hotline/Models/Preferences.swift +++ b/Hotline/Models/Preferences.swift @@ -23,7 +23,7 @@ enum PrefsKeys: String { @Observable class Prefs { init() { - UserDefaults.standard.register(defaults:[ + UserDefaults.standard.register(defaults: [ PrefsKeys.username.rawValue: "guest", PrefsKeys.userIconID.rawValue: 191, PrefsKeys.refusePrivateMessages.rawValue: false, @@ -42,93 +42,141 @@ class Prefs { PrefsKeys.showBannerToolbar.rawValue: true, PrefsKeys.showJoinLeaveMessages.rawValue: true, ]) - + self.username = UserDefaults.standard.string(forKey: PrefsKeys.username.rawValue)! self.userIconID = UserDefaults.standard.integer(forKey: PrefsKeys.userIconID.rawValue) - self.refusePrivateMessages = UserDefaults.standard.bool(forKey: PrefsKeys.refusePrivateMessages.rawValue) - self.refusePrivateChat = UserDefaults.standard.bool(forKey: PrefsKeys.refusePrivateChat.rawValue) - self.enableAutomaticMessage = UserDefaults.standard.bool(forKey: PrefsKeys.enableAutomaticMessage.rawValue) - self.automaticMessage = UserDefaults.standard.string(forKey: PrefsKeys.automaticMessage.rawValue)! + self.refusePrivateMessages = UserDefaults.standard.bool( + forKey: PrefsKeys.refusePrivateMessages.rawValue) + self.refusePrivateChat = UserDefaults.standard.bool( + forKey: PrefsKeys.refusePrivateChat.rawValue) + self.enableAutomaticMessage = UserDefaults.standard.bool( + forKey: PrefsKeys.enableAutomaticMessage.rawValue) + self.automaticMessage = UserDefaults.standard.string( + forKey: PrefsKeys.automaticMessage.rawValue)! self.playSounds = UserDefaults.standard.bool(forKey: PrefsKeys.playSounds.rawValue) self.playChatSound = UserDefaults.standard.bool(forKey: PrefsKeys.playChatSound.rawValue) - self.playFileTransferCompleteSound = UserDefaults.standard.bool(forKey: PrefsKeys.playFileTransferCompleteSound.rawValue) - self.playPrivateMessageSound = UserDefaults.standard.bool(forKey: PrefsKeys.playPrivateMessageSound.rawValue) + self.playFileTransferCompleteSound = UserDefaults.standard.bool( + forKey: PrefsKeys.playFileTransferCompleteSound.rawValue) + self.playPrivateMessageSound = UserDefaults.standard.bool( + forKey: PrefsKeys.playPrivateMessageSound.rawValue) self.playJoinSound = UserDefaults.standard.bool(forKey: PrefsKeys.playJoinSound.rawValue) self.playLeaveSound = UserDefaults.standard.bool(forKey: PrefsKeys.playLeaveSound.rawValue) - self.playLoggedInSound = UserDefaults.standard.bool(forKey: PrefsKeys.playLoggedInSound.rawValue) + self.playLoggedInSound = UserDefaults.standard.bool( + forKey: PrefsKeys.playLoggedInSound.rawValue) self.playErrorSound = UserDefaults.standard.bool(forKey: PrefsKeys.playErrorSound.rawValue) - self.playChatInvitationSound = UserDefaults.standard.bool(forKey: PrefsKeys.playChatInvitationSound.rawValue) - self.showBannerToolbar = UserDefaults.standard.bool(forKey: PrefsKeys.showBannerToolbar.rawValue) - self.showJoinLeaveMessages = UserDefaults.standard.bool(forKey: PrefsKeys.showJoinLeaveMessages.rawValue) + self.playChatInvitationSound = UserDefaults.standard.bool( + forKey: PrefsKeys.playChatInvitationSound.rawValue) + self.showBannerToolbar = UserDefaults.standard.bool( + forKey: PrefsKeys.showBannerToolbar.rawValue) + self.showJoinLeaveMessages = UserDefaults.standard.bool( + forKey: PrefsKeys.showJoinLeaveMessages.rawValue) } - + public static let shared = Prefs() var username: String { didSet { UserDefaults.standard.set(self.username, forKey: PrefsKeys.username.rawValue) } } - + var userIconID: Int { didSet { UserDefaults.standard.set(self.userIconID, forKey: PrefsKeys.userIconID.rawValue) } } - + var refusePrivateMessages: Bool { - didSet { UserDefaults.standard.set(self.refusePrivateMessages, forKey: PrefsKeys.refusePrivateMessages.rawValue) } + didSet { + UserDefaults.standard.set( + self.refusePrivateMessages, forKey: PrefsKeys.refusePrivateMessages.rawValue) + } } - + var playSounds: Bool { didSet { UserDefaults.standard.set(self.playSounds, forKey: PrefsKeys.playSounds.rawValue) } } - + var playChatSound: Bool { - didSet { UserDefaults.standard.set(self.playChatSound, forKey: PrefsKeys.playChatSound.rawValue) } + didSet { + UserDefaults.standard.set(self.playChatSound, forKey: PrefsKeys.playChatSound.rawValue) + } } - + var playFileTransferCompleteSound: Bool { - didSet { UserDefaults.standard.set(self.playFileTransferCompleteSound, forKey: PrefsKeys.playFileTransferCompleteSound.rawValue) } + didSet { + UserDefaults.standard.set( + self.playFileTransferCompleteSound, forKey: PrefsKeys.playFileTransferCompleteSound.rawValue + ) + } } - + var playPrivateMessageSound: Bool { - didSet { UserDefaults.standard.set(self.playPrivateMessageSound, forKey: PrefsKeys.playPrivateMessageSound.rawValue) } + didSet { + UserDefaults.standard.set( + self.playPrivateMessageSound, forKey: PrefsKeys.playPrivateMessageSound.rawValue) + } } - + var playJoinSound: Bool { - didSet { UserDefaults.standard.set(self.playJoinSound, forKey: PrefsKeys.playJoinSound.rawValue) } + didSet { + UserDefaults.standard.set(self.playJoinSound, forKey: PrefsKeys.playJoinSound.rawValue) + } } - + var playLeaveSound: Bool { - didSet { UserDefaults.standard.set(self.playLeaveSound, forKey: PrefsKeys.playLeaveSound.rawValue) } + didSet { + UserDefaults.standard.set(self.playLeaveSound, forKey: PrefsKeys.playLeaveSound.rawValue) + } } - + var playLoggedInSound: Bool { - didSet { UserDefaults.standard.set(self.playLoggedInSound, forKey: PrefsKeys.playLoggedInSound.rawValue) } + didSet { + UserDefaults.standard.set( + self.playLoggedInSound, forKey: PrefsKeys.playLoggedInSound.rawValue) + } } - + var playErrorSound: Bool { - didSet { UserDefaults.standard.set(self.playErrorSound, forKey: PrefsKeys.playErrorSound.rawValue) } + didSet { + UserDefaults.standard.set(self.playErrorSound, forKey: PrefsKeys.playErrorSound.rawValue) + } } - + var playChatInvitationSound: Bool { - didSet { UserDefaults.standard.set(self.playChatInvitationSound, forKey: PrefsKeys.playChatInvitationSound.rawValue) } + didSet { + UserDefaults.standard.set( + self.playChatInvitationSound, forKey: PrefsKeys.playChatInvitationSound.rawValue) + } } - + var refusePrivateChat: Bool { - didSet { UserDefaults.standard.set(self.refusePrivateChat, forKey: PrefsKeys.refusePrivateChat.rawValue) } + didSet { + UserDefaults.standard.set( + self.refusePrivateChat, forKey: PrefsKeys.refusePrivateChat.rawValue) + } } - + var enableAutomaticMessage: Bool { - didSet { UserDefaults.standard.set(self.enableAutomaticMessage, forKey: PrefsKeys.enableAutomaticMessage.rawValue) } + didSet { + UserDefaults.standard.set( + self.enableAutomaticMessage, forKey: PrefsKeys.enableAutomaticMessage.rawValue) + } } - + var automaticMessage: String { - didSet { UserDefaults.standard.set(self.automaticMessage, forKey: PrefsKeys.automaticMessage.rawValue) } + didSet { + UserDefaults.standard.set(self.automaticMessage, forKey: PrefsKeys.automaticMessage.rawValue) + } } - + var showBannerToolbar: Bool { - didSet { UserDefaults.standard.set(self.showBannerToolbar, forKey: PrefsKeys.showBannerToolbar.rawValue) } + didSet { + UserDefaults.standard.set( + self.showBannerToolbar, forKey: PrefsKeys.showBannerToolbar.rawValue) + } } - + var showJoinLeaveMessages: Bool { - didSet { UserDefaults.standard.set(self.showJoinLeaveMessages, forKey: PrefsKeys.showJoinLeaveMessages.rawValue) } + didSet { + UserDefaults.standard.set( + self.showJoinLeaveMessages, forKey: PrefsKeys.showJoinLeaveMessages.rawValue) + } } } diff --git a/Hotline/Models/PreviewFileInfo.swift b/Hotline/Models/PreviewFileInfo.swift index 7dd692f..06a2966 100644 --- a/Hotline/Models/PreviewFileInfo.swift +++ b/Hotline/Models/PreviewFileInfo.swift @@ -12,14 +12,13 @@ struct PreviewFileInfo: Identifiable, Codable { var port: Int var size: Int var name: String - + var previewType: FilePreviewType { let fileExtension = (self.name as NSString).pathExtension if let fileType = UTType(filenameExtension: fileExtension) { if fileType.isSubtype(of: .image) { return .image - } - else if fileType.isSubtype(of: .text) { + } else if fileType.isSubtype(of: .text) { return .text } } diff --git a/Hotline/Models/Server.swift b/Hotline/Models/Server.swift index 3f4c9e8..a3e3dde 100644 --- a/Hotline/Models/Server.swift +++ b/Hotline/Models/Server.swift @@ -4,14 +4,18 @@ struct Server: Codable { var name: String? var description: String? var users: Int - + var address: String var port: Int var login: String var password: String var autoconnect: Bool = false - - init(name: String?, description: String?, address: String, port: Int = HotlinePorts.DefaultServerPort, users: Int = 0, login: String? = nil, password: String? = nil, autoconnect: Bool? = false) { + + init( + name: String?, description: String?, address: String, + port: Int = HotlinePorts.DefaultServerPort, users: Int = 0, login: String? = nil, + password: String? = nil, autoconnect: Bool? = false + ) { self.name = name self.description = description self.address = address.lowercased() @@ -21,27 +25,27 @@ struct Server: Codable { self.password = password ?? "" self.autoconnect = autoconnect ?? false } - + init?(url: URL) { guard url.scheme?.lowercased() == "hotline" else { return nil } - + guard let host = url.host(percentEncoded: false) else { return nil } - + self.name = nil self.description = nil self.users = 0 - + self.address = host.lowercased() self.port = url.port ?? HotlinePorts.DefaultServerPort - + self.login = url.user(percentEncoded: false) ?? "" self.password = url.password(percentEncoded: false) ?? "" } - + static func parseServerAddressAndPort(_ address: String) -> (String, Int) { let url = URL(string: "hotline://\(address)") let port = url?.port ?? HotlinePorts.DefaultServerPort @@ -56,7 +60,8 @@ extension Server: Identifiable { extension Server: Equatable { static func == (lhs: Server, rhs: Server) -> Bool { - return (lhs.address == rhs.address) && (lhs.port == rhs.port) && (lhs.login == rhs.login) && (lhs.password == rhs.password) + return (lhs.address == rhs.address) && (lhs.port == rhs.port) && (lhs.login == rhs.login) + && (lhs.password == rhs.password) } } diff --git a/Hotline/Models/Tracker.swift b/Hotline/Models/Tracker.swift index 171cb35..0ebcf2f 100644 --- a/Hotline/Models/Tracker.swift +++ b/Hotline/Models/Tracker.swift @@ -3,12 +3,12 @@ import SwiftUI @Observable final class Tracker { let address: String let port: Int - + init(address: String, port: Int = HotlinePorts.DefaultTrackerPort) { self.address = address self.port = port } - + static func parseTrackerAddressAndPort(_ address: String) -> (String, Int) { let url = URL(string: "hotlinetracker://\(address)") let port = url?.port ?? HotlinePorts.DefaultTrackerPort diff --git a/Hotline/Models/TransferInfo.swift b/Hotline/Models/TransferInfo.swift index 50b23f5..8aacf1d 100644 --- a/Hotline/Models/TransferInfo.swift +++ b/Hotline/Models/TransferInfo.swift @@ -3,32 +3,32 @@ import SwiftUI @Observable class TransferInfo: Identifiable, Equatable, Hashable { var id: UInt32 - + var title: String var size: UInt var progress: Double = 0.0 var timeRemaining: TimeInterval = 0.0 var completed: Bool = false var failed: Bool = false - + // For file based transfers (i.e. not previews) var fileURL: URL? = nil - + var progressCallback: ((TransferInfo, Double) -> Void)? = nil var downloadCallback: ((TransferInfo, URL) -> Void)? = nil var uploadCallback: ((TransferInfo) -> Void)? = nil var previewCallback: ((TransferInfo, Data) -> Void)? = nil - + init(id: UInt32, title: String, size: UInt) { self.id = id self.title = title self.size = size } - + static func == (lhs: TransferInfo, rhs: TransferInfo) -> Bool { return lhs.id == rhs.id } - + func hash(into hasher: inout Hasher) { hasher.combine(self.id) } diff --git a/Hotline/Models/User.swift b/Hotline/Models/User.swift index 21a8fff..90b0801 100644 --- a/Hotline/Models/User.swift +++ b/Hotline/Models/User.swift @@ -12,21 +12,21 @@ struct User: Identifiable { var name: String var iconID: UInt var status: UserStatus - + var isAdmin: Bool { self.status.contains(.admin) } var isIdle: Bool { self.status.contains(.idle) } - + init(hotlineUser: HotlineUser) { var status: UserStatus = UserStatus() if hotlineUser.isIdle { status.update(with: .idle) } if hotlineUser.isAdmin { status.update(with: .admin) } - + self.id = hotlineUser.id self.name = hotlineUser.name self.iconID = UInt(hotlineUser.iconID) self.status = status } - + init(id: UInt16, name: String, iconID: UInt, status: UserStatus) { self.id = id self.name = name |