import SwiftUI import Sparkle // Sparkle's "gentle scheduled update reminders": a background check that finds // an update must never pop the update dialog over someone's conversation (the // stock driver's behaviour — the exact interruption Rogue Amoeba's apps moved // away from). Instead the find is published here and the rail paints a quiet // inline indicator; clicking it re-runs the check as USER-initiated, which // shows Sparkle's standard flow at a moment the member chose. The app-menu // "Check for Updates…" is user-initiated already and unchanged. final class GentleUpdateReminder: NSObject, ObservableObject, SPUStandardUserDriverDelegate { // The version string the rail shows; nil = no indicator. Delegate // callbacks arrive on the main queue, so plain @Published is safe. @Published var availableVersion: String? // Set right after the controller is built (AppDelegate) — the click's // route back into Sparkle. Weak: the controller owns the updater. weak var updater: SPUUpdater? var supportsGentleScheduledUpdateReminders: Bool { true } // Never let a scheduled check take the window — even the "immediate // focus" case (an update found moments after launch) waits for a click. func standardUserDriverShouldHandleShowingScheduledUpdate( _ update: SUAppcastItem, andInImmediateFocus immediateFocus: Bool) -> Bool { false } func standardUserDriverWillHandleShowingUpdate( _ handleShowingUpdate: Bool, forUpdate update: SUAppcastItem, state: SPUUserUpdateState) { guard !state.userInitiated else { return } availableVersion = update.displayVersionString } // The member engaged with Sparkle's own UI (via our indicator or the // menu) — the reminder has done its job either way. func standardUserDriverDidReceiveUserAttention(forUpdate update: SUAppcastItem) { availableVersion = nil } func standardUserDriverWillFinishUpdateSession() { availableVersion = nil } // The indicator's click: a user-initiated check re-finds the update and // presents the standard flow. func install() { updater?.checkForUpdates() } } // Sparkle's own documented SwiftUI idiom for a menu item that's disabled while // a check is already in flight (canCheckForUpdates), rather than a plain // always-enabled button. final class CheckForUpdatesViewModel: ObservableObject { @Published var canCheckForUpdates = false init(updater: SPUUpdater) { updater.publisher(for: \.canCheckForUpdates).assign(to: &$canCheckForUpdates) } } struct CheckForUpdatesView: View { @ObservedObject private var model: CheckForUpdatesViewModel private let updater: SPUUpdater init(updater: SPUUpdater) { self.updater = updater self.model = CheckForUpdatesViewModel(updater: updater) } var body: some View { Button("Check for Updates…", action: updater.checkForUpdates) .disabled(!model.canCheckForUpdates) } } // The Settings window's General pane: a single on/off switch for background // updates (rather than separate "check" and "download" toggles) — "Check for // Updates…" in the app menu always works for a manual kick-off regardless of // this setting. // The Updates pane of Settings. (Formerly misnamed GeneralSettingsView — the // General pane is MacSettingsView.) struct UpdatesSettingsView: View { let updater: SPUUpdater @State private var automaticUpdates: Bool init(updater: SPUUpdater) { self.updater = updater _automaticUpdates = State(initialValue: updater.automaticallyDownloadsUpdates) } var body: some View { // A bare checkbox + clickable label, per Mac preference conventions — // a grouped-form box around a single control is chrome with nothing // to group. (The General pane keeps .grouped for now.) The TabView in // LChatMacApp owns the Settings window's one fixed frame, so no size // is pinned here. Toggle("Automatically download and install updates", isOn: $automaticUpdates) .toggleStyle(.checkbox) // Settings-window convention — see MacSettingsView .onChange(of: automaticUpdates) { _, enabled in // Downloading automatically requires checking automatically; // turning the checkbox off falls back to fully manual // (menu-triggered) checks only. updater.automaticallyChecksForUpdates = enabled updater.automaticallyDownloadsUpdates = enabled } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading) .padding(20) } }