Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 4 additions & 13 deletions English.lproj/MainMenu.xib
Original file line number Diff line number Diff line change
Expand Up @@ -771,9 +771,6 @@
<object class="NSCustomObject" id="269446200">
<string key="NSClassName">RemoteHostsManager</string>
</object>
<object class="NSCustomObject" id="354390743">
<string key="NSClassName">SUUpdater</string>
</object>
</array>
<object class="IBObjectContainer" key="IBDocument.Objects">
<array key="connectionRecords">
Expand Down Expand Up @@ -1190,12 +1187,12 @@
<int key="connectionID">552</int>
</object>
<object class="IBConnectionRecord">
<object class="IBActionConnection" key="connection">
<string key="label">checkForUpdates:</string>
<reference key="source" ref="354390743"/>
<object class="IBOutletConnection" key="connection">
<string key="label">checkForUpdatesMenuItem</string>
<reference key="source" ref="82089617"/>
<reference key="destination" ref="263053057"/>
</object>
<int key="connectionID">606</int>
<int key="connectionID">623</int>
</object>
<object class="IBConnectionRecord">
<object class="IBBindingConnection" key="connection">
Expand Down Expand Up @@ -1795,11 +1792,6 @@
<reference key="object" ref="1023338729"/>
<reference key="parent" ref="110575045"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">605</int>
<reference key="object" ref="354390743"/>
<reference key="parent" ref="0"/>
</object>
<object class="IBObjectRecord">
<int key="objectID">609</int>
<reference key="object" ref="155258003"/>
Expand Down Expand Up @@ -1897,7 +1889,6 @@
<string key="602.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="603.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="604.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="605.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="609.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="611.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
<string key="614.IBPluginDependency">com.apple.InterfaceBuilder.CocoaPlugin</string>
Expand Down
2 changes: 1 addition & 1 deletion Gas Mask.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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" */ = {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@
<string>http://gmask.clockwise.ee/check_update/</string>
<key>SUPublicDSAKeyFile</key>
<string>sparkle_dsa_pub.pem</string>
<key>SUPublicEDKey</key>
<string>REPLACE_WITH_EDDSA_PUBLIC_KEY</string>
<key>SUScheduledCheckInterval</key>
<string>86400</string>
<key>UTExportedTypeDeclarations</key>
Expand Down
6 changes: 6 additions & 0 deletions Source/ApplicationController.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
21 changes: 19 additions & 2 deletions Source/ApplicationController.m
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
#import "LocalHostsController.h"
#import "RemoteHostsController.h"
#import "NotificationHelper.h"
#import <Sparkle/Sparkle.h>

@interface ApplicationController ()
{
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -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];
Expand Down
1 change: 1 addition & 0 deletions Source/Swift/GasMask-Bridging-Header.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@
#import "ListController.h"
#import "Preferences+Remote.h"
#import "LoginItem.h"
#import "ApplicationController.h"
2 changes: 1 addition & 1 deletion Source/Swift/PreferencesView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ struct UpdateTab: View {
Button("Check Now") {
sparkleObserver.checkForUpdates()
}
.disabled(!sparkleObserver.automaticChecksEnabled)
.disabled(!sparkleObserver.canCheckForUpdates)
}
.padding(20)
}
Expand Down
56 changes: 33 additions & 23 deletions Source/Swift/SparkleObserver.swift
Original file line number Diff line number Diff line change
@@ -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()
Expand All @@ -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()
}
}
11 changes: 9 additions & 2 deletions Tests/GasMaskTests/SparkleObserverTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down