feat: inline Sparkle update checks to avoid unnecessary modals

This commit is contained in:
nimbold
2026-06-06 15:22:58 +03:30
parent b1802c718a
commit 890b6935b9
2 changed files with 68 additions and 10 deletions
+48 -9
View File
@@ -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)
@@ -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"))