diff --git a/English.lproj/MainMenu.xib b/English.lproj/MainMenu.xib index 5378e5b..8def9ed 100644 --- a/English.lproj/MainMenu.xib +++ b/English.lproj/MainMenu.xib @@ -771,9 +771,6 @@ RemoteHostsManager - - SUUpdater - @@ -1190,12 +1187,12 @@ 552 - - checkForUpdates: - + + checkForUpdatesMenuItem + - 606 + 623 @@ -1795,11 +1792,6 @@ - - 605 - - - 609 @@ -1897,7 +1889,6 @@ com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin - com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin com.apple.InterfaceBuilder.CocoaPlugin diff --git a/Gas Mask.xcodeproj/project.pbxproj b/Gas Mask.xcodeproj/project.pbxproj index a6ea577..30775f1 100644 --- a/Gas Mask.xcodeproj/project.pbxproj +++ b/Gas Mask.xcodeproj/project.pbxproj @@ -1502,7 +1502,7 @@ repositoryURL = "https://github.com/sparkle-project/Sparkle.git"; requirement = { kind = exactVersion; - version = 1.27.1; + version = 2.9.0; }; }; AA1B2C3D4E5F000300000001 /* XCRemoteSwiftPackageReference "MASShortcut" */ = { diff --git a/Gas Mask.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/Gas Mask.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 891922a..7d4ac8e 100644 --- a/Gas Mask.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/Gas Mask.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,12 +1,11 @@ { - "originHash" : "74784ff33407b9ca71d04ea3acb135980046aa512ad6ef909abb7da8127b1333", + "originHash" : "34d6a563af95a9a6489907cf183989fcd49db5e2b85aa1c4f84d6c2e053df114", "pins" : [ { "identity" : "masshortcut", "kind" : "remoteSourceControl", "location" : "https://github.com/shpakovski/MASShortcut", "state" : { - "branch" : "master", "revision" : "6f2603c6b6cc18f64a799e5d2c9d3bbc467c413a" } }, @@ -15,8 +14,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/sparkle-project/Sparkle.git", "state" : { - "revision" : "7918c1c8fc68baa37917eeaa67286b077ad5e393", - "version" : "1.27.1" + "revision" : "21d8df80440b1ca3b65fa82e40782f1e5a9e6ba2", + "version" : "2.9.0" } } ], diff --git a/Info.plist b/Info.plist index bee1ca8..85f039b 100644 --- a/Info.plist +++ b/Info.plist @@ -95,6 +95,8 @@ http://gmask.clockwise.ee/check_update/ SUPublicDSAKeyFile sparkle_dsa_pub.pem + SUPublicEDKey + REPLACE_WITH_EDDSA_PUBLIC_KEY SUScheduledCheckInterval 86400 UTExportedTypeDeclarations diff --git a/Source/ApplicationController.h b/Source/ApplicationController.h index 8aa3fa8..48d2f41 100644 --- a/Source/ApplicationController.h +++ b/Source/ApplicationController.h @@ -21,19 +21,25 @@ #import "HostsMainController.h" @class AboutBoxController; +@class SPUStandardUpdaterController; +@class SPUUpdater; @interface ApplicationController : NSObject { @private IBOutlet NSProgressIndicator *busyIndicator; IBOutlet HostsMainController *hostsController; + IBOutlet NSMenuItem *checkForUpdatesMenuItem; int busyThreads; BOOL shouldQuit; BOOL editorWindowOpened; AboutBoxController *aboutBoxController; + SPUStandardUpdaterController *_updaterController; } + (ApplicationController*)defaultInstance; +@property (nonatomic, readonly) SPUUpdater *updater; + - (IBAction)openPreferencesWindow:(id)sender; - (IBAction)displayAboutBox:(id)sender; - (IBAction)reportBugs:(id)sender; diff --git a/Source/ApplicationController.m b/Source/ApplicationController.m index b35f19d..fd830ea 100644 --- a/Source/ApplicationController.m +++ b/Source/ApplicationController.m @@ -27,6 +27,7 @@ #import "LocalHostsController.h" #import "RemoteHostsController.h" #import "NotificationHelper.h" +#import @interface ApplicationController () { @@ -64,18 +65,31 @@ - (id)init if (self = [super init]) { busyThreads = 0; shouldQuit = YES; - + + BOOL isTesting = NSClassFromString(@"XCTestCase") != nil; + if (!isTesting) { + _updaterController = [[SPUStandardUpdaterController alloc] + initWithStartingUpdater:YES + updaterDelegate:nil + userDriverDelegate:nil]; + } + NSNotificationCenter *nc = [NSNotificationCenter defaultCenter]; [nc addObserver:self selector:@selector(activatePreviousFile:) name:ActivatePreviousFileNotification object:nil]; [nc addObserver:self selector:@selector(activateNextFile:) name:ActivateNextFileNotification object:nil]; [nc addObserver:self selector:@selector(notifyOfFileRestored:) name:RestoredHostsFileNotification object:nil]; - + sharedInstance = self; return self; } return sharedInstance; } +- (SPUUpdater *)updater +{ + return _updaterController.updater; +} + -(IBAction)openPreferencesWindow:(id)sender { [self showApplicationInDock]; @@ -179,6 +193,9 @@ - (void)applicationWillFinishLaunching:(NSNotification *)aNotification { (void)[GlobalShortcuts shared]; // Register global hotkeys + checkForUpdatesMenuItem.target = _updaterController; + checkForUpdatesMenuItem.action = @selector(checkForUpdates:); + [NSApp setServicesProvider:self]; [self initStructure]; diff --git a/Source/Swift/GasMask-Bridging-Header.h b/Source/Swift/GasMask-Bridging-Header.h index 2912fe5..a98982a 100644 --- a/Source/Swift/GasMask-Bridging-Header.h +++ b/Source/Swift/GasMask-Bridging-Header.h @@ -14,3 +14,4 @@ #import "ListController.h" #import "Preferences+Remote.h" #import "LoginItem.h" +#import "ApplicationController.h" diff --git a/Source/Swift/PreferencesView.swift b/Source/Swift/PreferencesView.swift index c221c6e..77f4586 100644 --- a/Source/Swift/PreferencesView.swift +++ b/Source/Swift/PreferencesView.swift @@ -108,7 +108,7 @@ struct UpdateTab: View { Button("Check Now") { sparkleObserver.checkForUpdates() } - .disabled(!sparkleObserver.automaticChecksEnabled) + .disabled(!sparkleObserver.canCheckForUpdates) } .padding(20) } diff --git a/Source/Swift/SparkleObserver.swift b/Source/Swift/SparkleObserver.swift index 66ee4c1..4dbd203 100644 --- a/Source/Swift/SparkleObserver.swift +++ b/Source/Swift/SparkleObserver.swift @@ -1,14 +1,14 @@ +import Combine import Foundation import Sparkle -/// Wraps `SUUpdater.shared()` as an `ObservableObject` for SwiftUI. -/// -/// Replaces the XIB-instantiated `SUUpdater` object and `UpdateDateTransformer`. +/// Wraps `SPUUpdater` as an `ObservableObject` for SwiftUI. final class SparkleObserver: ObservableObject { @Published var lastCheckDate: Date? @Published var automaticChecksEnabled: Bool + @Published var canCheckForUpdates = false - private var dateObservation: NSKeyValueObservation? + private let updater: SPUUpdater? private static let dateFormatter: DateFormatter = { let formatter = DateFormatter() @@ -25,30 +25,40 @@ final class SparkleObserver: ObservableObject { return "Last Checked: \(Self.dateFormatter.string(from: date))" } - init() { - guard let updater = SUUpdater.shared() else { - self.lastCheckDate = nil - self.automaticChecksEnabled = false - return - } - self.lastCheckDate = updater.lastUpdateCheckDate - self.automaticChecksEnabled = updater.automaticallyChecksForUpdates - - // Observe lastUpdateCheckDate via KVO - dateObservation = updater.observe(\.lastUpdateCheckDate, options: [.new]) { [weak self] _, change in - DispatchQueue.main.async { - self?.lastCheckDate = change.newValue ?? nil - } - } + convenience init() { + self.init(updater: ApplicationController.defaultInstance()?.updater) + } + + init(updater: SPUUpdater?) { + self.updater = updater + self.lastCheckDate = updater?.lastUpdateCheckDate + self.automaticChecksEnabled = updater?.automaticallyChecksForUpdates ?? false + + guard let updater else { return } + + updater.publisher(for: \.canCheckForUpdates) + .receive(on: DispatchQueue.main) + .assign(to: &$canCheckForUpdates) + + updater.publisher(for: \.automaticallyChecksForUpdates) + .receive(on: DispatchQueue.main) + .assign(to: &$automaticChecksEnabled) + + updater.publisher(for: \.lastUpdateCheckDate) + .receive(on: DispatchQueue.main) + .assign(to: &$lastCheckDate) } - /// Writes only to UserDefaults — SUUpdater observes this key via its own KVO. func setAutomaticChecks(_ enabled: Bool) { - automaticChecksEnabled = enabled - UserDefaults.standard.set(enabled, forKey: "SUEnableAutomaticChecks") + if let updater { + updater.automaticallyChecksForUpdates = enabled + } else { + automaticChecksEnabled = enabled + } } func checkForUpdates() { - SUUpdater.shared()?.checkForUpdates(nil) + guard let updater, updater.canCheckForUpdates else { return } + updater.checkForUpdates() } } diff --git a/Tests/GasMaskTests/SparkleObserverTests.swift b/Tests/GasMaskTests/SparkleObserverTests.swift index fd44774..a6c707d 100644 --- a/Tests/GasMaskTests/SparkleObserverTests.swift +++ b/Tests/GasMaskTests/SparkleObserverTests.swift @@ -4,13 +4,20 @@ import XCTest final class SparkleObserverTests: XCTestCase { func testLastCheckDateFormatted_nil_returnsNever() { - let observer = SparkleObserver() + let observer = SparkleObserver(updater: nil) observer.lastCheckDate = nil XCTAssertEqual(observer.lastCheckDateFormatted, "Last Checked: Never") } + func testNilUpdater_defaultState() { + let observer = SparkleObserver(updater: nil) + XCTAssertNil(observer.lastCheckDate) + XCTAssertFalse(observer.automaticChecksEnabled) + XCTAssertFalse(observer.canCheckForUpdates) + } + func testLastCheckDateFormatted_date_returnsFormattedString() { - let observer = SparkleObserver() + let observer = SparkleObserver(updater: nil) // Use a fixed date: 2026-02-28 15:45:00 UTC var components = DateComponents() components.year = 2026