feat(mac): surface update-ready state
This commit is contained in:
@@ -92,10 +92,12 @@ struct AboutSettings: View {
|
|||||||
guard let updater, !self.didLoadUpdaterState else { return }
|
guard let updater, !self.didLoadUpdaterState else { return }
|
||||||
// Keep Sparkle’s auto-check setting in sync with the persisted toggle.
|
// Keep Sparkle’s auto-check setting in sync with the persisted toggle.
|
||||||
updater.automaticallyChecksForUpdates = self.autoCheckEnabled
|
updater.automaticallyChecksForUpdates = self.autoCheckEnabled
|
||||||
|
updater.automaticallyDownloadsUpdates = self.autoCheckEnabled
|
||||||
self.didLoadUpdaterState = true
|
self.didLoadUpdaterState = true
|
||||||
}
|
}
|
||||||
.onChange(of: self.autoCheckEnabled) { _, newValue in
|
.onChange(of: self.autoCheckEnabled) { _, newValue in
|
||||||
self.updater?.automaticallyChecksForUpdates = newValue
|
self.updater?.automaticallyChecksForUpdates = newValue
|
||||||
|
self.updater?.automaticallyDownloadsUpdates = newValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import AppKit
|
|||||||
import Darwin
|
import Darwin
|
||||||
import Foundation
|
import Foundation
|
||||||
import MenuBarExtraAccess
|
import MenuBarExtraAccess
|
||||||
|
import Observation
|
||||||
import OSLog
|
import OSLog
|
||||||
import Security
|
import Security
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
@@ -306,27 +307,94 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
|||||||
@MainActor
|
@MainActor
|
||||||
protocol UpdaterProviding: AnyObject {
|
protocol UpdaterProviding: AnyObject {
|
||||||
var automaticallyChecksForUpdates: Bool { get set }
|
var automaticallyChecksForUpdates: Bool { get set }
|
||||||
|
var automaticallyDownloadsUpdates: Bool { get set }
|
||||||
var isAvailable: Bool { get }
|
var isAvailable: Bool { get }
|
||||||
|
var updateStatus: UpdateStatus { get }
|
||||||
func checkForUpdates(_ sender: Any?)
|
func checkForUpdates(_ sender: Any?)
|
||||||
}
|
}
|
||||||
|
|
||||||
// No-op updater used for debug/dev runs to suppress Sparkle dialogs.
|
// No-op updater used for debug/dev runs to suppress Sparkle dialogs.
|
||||||
final class DisabledUpdaterController: UpdaterProviding {
|
final class DisabledUpdaterController: UpdaterProviding {
|
||||||
var automaticallyChecksForUpdates: Bool = false
|
var automaticallyChecksForUpdates: Bool = false
|
||||||
|
var automaticallyDownloadsUpdates: Bool = false
|
||||||
let isAvailable: Bool = false
|
let isAvailable: Bool = false
|
||||||
|
let updateStatus = UpdateStatus()
|
||||||
func checkForUpdates(_: Any?) {}
|
func checkForUpdates(_: Any?) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
@Observable
|
||||||
|
final class UpdateStatus {
|
||||||
|
static let disabled = UpdateStatus()
|
||||||
|
var isUpdateReady: Bool
|
||||||
|
|
||||||
|
init(isUpdateReady: Bool = false) {
|
||||||
|
self.isUpdateReady = isUpdateReady
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#if canImport(Sparkle)
|
#if canImport(Sparkle)
|
||||||
import Sparkle
|
import Sparkle
|
||||||
|
|
||||||
extension SPUStandardUpdaterController: UpdaterProviding {
|
@MainActor
|
||||||
|
final class SparkleUpdaterController: NSObject, UpdaterProviding, SPUUpdaterDelegate {
|
||||||
|
private lazy var controller = SPUStandardUpdaterController(
|
||||||
|
startingUpdater: false,
|
||||||
|
updaterDelegate: self,
|
||||||
|
userDriverDelegate: nil)
|
||||||
|
let updateStatus = UpdateStatus()
|
||||||
|
|
||||||
|
init(savedAutoUpdate: Bool) {
|
||||||
|
super.init()
|
||||||
|
let updater = self.controller.updater
|
||||||
|
updater.automaticallyChecksForUpdates = savedAutoUpdate
|
||||||
|
updater.automaticallyDownloadsUpdates = savedAutoUpdate
|
||||||
|
self.controller.startUpdater()
|
||||||
|
}
|
||||||
|
|
||||||
var automaticallyChecksForUpdates: Bool {
|
var automaticallyChecksForUpdates: Bool {
|
||||||
get { self.updater.automaticallyChecksForUpdates }
|
get { self.controller.updater.automaticallyChecksForUpdates }
|
||||||
set { self.updater.automaticallyChecksForUpdates = newValue }
|
set { self.controller.updater.automaticallyChecksForUpdates = newValue }
|
||||||
|
}
|
||||||
|
|
||||||
|
var automaticallyDownloadsUpdates: Bool {
|
||||||
|
get { self.controller.updater.automaticallyDownloadsUpdates }
|
||||||
|
set { self.controller.updater.automaticallyDownloadsUpdates = newValue }
|
||||||
}
|
}
|
||||||
|
|
||||||
var isAvailable: Bool { true }
|
var isAvailable: Bool { true }
|
||||||
|
|
||||||
|
func checkForUpdates(_ sender: Any?) {
|
||||||
|
self.controller.checkForUpdates(sender)
|
||||||
|
}
|
||||||
|
|
||||||
|
func updater(_ updater: SPUUpdater, didDownloadUpdate item: SUAppcastItem) {
|
||||||
|
self.updateStatus.isUpdateReady = true
|
||||||
|
}
|
||||||
|
|
||||||
|
func updater(_ updater: SPUUpdater, failedToDownloadUpdate item: SUAppcastItem, error: Error) {
|
||||||
|
self.updateStatus.isUpdateReady = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func userDidCancelDownload(_ updater: SPUUpdater) {
|
||||||
|
self.updateStatus.isUpdateReady = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func updater(
|
||||||
|
_ updater: SPUUpdater,
|
||||||
|
userDidMakeChoice choice: SPUUserUpdateChoice,
|
||||||
|
forUpdate updateItem: SUAppcastItem,
|
||||||
|
state: SPUUserUpdateState
|
||||||
|
) {
|
||||||
|
switch choice {
|
||||||
|
case .install, .skip:
|
||||||
|
self.updateStatus.isUpdateReady = false
|
||||||
|
case .dismiss:
|
||||||
|
self.updateStatus.isUpdateReady = (state.stage == .downloaded)
|
||||||
|
@unknown default:
|
||||||
|
self.updateStatus.isUpdateReady = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func isDeveloperIDSigned(bundleURL: URL) -> Bool {
|
private func isDeveloperIDSigned(bundleURL: URL) -> Bool {
|
||||||
@@ -359,14 +427,7 @@ private func makeUpdaterController() -> UpdaterProviding {
|
|||||||
let autoUpdateKey = "autoUpdateEnabled"
|
let autoUpdateKey = "autoUpdateEnabled"
|
||||||
// Default to true; honor the user's last choice otherwise.
|
// Default to true; honor the user's last choice otherwise.
|
||||||
let savedAutoUpdate = (defaults.object(forKey: autoUpdateKey) as? Bool) ?? true
|
let savedAutoUpdate = (defaults.object(forKey: autoUpdateKey) as? Bool) ?? true
|
||||||
|
return SparkleUpdaterController(savedAutoUpdate: savedAutoUpdate)
|
||||||
let controller = SPUStandardUpdaterController(
|
|
||||||
startingUpdater: false,
|
|
||||||
updaterDelegate: nil,
|
|
||||||
userDriverDelegate: nil)
|
|
||||||
controller.updater.automaticallyChecksForUpdates = savedAutoUpdate
|
|
||||||
controller.startUpdater()
|
|
||||||
return controller
|
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
private func makeUpdaterController() -> UpdaterProviding {
|
private func makeUpdaterController() -> UpdaterProviding {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import SwiftUI
|
|||||||
struct MenuContent: View {
|
struct MenuContent: View {
|
||||||
@Bindable var state: AppState
|
@Bindable var state: AppState
|
||||||
let updater: UpdaterProviding?
|
let updater: UpdaterProviding?
|
||||||
|
@Bindable private var updateStatus: UpdateStatus
|
||||||
private let gatewayManager = GatewayProcessManager.shared
|
private let gatewayManager = GatewayProcessManager.shared
|
||||||
private let healthStore = HealthStore.shared
|
private let healthStore = HealthStore.shared
|
||||||
private let heartbeatStore = HeartbeatStore.shared
|
private let heartbeatStore = HeartbeatStore.shared
|
||||||
@@ -18,6 +19,12 @@ struct MenuContent: View {
|
|||||||
@State private var loadingMics = false
|
@State private var loadingMics = false
|
||||||
@State private var browserControlEnabled = true
|
@State private var browserControlEnabled = true
|
||||||
|
|
||||||
|
init(state: AppState, updater: UpdaterProviding?) {
|
||||||
|
self._state = Bindable(wrappedValue: state)
|
||||||
|
self.updater = updater
|
||||||
|
self._updateStatus = Bindable(wrappedValue: updater?.updateStatus ?? UpdateStatus.disabled)
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
Toggle(isOn: self.activeBinding) {
|
Toggle(isOn: self.activeBinding) {
|
||||||
@@ -86,8 +93,8 @@ struct MenuContent: View {
|
|||||||
.keyboardShortcut(",", modifiers: [.command])
|
.keyboardShortcut(",", modifiers: [.command])
|
||||||
self.debugMenu
|
self.debugMenu
|
||||||
Button("About Clawdis") { self.open(tab: .about) }
|
Button("About Clawdis") { self.open(tab: .about) }
|
||||||
if let updater, updater.isAvailable {
|
if let updater, updater.isAvailable, self.updateStatus.isUpdateReady {
|
||||||
Button("Check for Updates…") { updater.checkForUpdates(nil) }
|
Button("Update ready, restart now?") { updater.checkForUpdates(nil) }
|
||||||
}
|
}
|
||||||
Button("Quit") { NSApplication.shared.terminate(nil) }
|
Button("Quit") { NSApplication.shared.terminate(nil) }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user