diff --git a/Xcodes/Backend/AppState+Install.swift b/Xcodes/Backend/AppState+Install.swift index d5392636..416d38e6 100644 --- a/Xcodes/Backend/AppState+Install.swift +++ b/Xcodes/Backend/AppState+Install.swift @@ -44,6 +44,8 @@ extension AppState { Logger.appState.info("Using \(downloader) downloader") + setupDockProgress() + return validateSession() .flatMap { _ in self.getXcodeArchive(installationType, downloader: downloader) @@ -52,6 +54,8 @@ extension AppState { self.installArchivedXcode(xcode, at: url) } .catch { error -> AnyPublisher in + self.resetDockProgressTracking() + switch error { case InstallationError.damagedXIP(let damagedXIPURL): guard attemptNumber < 1 else { return Fail(error: error).eraseToAnyPublisher() } @@ -100,6 +104,7 @@ extension AppState { self.downloadOrUseExistingArchive(for: availableXcode, downloader: downloader, progressChanged: { [unowned self] progress in DispatchQueue.main.async { self.setInstallationStep(of: availableXcode.version, to: .downloading(progress: progress)) + self.overallProgress.addChild(progress, withPendingUnitCount: AppState.totalProgressUnits - AppState.unxipProgressWeight) } }) .map { return (availableXcode, $0) } @@ -152,7 +157,7 @@ extension AppState { cookies ) progressChanged(progress) - updateDockIcon(withProgress: progress) + return publisher .map { _ in destination.url } .eraseToAnyPublisher() @@ -162,12 +167,12 @@ extension AppState { let resumeDataPath = Path.xcodesApplicationSupport/"Xcode-\(availableXcode.version).resumedata" let persistedResumeData = Current.files.contents(atPath: resumeDataPath.string) - return attemptResumableTask(maximumRetryCount: 3) { [weak self] resumeData -> AnyPublisher in + return attemptResumableTask(maximumRetryCount: 3) { resumeData -> AnyPublisher in let (progress, publisher) = Current.network.downloadTask(with: availableXcode.url, to: destination.url, resumingWith: resumeData ?? persistedResumeData) progressChanged(progress) - self?.updateDockIcon(withProgress: progress) + return publisher .map { $0.saveLocation } .eraseToAnyPublisher() @@ -177,13 +182,11 @@ extension AppState { }) .eraseToAnyPublisher() } - - private func updateDockIcon(withProgress progress: Progress) { - DockProgress.style = .bar - DockProgress.progressInstance = progress - } public func installArchivedXcode(_ availableXcode: AvailableXcode, at archiveURL: URL) -> AnyPublisher { + unxipProgress.completedUnitCount = 0 + overallProgress.addChild(unxipProgress, withPendingUnitCount: AppState.unxipProgressWeight) + do { let destinationURL = Path.installDirectory.join("Xcode-\(availableXcode.version.descriptionWithoutBuildMetadata).app").url switch archiveURL.pathExtension { @@ -423,6 +426,9 @@ extension AppState { } self.presentedAlert = .privilegedHelper } + + unxipProgress.completedUnitCount = AppState.totalProgressUnits + resetDockProgressTracking() return helperInstallConsentSubject .flatMap { @@ -463,6 +469,24 @@ extension AppState { .eraseToAnyPublisher() } + // MARK: - Dock Progress Tracking + + private func setupDockProgress() { + DockProgress.progressInstance = nil + DockProgress.style = .bar + + let progress = Progress(totalUnitCount: AppState.totalProgressUnits) + progress.kind = .file + progress.fileOperationKind = .downloading + overallProgress = progress + + DockProgress.progressInstance = overallProgress + } + + func resetDockProgressTracking() { + DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete + } + // MARK: - func setInstallationStep(of version: Version, to step: InstallationStep) { diff --git a/Xcodes/Backend/AppState.swift b/Xcodes/Backend/AppState.swift index ca629f2e..49e22de2 100644 --- a/Xcodes/Backend/AppState.swift +++ b/Xcodes/Backend/AppState.swift @@ -107,6 +107,19 @@ class AppState: ObservableObject { private var selectPublisher: AnyCancellable? private var uninstallPublisher: AnyCancellable? private var autoInstallTimer: Timer? + + // MARK: - Dock Progress Tracking + + public static let totalProgressUnits = Int64(10) + public static let unxipProgressWeight = Int64(1) + var overallProgress = Progress() + var unxipProgress = { + let progress = Progress(totalUnitCount: totalProgressUnits) + progress.kind = .file + progress.fileOperationKind = .copying + return progress + }() + // MARK: - var dataSource: DataSource { @@ -491,8 +504,7 @@ class AppState: ObservableObject { // Cancel the publisher installationPublishers[id] = nil - // Remove dock icon progress indicator - DockProgress.progress = 1 // Only way to completely remove overlay with DockProgress is setting progress to complete + resetDockProgressTracking() // If the download is cancelled by the user, clean up the download files that aria2 creates. // This isn't done as part of the publisher with handleEvents(receiveCancel:) because it shouldn't happen when e.g. the app quits.