SPB Git

spb/zyquo-atlas Public License

The AI-native macOS web browser — every surface, intelligent.

Swift 75.2% JavaScript 22% Shell 2% Makefile 0.9%
4.0 KB · 107 lines swift
Raw Blame History
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