macOS: Make a lot of things more robust

This commit is contained in:
Mitchell Hashimoto
2025-10-10 07:11:22 -07:00
parent ba8eae027e
commit 6993947a3a
6 changed files with 34 additions and 15 deletions

View File

@@ -998,8 +998,8 @@ class AppDelegate: NSObject,
} }
@IBAction func checkForUpdates(_ sender: Any?) { @IBAction func checkForUpdates(_ sender: Any?) {
updateController.checkForUpdates() //updateController.checkForUpdates()
//UpdateSimulator.permissionRequest.simulate(with: updateViewModel) UpdateSimulator.happyPath.simulate(with: updateViewModel)
} }

View File

@@ -17,14 +17,14 @@ struct UpdateBadge: View {
switch model.state { switch model.state {
case .downloading(let download): case .downloading(let download):
if let expectedLength = download.expectedLength, expectedLength > 0 { 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) ProgressRingView(progress: progress)
} else { } else {
Image(systemName: "arrow.down.circle") Image(systemName: "arrow.down.circle")
} }
case .extracting(let extracting): case .extracting(let extracting):
ProgressRingView(progress: extracting.progress) ProgressRingView(progress: min(1, max(0, extracting.progress)))
case .checking, .installing: case .checking, .installing:
if let iconName = model.iconName { if let iconName = model.iconName {

View File

@@ -32,9 +32,22 @@ class UpdateController {
/// Start the updater. /// Start the updater.
/// ///
/// This must be called before the updater can check for updates. If starting fails, /// 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() { 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. /// Check for updates.

View File

@@ -86,9 +86,10 @@ class UpdateDriver: NSObject, SPUUserDriver {
acknowledgement: @escaping () -> Void) { acknowledgement: @escaping () -> Void) {
viewModel.state = .error(.init( viewModel.state = .error(.init(
error: error, error: error,
retry: { [weak viewModel] in retry: { [weak self, weak viewModel] in
viewModel?.state = .idle 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 } guard let delegate = NSApp.delegate as? AppDelegate else { return }
delegate.checkForUpdates(self) delegate.checkForUpdates(self)
} }

View File

@@ -8,6 +8,9 @@ struct UpdatePill: View {
/// Whether the update popover is currently visible /// Whether the update popover is currently visible
@State private var showPopover = false @State private var showPopover = false
/// Task for auto-dismissing the "No Updates" state
@State private var resetTask: Task<Void, Never>?
/// The font used for the pill text /// The font used for the pill text
private let textFont = NSFont.systemFont(ofSize: 11, weight: .medium) private let textFont = NSFont.systemFont(ofSize: 11, weight: .medium)
@@ -19,13 +22,15 @@ struct UpdatePill: View {
} }
.transition(.opacity.combined(with: .scale(scale: 0.95))) .transition(.opacity.combined(with: .scale(scale: 0.95)))
.onChange(of: model.state) { newState in .onChange(of: model.state) { newState in
resetTask?.cancel()
if case .notFound = newState { if case .notFound = newState {
Task { resetTask = Task { [weak model] in
try? await Task.sleep(for: .seconds(5)) try? await Task.sleep(for: .seconds(5))
if case .notFound = model.state { guard !Task.isCancelled, case .notFound? = model?.state else { return }
model.state = .idle model?.state = .idle
}
} }
} else {
resetTask = nil
} }
} }
} }

View File

@@ -228,7 +228,7 @@ fileprivate struct DownloadingView: View {
.font(.system(size: 13, weight: .semibold)) .font(.system(size: 13, weight: .semibold))
if let expectedLength = download.expectedLength, expectedLength > 0 { 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) { VStack(alignment: .leading, spacing: 6) {
ProgressView(value: progress) ProgressView(value: progress)
Text(String(format: "%.0f%%", progress * 100)) Text(String(format: "%.0f%%", progress * 100))
@@ -264,8 +264,8 @@ fileprivate struct ExtractingView: View {
.font(.system(size: 13, weight: .semibold)) .font(.system(size: 13, weight: .semibold))
VStack(alignment: .leading, spacing: 6) { VStack(alignment: .leading, spacing: 6) {
ProgressView(value: extracting.progress, total: 1.0) ProgressView(value: min(1, max(0, extracting.progress)), total: 1.0)
Text(String(format: "%.0f%%", extracting.progress * 100)) Text(String(format: "%.0f%%", min(1, max(0, extracting.progress)) * 100))
.font(.system(size: 11)) .font(.system(size: 11))
.foregroundColor(.secondary) .foregroundColor(.secondary)
} }