diff options
Diffstat (limited to 'Hotline/State')
| -rw-r--r-- | Hotline/State/AppState.swift | 58 | ||||
| -rw-r--r-- | Hotline/State/FilePreviewState.swift | 207 | ||||
| -rw-r--r-- | Hotline/State/ServerState.swift | 4 |
3 files changed, 266 insertions, 3 deletions
diff --git a/Hotline/State/AppState.swift b/Hotline/State/AppState.swift index 3917ad0..558af2a 100644 --- a/Hotline/State/AppState.swift +++ b/Hotline/State/AppState.swift @@ -12,8 +12,64 @@ final class AppState { } - var activeHotline: Hotline? = nil + var activeHotline: HotlineState? = nil var activeServerState: ServerState? = nil var cloudKitReady: Bool = false + + // MARK: - Transfers + + /// All active transfers across all servers + /// Transfers persist even if you disconnect from the server + var transfers: [TransferInfo] = [] + + /// Track download tasks by reference number for cancellation + @ObservationIgnored private var transferTasks: [UUID: Task<Void, Never>] = [:] + + /// Add a transfer to the transfer list + @MainActor + func addTransfer(_ transfer: TransferInfo) { + self.transfers.append(transfer) + } + + /// Cancel a transfer by transfer ID + @MainActor + func cancelTransfer(id: UUID) { + guard let transferIndex = self.transfers.firstIndex(where: { $0.id == id }) else { + return + } + + // Cancel the task if it exists + if let task = self.transferTasks[id] { + task.cancel() + self.transferTasks.removeValue(forKey: id) + } + + // Remove from transfers list + self.transfers.remove(at: transferIndex) + } + + /// Cancel all active transfers + @MainActor + func cancelAllTransfers() { + for (_, task) in self.transferTasks { + task.cancel() + } + self.transferTasks.removeAll() + + // Clear transfers + self.transfers.removeAll() + } + + /// Register a transfer task + @MainActor + func registerTransferTask(_ task: Task<Void, Never>, transferID: UUID) { + self.transferTasks[transferID] = task + } + + /// Unregister a download task (called on completion/failure) + @MainActor + func unregisterTransferTask(for transferID: UUID) { + self.transferTasks.removeValue(forKey: transferID) + } } diff --git a/Hotline/State/FilePreviewState.swift b/Hotline/State/FilePreviewState.swift new file mode 100644 index 0000000..97bd907 --- /dev/null +++ b/Hotline/State/FilePreviewState.swift @@ -0,0 +1,207 @@ +// +// FilePreviewState.swift +// Hotline +// +// Modern file preview state using HotlineFilePreviewClientNew +// + +import SwiftUI +import UniformTypeIdentifiers + +// MARK: - Preview Type + +enum FilePreviewType: Equatable { + case unknown + case image + case text +} + +/// State for a file preview download +@MainActor +@Observable +final class FilePreviewState { + // MARK: - Properties + + let info: PreviewFileInfo + + private var previewClient: HotlineFilePreviewClientNew? + private var previewTask: Task<Void, Never>? + + var state: LoadState = .unloaded + var progress: Double = 0.0 + + var fileURL: URL? = nil + + #if os(iOS) + var image: UIImage? = nil + #elseif os(macOS) + var image: NSImage? = nil + #endif + + var text: String? = nil + var styledText: NSAttributedString? = nil + + // MARK: - Computed Properties + + var previewType: FilePreviewType { + info.previewType + } + + // MARK: - Initialization + + init(info: PreviewFileInfo) { + self.info = info + } + + nonisolated deinit { + // Note: Can't access @MainActor properties from deinit + // Cleanup will happen when previewClient is deallocated + } + + // MARK: - Public API + + func download() { + // Cancel any existing download + previewTask?.cancel() + previewClient?.cleanup() + + let task = Task { @MainActor in + do { + let client = HotlineFilePreviewClientNew( + fileName: info.name, + address: info.address, + port: UInt16(info.port), + reference: info.id, + size: UInt32(info.size) + ) + self.previewClient = client + + self.state = .loading + self.progress = 0.0 + + let url = try await client.preview { [weak self] progress in + guard let self else { return } + + Task { @MainActor in + switch progress { + case .preparing: + self.state = .loading + self.progress = 0.0 + + case .connecting: + self.state = .loading + self.progress = 0.0 + + case .connected: + self.state = .loading + self.progress = 0.0 + + case .transfer(name: _, size: _, total: _, progress: let p, speed: _, estimate: _): + self.state = .loading + self.progress = p + + case .completed(url: let url): + self.state = .loaded + self.progress = 1.0 + self.fileURL = url +// self.loadPreview(from: url) + + case .error(let error): + self.state = .failed + self.progress = 0.0 + print("FilePreviewState: Download failed: \(error)") + + case .unconnected: + break + } + } + } + + // Final load if not already loaded + if self.state != .loaded { + self.state = .loaded + self.progress = 1.0 + self.fileURL = url +// self.loadPreview(from: url) + } + + } catch is CancellationError { + // Cancelled, do nothing + return + } catch { + self.state = .failed + self.progress = 0.0 + print("FilePreviewState: Download error: \(error)") + } + } + + self.previewTask = task + } + + func cancel() { + previewTask?.cancel() + previewTask = nil + previewClient?.cancel() + } + + func cleanup() { + previewClient?.cleanup() + previewClient = nil + fileURL = nil + image = nil + text = nil + styledText = nil + } + + // MARK: - Private Implementation + + private func loadPreview(from url: URL) { + guard let data = try? Data(contentsOf: url) else { + self.state = .failed + print("FilePreviewState: Failed to read preview data from \(url.path)") + return + } + + switch self.previewType { + case .image: + #if os(iOS) + self.image = UIImage(data: data) + #elseif os(macOS) + self.image = NSImage(data: data) + #endif + + if self.image == nil { + self.state = .failed + print("FilePreviewState: Failed to create image from data") + } + + case .text: + 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 { + self.text = String(data: data, encoding: .utf8) + } + + if self.text == nil { + self.state = .failed + print("FilePreviewState: Failed to decode text data") + } + + case .unknown: + print("FilePreviewState: Unknown preview type for \(info.name)") + break + } + } +} + +// MARK: - Load State + +extension FilePreviewState { + enum LoadState: Equatable { + case unloaded + case loading + case loaded + case failed + } +} diff --git a/Hotline/State/ServerState.swift b/Hotline/State/ServerState.swift index e2fa40f..5913bf5 100644 --- a/Hotline/State/ServerState.swift +++ b/Hotline/State/ServerState.swift @@ -5,8 +5,8 @@ class ServerState: Equatable { var id: UUID = UUID() var selection: ServerNavigationType var serverName: String? = nil - var serverBanner: NSImage? = nil - var bannerColors: ColorArt? = nil +// var serverBanner: NSImage? = nil +// var bannerBackgroundColor: Color? = nil init(selection: ServerNavigationType) { self.selection = selection |