diff --git a/macos/Sources/App/macOS/AppDelegate.swift b/macos/Sources/App/macOS/AppDelegate.swift index 216373e7e..cf717993a 100644 --- a/macos/Sources/App/macOS/AppDelegate.swift +++ b/macos/Sources/App/macOS/AppDelegate.swift @@ -998,8 +998,8 @@ class AppDelegate: NSObject, } @IBAction func checkForUpdates(_ sender: Any?) { - updateController.checkForUpdates() - //UpdateSimulator.permissionRequest.simulate(with: updateViewModel) + //updateController.checkForUpdates() + UpdateSimulator.happyPath.simulate(with: updateViewModel) } diff --git a/macos/Sources/Features/Update/UpdateBadge.swift b/macos/Sources/Features/Update/UpdateBadge.swift index fd1eb3498..afd0849be 100644 --- a/macos/Sources/Features/Update/UpdateBadge.swift +++ b/macos/Sources/Features/Update/UpdateBadge.swift @@ -17,14 +17,14 @@ struct UpdateBadge: View { switch model.state { case .downloading(let download): if let expectedLength = download.expectedLength, expectedLength > 0 { - let progress = Double(download.progress) / Double(expectedLength) + let progress = min(1, max(0, Double(download.progress) / Double(expectedLength))) ProgressRingView(progress: progress) } else { Image(systemName: "arrow.down.circle") } case .extracting(let extracting): - ProgressRingView(progress: extracting.progress) + ProgressRingView(progress: min(1, max(0, extracting.progress))) case .checking, .installing: if let iconName = model.iconName { diff --git a/macos/Sources/Features/Update/UpdateController.swift b/macos/Sources/Features/Update/UpdateController.swift index 8dc24698b..446b82ebc 100644 --- a/macos/Sources/Features/Update/UpdateController.swift +++ b/macos/Sources/Features/Update/UpdateController.swift @@ -32,9 +32,22 @@ class UpdateController { /// Start the updater. /// /// This must be called before the updater can check for updates. If starting fails, - /// an error alert will be shown after a short delay. + /// the error will be shown to the user. func startUpdater() { - try? updater.start() + do { + try updater.start() + } catch { + userDriver.viewModel.state = .error(.init( + error: error, + retry: { [weak self] in + self?.userDriver.viewModel.state = .idle + self?.startUpdater() + }, + dismiss: { [weak self] in + self?.userDriver.viewModel.state = .idle + } + )) + } } /// Check for updates. diff --git a/macos/Sources/Features/Update/UpdateDriver.swift b/macos/Sources/Features/Update/UpdateDriver.swift index cd1d051e2..9196d9ad9 100644 --- a/macos/Sources/Features/Update/UpdateDriver.swift +++ b/macos/Sources/Features/Update/UpdateDriver.swift @@ -86,9 +86,10 @@ class UpdateDriver: NSObject, SPUUserDriver { acknowledgement: @escaping () -> Void) { viewModel.state = .error(.init( error: error, - retry: { [weak viewModel] in + retry: { [weak self, weak viewModel] in viewModel?.state = .idle - DispatchQueue.main.async { + DispatchQueue.main.async { [weak self] in + guard let self else { return } guard let delegate = NSApp.delegate as? AppDelegate else { return } delegate.checkForUpdates(self) } diff --git a/macos/Sources/Features/Update/UpdatePill.swift b/macos/Sources/Features/Update/UpdatePill.swift index b975e81c9..3b48ac218 100644 --- a/macos/Sources/Features/Update/UpdatePill.swift +++ b/macos/Sources/Features/Update/UpdatePill.swift @@ -8,6 +8,9 @@ struct UpdatePill: View { /// Whether the update popover is currently visible @State private var showPopover = false + /// Task for auto-dismissing the "No Updates" state + @State private var resetTask: Task? + /// The font used for the pill text private let textFont = NSFont.systemFont(ofSize: 11, weight: .medium) @@ -19,13 +22,15 @@ struct UpdatePill: View { } .transition(.opacity.combined(with: .scale(scale: 0.95))) .onChange(of: model.state) { newState in + resetTask?.cancel() if case .notFound = newState { - Task { + resetTask = Task { [weak model] in try? await Task.sleep(for: .seconds(5)) - if case .notFound = model.state { - model.state = .idle - } + guard !Task.isCancelled, case .notFound? = model?.state else { return } + model?.state = .idle } + } else { + resetTask = nil } } } diff --git a/macos/Sources/Features/Update/UpdatePopoverView.swift b/macos/Sources/Features/Update/UpdatePopoverView.swift index a73116ca0..7634d27de 100644 --- a/macos/Sources/Features/Update/UpdatePopoverView.swift +++ b/macos/Sources/Features/Update/UpdatePopoverView.swift @@ -228,7 +228,7 @@ fileprivate struct DownloadingView: View { .font(.system(size: 13, weight: .semibold)) if let expectedLength = download.expectedLength, expectedLength > 0 { - let progress = Double(download.progress) / Double(expectedLength) + let progress = min(1, max(0, Double(download.progress) / Double(expectedLength))) VStack(alignment: .leading, spacing: 6) { ProgressView(value: progress) Text(String(format: "%.0f%%", progress * 100)) @@ -264,8 +264,8 @@ fileprivate struct ExtractingView: View { .font(.system(size: 13, weight: .semibold)) VStack(alignment: .leading, spacing: 6) { - ProgressView(value: extracting.progress, total: 1.0) - Text(String(format: "%.0f%%", extracting.progress * 100)) + ProgressView(value: min(1, max(0, extracting.progress)), total: 1.0) + Text(String(format: "%.0f%%", min(1, max(0, extracting.progress)) * 100)) .font(.system(size: 11)) .foregroundColor(.secondary) }