From 890b6935b9a4d88aa59900926d78b5d4e85707a7 Mon Sep 17 00:00:00 2001 From: nimbold <11913706+nimbold@users.noreply.github.com> Date: Sat, 6 Jun 2026 15:22:58 +0330 Subject: [PATCH] feat: inline Sparkle update checks to avoid unnecessary modals --- Sources/Firelink/FirelinkApp.swift | 57 ++++++++++++++++--- .../Firelink/Settings/AboutSettingsPane.swift | 21 ++++++- 2 files changed, 68 insertions(+), 10 deletions(-) diff --git a/Sources/Firelink/FirelinkApp.swift b/Sources/Firelink/FirelinkApp.swift index e765f80..f25961d 100644 --- a/Sources/Firelink/FirelinkApp.swift +++ b/Sources/Firelink/FirelinkApp.swift @@ -1,16 +1,58 @@ import SwiftUI import Sparkle -final class SparkleUpdater: ObservableObject { - let controller: SPUStandardUpdaterController - init(controller: SPUStandardUpdaterController) { - self.controller = controller +final class SparkleUpdater: NSObject, ObservableObject, SPUUpdaterDelegate { + private var _controller: SPUStandardUpdaterController? + var controller: SPUStandardUpdaterController { _controller! } + + @Published var isChecking = false + @Published var updateStatus: String? + @Published var foundUpdateItem: SUAppcastItem? + + override init() { + super.init() + self._controller = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: self, userDriverDelegate: nil) + } + + func checkForUpdates() { + isChecking = true + updateStatus = "Checking for updates..." + foundUpdateItem = nil + controller.updater.checkForUpdatesInBackground() + } + + func updater(_ updater: SPUUpdater, didFindValidUpdate item: SUAppcastItem) { + DispatchQueue.main.async { + self.isChecking = false + self.foundUpdateItem = item + self.updateStatus = "Update available: Version \(item.displayVersionString)" + } + } + + func updaterDidNotFindUpdate(_ updater: SPUUpdater, error: Error) { + DispatchQueue.main.async { + self.isChecking = false + let version = updater.hostBundle.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "" + let build = updater.hostBundle.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "" + self.updateStatus = "You're up to date! (Version \(version) (\(build)))" + } + } + + func updater(_ updater: SPUUpdater, didAbortWithError error: Error) { + DispatchQueue.main.async { + self.isChecking = false + let nsError = error as NSError + if nsError.domain == "SUSparkleErrorDomain" && nsError.code == 1002 { + // SUNoUpdateError, handled by updaterDidNotFindUpdate + } else { + self.updateStatus = "Update check failed: \(error.localizedDescription)" + } + } } } @main struct FirelinkApp: App { - private let updaterController: SPUStandardUpdaterController @StateObject private var sparkleUpdater: SparkleUpdater @StateObject private var settings: AppSettings @@ -22,10 +64,7 @@ struct FirelinkApp: App { private let extensionServer: LocalExtensionServer? init() { - // Initialize Sparkle updater - let updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) - self.updaterController = updaterController - self._sparkleUpdater = StateObject(wrappedValue: SparkleUpdater(controller: updaterController)) + self._sparkleUpdater = StateObject(wrappedValue: SparkleUpdater()) let settings = AppSettings() let controller = DownloadController(settings: settings) diff --git a/Sources/Firelink/Settings/AboutSettingsPane.swift b/Sources/Firelink/Settings/AboutSettingsPane.swift index 91237af..9abf968 100644 --- a/Sources/Firelink/Settings/AboutSettingsPane.swift +++ b/Sources/Firelink/Settings/AboutSettingsPane.swift @@ -41,12 +41,31 @@ struct AboutSettingsPane: View { Section("Updates") { VStack(alignment: .leading, spacing: 12) { + if let status = sparkleUpdater.updateStatus { + HStack { + if sparkleUpdater.isChecking { + ProgressView() + .controlSize(.small) + } else if sparkleUpdater.foundUpdateItem != nil { + Image(systemName: "exclamationmark.circle.fill") + .foregroundStyle(.orange) + } else { + Image(systemName: "checkmark.circle.fill") + .foregroundStyle(.green) + } + Text(status) + .font(.subheadline) + .foregroundStyle(.secondary) + } + } + HStack(spacing: 12) { Button { - sparkleUpdater.controller.checkForUpdates(nil) + sparkleUpdater.checkForUpdates() } label: { Label("Check for Updates", systemImage: "arrow.clockwise") } + .disabled(sparkleUpdater.isChecking) Button { NSWorkspace.shared.open(projectURL.appendingPathComponent("releases"))