spb/zyquo-atlas Public License
The AI-native macOS web browser — every surface, intelligent.
Swift 75.2%
JavaScript 22%
Shell 2%
Makefile 0.9%
1//2// DownloadManager.swift3// Zyquo Atlas4//5// Author: Simon-Pierre Boucher6// Mail: contact@spboucher.ai7//8// Handles file downloads via WKDownload: routes new downloads to ~/Downloads,9// tracks progress, and exposes an observable list for the toolbar downloads10// popover (reveal / open). One manager per window, set as each web view's11// download-triggering delegate path through the Tab.12//1314import Foundation15import WebKit16import Combine1718@MainActor19final class DownloadManager: NSObject, ObservableObject {20 @Published private(set) var items: [DownloadItem] = []2122 private var progressObservations: [ObjectIdentifier: NSKeyValueObservation] = [:]2324 /// Attaches to a WKDownload (from WKNavigation/UI delegate download hooks).25 func attach(_ download: WKDownload) {26 download.delegate = self27 }2829 func reveal(_ item: DownloadItem) {30 guard let url = item.fileURL else { return }31 NSWorkspace.shared.activateFileViewerSelecting([url])32 }3334 func open(_ item: DownloadItem) {35 guard let url = item.fileURL, item.state == .finished else { return }36 NSWorkspace.shared.open(url)37 }3839 func clearFinished() { items.removeAll { $0.state == .finished } }4041 private func index(of download: WKDownload) -> Int? {42 items.firstIndex { $0.downloadID == ObjectIdentifier(download) }43 }44}4546// MARK: - Model4748struct DownloadItem: Identifiable {49 enum State: Equatable { case inProgress, finished, failed }50 let id = UUID()51 let downloadID: ObjectIdentifier52 var filename: String53 var fileURL: URL?54 var receivedBytes: Int64 = 055 var totalBytes: Int64 = 056 var state: State = .inProgress5758 var fraction: Double {59 totalBytes > 0 ? Double(receivedBytes) / Double(totalBytes) : 060 }61}6263// MARK: - WKDownloadDelegate6465extension DownloadManager: WKDownloadDelegate {66 func download(_ download: WKDownload,67 decideDestinationUsing response: URLResponse,68 suggestedFilename: String,69 completionHandler: @escaping (URL?) -> Void) {70 let downloads = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first71 ?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Downloads")72 var dest = downloads.appendingPathComponent(suggestedFilename)73 var n = 174 while FileManager.default.fileExists(atPath: dest.path) {75 let ext = (suggestedFilename as NSString).pathExtension76 let base = (suggestedFilename as NSString).deletingPathExtension77 dest = downloads.appendingPathComponent(ext.isEmpty ? "\(base) \(n)" : "\(base) \(n).\(ext)")78 n += 179 }80 let item = DownloadItem(downloadID: ObjectIdentifier(download),81 filename: dest.lastPathComponent, fileURL: dest,82 totalBytes: response.expectedContentLength)83 items.insert(item, at: 0)84 progressObservations[ObjectIdentifier(download)] =85 download.progress.observe(\.completedUnitCount) { [weak self] progress, _ in86 Task { @MainActor in87 guard let self, let i = self.index(of: download) else { return }88 self.items[i].receivedBytes = progress.completedUnitCount89 self.items[i].totalBytes = progress.totalUnitCount90 }91 }92 completionHandler(dest)93 }9495 func downloadDidFinish(_ download: WKDownload) {96 if let i = index(of: download) { items[i].state = .finished }97 progressObservations[ObjectIdentifier(download)]?.invalidate()98 progressObservations[ObjectIdentifier(download)] = nil99 }100101 func download(_ download: WKDownload, didFailWithError error: Error, resumeData: Data?) {102 if let i = index(of: download) { items[i].state = .failed }103 progressObservations[ObjectIdentifier(download)]?.invalidate()104 progressObservations[ObjectIdentifier(download)] = nil105 }106}107