import Foundation import AppKit public actor UploadManager { private let client: APIClient private let config: SyncConfig public init(client: APIClient, config: SyncConfig) { self.client = client self.config = config } public func uploadFile( _ url: URL, duplicateAction: UploadDuplicateAction? = nil, onProgress: UploadProgressHandler? = nil ) async throws -> UploadResult { let response = try await client.uploadFile( url: url, folder: config.defaultUploadFolder, duplicateAction: duplicateAction, onProgress: onProgress ) let fullURL = resolvedURL(response.url ?? response.attachmentUrl) // Copy URL to clipboard await MainActor.run { NSPasteboard.general.clearContents() NSPasteboard.general.setString(fullURL.absoluteString, forType: .string) } return UploadResult( filename: url.lastPathComponent, hash: response.hash, url: fullURL, copiedToClipboard: true ) } public func uploadData( _ data: Data, filename: String, duplicateAction: UploadDuplicateAction? = nil, onProgress: UploadProgressHandler? = nil ) async throws -> UploadResult { let response = try await client.uploadData( data, filename: filename, folder: config.defaultUploadFolder, duplicateAction: duplicateAction, onProgress: onProgress ) let fullURL = resolvedURL(response.url ?? response.attachmentUrl) // Copy URL to clipboard await MainActor.run { NSPasteboard.general.clearContents() NSPasteboard.general.setString(fullURL.absoluteString, forType: .string) } return UploadResult( filename: filename, hash: response.hash, url: fullURL, copiedToClipboard: true ) } public func createNote(from text: String, title: String? = nil) async throws -> DocumentSaveResponse { let noteTitle = title ?? generateTitle(from: text) let folder = config.defaultUploadFolder ?? "" let path = folder.isEmpty ? "\(sanitizeFilename(noteTitle)).md" : "\(folder)/\(sanitizeFilename(noteTitle)).md" let content = "# \(noteTitle)\n\n\(text)" return try await client.saveDocument(path: path, content: content) } private func generateTitle(from text: String) -> String { let lines = text.split(separator: "\n", omittingEmptySubsequences: true) if let firstLine = lines.first { let title = String(firstLine).trimmingCharacters(in: .whitespaces) if title.count > 60 { return String(title.prefix(60)) + "..." } return title } return "Note \(ISO8601DateFormatter().string(from: Date()))" } private func sanitizeFilename(_ name: String) -> String { let invalidChars = CharacterSet(charactersIn: ":/\\?%*|\"<>").union(.controlCharacters) return name.components(separatedBy: invalidChars).joined(separator: "-") } private func resolvedURL(_ path: String) -> URL { URL(string: path, relativeTo: client.baseURL)?.absoluteURL ?? client.baseURL.appendingPathComponent(path) } } public struct UploadResult { public let filename: String public let hash: String public let url: URL public let copiedToClipboard: Bool } public struct UploadCancelledError: LocalizedError { public init() {} public var errorDescription: String? { "Upload cancelled" } } @MainActor public func promptForUploadDuplicate(_ error: APIError, filename: String) -> UploadDuplicateAction? { let alert = NSAlert() alert.alertStyle = .warning if error.conflictType == "document" { alert.messageText = "A document named \(filename) already exists" alert.informativeText = "Choose whether to replace the inbox document or keep both files." alert.addButton(withTitle: "Keep Both") alert.addButton(withTitle: "Replace") alert.addButton(withTitle: "Cancel") switch alert.runModal() { case .alertFirstButtonReturn: return .rename case .alertSecondButtonReturn: return .overwrite default: return nil } } alert.messageText = "\(filename) was already uploaded" alert.informativeText = "The server already has the same file content. Use its existing URL?" alert.addButton(withTitle: "Copy Existing URL") alert.addButton(withTitle: "Cancel") return alert.runModal() == .alertFirstButtonReturn ? .reuse : nil }