diff options
Diffstat (limited to 'Hotline/Models/Hotline.swift')
| -rw-r--r-- | Hotline/Models/Hotline.swift | 699 |
1 files changed, 373 insertions, 326 deletions
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 } } |