Skip to content

Commit e6b4225

Browse files
committed
check for software updates
- automatically on launch by default, no less than 30 days from the last check, but can be disabled in Settings - on-demand from Mariani > Check for Updates... menu item
1 parent 26ff7f8 commit e6b4225

File tree

9 files changed

+289
-8
lines changed

9 files changed

+289
-8
lines changed

source/frontends/mariani/AppDelegate.mm

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,11 @@ - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
222222
#endif
223223

224224
[self.emulatorVC start];
225+
226+
if ([[UserDefaults sharedInstance] automaticallyCheckForUpdates]) {
227+
// parameter must be nil for a silent check
228+
[self checkForUpdates:nil];
229+
}
225230
}
226231

227232
- (void)applicationWillTerminate:(NSNotification *)aNotification {
@@ -409,6 +414,127 @@ - (IBAction)aboutLinkAction:(id)sender {
409414
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:@"https://github.com/sh95014/AppleWin"]];
410415
}
411416

417+
- (IBAction)checkForUpdates:(id)sender {
418+
NSLog(@"%s", __PRETTY_FUNCTION__);
419+
420+
if (sender == nil) {
421+
// not user-initiated
422+
NSDate *lastUpdateCheckDate = [[UserDefaults sharedInstance] lastUpdateCheckDate];
423+
if (lastUpdateCheckDate != nil && // never checked
424+
[lastUpdateCheckDate timeIntervalSinceNow] > -30 * 24 * 60 * 60) { // checked over 30 days ago
425+
NSLog(@"Skip, last check only %f seconds ago", -[lastUpdateCheckDate timeIntervalSinceNow]);
426+
return;
427+
}
428+
}
429+
430+
dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_BACKGROUND, 0), ^{
431+
enum {
432+
UP_TO_DATE, UPDATE_AVAILABLE, UNEXPECTED_RESPONSE, FETCH_ERROR,
433+
} updateAction = UNEXPECTED_RESPONSE;
434+
NSString *updateURLString = nil;
435+
NSURL *url = [NSURL URLWithString:@"https://api.github.com/repos/sh95014/AppleWin/releases/latest"];
436+
NSData *data = [NSData dataWithContentsOfURL:url];
437+
NSError *error = nil;
438+
id object = [NSJSONSerialization JSONObjectWithData:data options:0 error:&error];
439+
if (error == nil) {
440+
if ([object isKindOfClass:[NSDictionary class]]) {
441+
NSDictionary *results = object;
442+
if (![[results objectForKey:@"prerelease"] boolValue]) {
443+
// "prerelease": false
444+
if ((updateURLString = [[results objectForKey:@"html_url"] stringValue]) != nil) {
445+
// "html_url": "https://...",
446+
NSString *latestReleaseString = [[results objectForKey:@"name"] stringValue];
447+
// e.g., "name": "Mariani 1.5 (2)" => ["Mariani", "1.5", "(2)"]
448+
NSArray *latestReleaseParts = [latestReleaseString componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
449+
if (latestReleaseParts.count == 3) {
450+
// e.g., "1.5" => ["1", "5"]
451+
NSArray<NSString *> *latestVersionParts = [latestReleaseParts[1] componentsSeparatedByString:@"."];
452+
// e.g., "(2)" => "2"
453+
NSCharacterSet *parentheses = [NSCharacterSet characterSetWithCharactersInString:@"()"];
454+
NSString *latestBuildString = [latestReleaseParts[2] stringByTrimmingCharactersInSet:parentheses];
455+
456+
NSDictionary *infoDictionary = [[NSBundle mainBundle] infoDictionary];
457+
NSString *myVersionString = infoDictionary[@"CFBundleShortVersionString"];
458+
NSArray<NSString *> *myVersionParts = [myVersionString componentsSeparatedByString:@"."];
459+
NSString *myBuildString = infoDictionary[@"CFBundleVersion"];
460+
461+
NSInteger latestVersion =
462+
latestVersionParts[0].integerValue * 1000000 +
463+
latestVersionParts[1].integerValue * 1000 +
464+
latestBuildString.integerValue;
465+
NSInteger myVersion =
466+
myVersionParts[0].integerValue * 1000000 +
467+
myVersionParts[1].integerValue * 1000 +
468+
myBuildString.integerValue;
469+
470+
NSLog(@"Latest version: %ld.%ld (%ld)",
471+
latestVersionParts[0].integerValue,
472+
latestVersionParts[1].integerValue,
473+
latestBuildString.integerValue);
474+
updateAction = (latestVersion > myVersion) ? UPDATE_AVAILABLE : UP_TO_DATE;
475+
[[UserDefaults sharedInstance] setLastUpdateCheckDate:[NSDate now]];
476+
}
477+
else {
478+
NSLog(@"Unexpected version '%@'", latestReleaseString);
479+
}
480+
}
481+
else {
482+
NSLog(@"Unexpected html_url");
483+
}
484+
}
485+
}
486+
else {
487+
NSLog(@"Unexpected data format");
488+
}
489+
}
490+
else {
491+
updateAction = FETCH_ERROR;
492+
NSLog(@"Error: %@", error.localizedDescription);
493+
}
494+
if (updateAction == UPDATE_AVAILABLE || sender != nil) {
495+
// pop a dialog if updates are available, or if this was initiated
496+
// by the user from the menu.
497+
dispatch_async(dispatch_get_main_queue(), ^{
498+
NSAlert *alert = [[NSAlert alloc] init];
499+
500+
switch (updateAction) {
501+
case UP_TO_DATE:
502+
alert.messageText = NSLocalizedString(@"Up-to-Date", @"");
503+
alert.informativeText = NSLocalizedString(@"No newer version of this software is available.", @"");
504+
alert.alertStyle = NSAlertStyleInformational;
505+
alert.icon = [NSImage imageWithSystemSymbolName:@"hand.thumbsup" accessibilityDescription:@""];
506+
break;
507+
case UPDATE_AVAILABLE:
508+
alert.messageText = NSLocalizedString(@"Update Available", @"");
509+
alert.informativeText = NSLocalizedString(@"A newer version of this software is available.", @"");
510+
alert.alertStyle = NSAlertStyleInformational;
511+
alert.icon = [NSImage imageWithSystemSymbolName:@"square.and.arrow.down" accessibilityDescription:@""];
512+
[alert addButtonWithTitle:NSLocalizedString(@"Download…", @"")];
513+
[alert addButtonWithTitle:NSLocalizedString(@"Cancel", @"")];
514+
break;
515+
case UNEXPECTED_RESPONSE:
516+
alert.messageText = NSLocalizedString(@"Error", @"");
517+
alert.informativeText = NSLocalizedString(@"Unexpected server response", @"");
518+
alert.alertStyle = NSAlertStyleWarning;
519+
alert.icon = [NSImage imageWithSystemSymbolName:@"exclamationmark.triangle" accessibilityDescription:@""];
520+
break;
521+
case FETCH_ERROR:
522+
alert.messageText = NSLocalizedString(@"Error", @"");
523+
alert.informativeText = error.localizedDescription;
524+
alert.alertStyle = NSAlertStyleWarning;
525+
alert.icon = [NSImage imageWithSystemSymbolName:@"exclamationmark.triangle" accessibilityDescription:@""];
526+
break;
527+
}
528+
[alert beginSheetModalForWindow:self.window completionHandler:^(NSModalResponse returnCode) {
529+
if (returnCode == NSAlertFirstButtonReturn) {
530+
[[NSWorkspace sharedWorkspace] openURL:[NSURL URLWithString:updateURLString]];
531+
}
532+
}];
533+
});
534+
}
535+
});
536+
}
537+
412538
#pragma mark - App menu actions
413539

414540
- (IBAction)preferencesAction:(id)sender {

source/frontends/mariani/Base.lproj/MainMenu.xib

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,12 @@
5656
<action selector="preferencesAction:" target="Voe-Tx-rLC" id="1Hw-i2-VAj"/>
5757
</connections>
5858
</menuItem>
59+
<menuItem title="Check for Updates…" id="aV1-6h-w7i">
60+
<modifierMask key="keyEquivalentModifierMask"/>
61+
<connections>
62+
<action selector="checkForUpdates:" target="Voe-Tx-rLC" id="mJt-Z9-0nX"/>
63+
</connections>
64+
</menuItem>
5965
<menuItem isSeparatorItem="YES" id="wFC-TO-SCJ"/>
6066
<menuItem title="Services" id="NMo-om-nkz">
6167
<modifierMask key="keyEquivalentModifierMask"/>

source/frontends/mariani/Localizable.xcstrings

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@
123123
}
124124
}
125125
},
126+
"A newer version of this software is available." : {
127+
"localizations" : {
128+
"zh-Hant" : {
129+
"stringUnit" : {
130+
"state" : "translated",
131+
"value" : "已有比本軟體較新的版本"
132+
}
133+
}
134+
}
135+
},
126136
"About %@" : {
127137
"localizations" : {
128138
"zh-Hant" : {
@@ -441,6 +451,16 @@
441451
}
442452
}
443453
},
454+
"Download…" : {
455+
"localizations" : {
456+
"zh-Hant" : {
457+
"stringUnit" : {
458+
"state" : "translated",
459+
"value" : "前往下載⋯"
460+
}
461+
}
462+
}
463+
},
444464
"Echo (speech)" : {
445465
"localizations" : {
446466
"zh-Hant" : {
@@ -492,6 +512,16 @@
492512
}
493513
}
494514
},
515+
"Error" : {
516+
"localizations" : {
517+
"zh-Hant" : {
518+
"stringUnit" : {
519+
"state" : "translated",
520+
"value" : "錯誤"
521+
}
522+
}
523+
}
524+
},
495525
"Examine…" : {
496526
"comment" : "browse disk image",
497527
"localizations" : {
@@ -726,6 +756,16 @@
726756
}
727757
}
728758
},
759+
"No newer version of this software is available." : {
760+
"localizations" : {
761+
"zh-Hant" : {
762+
"stringUnit" : {
763+
"state" : "translated",
764+
"value" : "本軟體仍是最新版本"
765+
}
766+
}
767+
}
768+
},
729769
"None" : {
730770
"localizations" : {
731771
"zh-Hant" : {
@@ -1210,6 +1250,16 @@
12101250
}
12111251
}
12121252
},
1253+
"Unexpected server response" : {
1254+
"localizations" : {
1255+
"zh-Hant" : {
1256+
"stringUnit" : {
1257+
"state" : "translated",
1258+
"value" : "未預期的伺服器回應"
1259+
}
1260+
}
1261+
}
1262+
},
12131263
"Unknown" : {
12141264
"localizations" : {
12151265
"zh-Hant" : {
@@ -1240,6 +1290,26 @@
12401290
}
12411291
}
12421292
},
1293+
"Up-to-Date" : {
1294+
"localizations" : {
1295+
"zh-Hant" : {
1296+
"stringUnit" : {
1297+
"state" : "translated",
1298+
"value" : "仍是最新"
1299+
}
1300+
}
1301+
}
1302+
},
1303+
"Update Available" : {
1304+
"localizations" : {
1305+
"zh-Hant" : {
1306+
"stringUnit" : {
1307+
"state" : "translated",
1308+
"value" : "已有更新"
1309+
}
1310+
}
1311+
}
1312+
},
12431313
"Uthernet I (network)" : {
12441314
"localizations" : {
12451315
"zh-Hant" : {

source/frontends/mariani/UserDefaults.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ extern NSString *GameControllerNumericKeypad;
2121
@property (nonatomic) NSURL *screenshotsFolder;
2222
@property (nonatomic) BOOL mapDeleteKeyToLeftArrow;
2323
@property (nonatomic) BOOL takeScreenshotsBasedOnWindowSize;
24+
@property (nonatomic) BOOL automaticallyCheckForUpdates;
2425

2526
@property (nonatomic) NSString *gameController;
2627
@property (readonly) NSArray<NSString *> *joystickOptions;
@@ -31,6 +32,8 @@ extern NSString *GameControllerNumericKeypad;
3132

3233
@property (nonatomic) BOOL showStatusBar;
3334

35+
@property (nonatomic) NSDate *lastUpdateCheckDate;
36+
3437
@end
3538

3639
@interface GCController (Mariani)

source/frontends/mariani/UserDefaults.mm

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,13 @@
1313
#define SCREENSHOTS_FOLDER_KEY @"ScreenshotsFolder"
1414
#define MAP_DELETE_KEY_TO_LEFT_ARROW @"MapDeleteKeyToLeftArrow"
1515
#define TAKE_SCREENSHOTS_BASED_ON_WINDOW_SIZE @"TakeScreenshotsBasedOnWindowSize"
16+
#define AUTOMATICALLY_CHECK_FOR_UPDATES @"AutomaticallyCheckForUpdates"
1617
#define GAME_CONTROLLER_KEY @"GameController"
1718
#define JOYSTICK_MAPPING_KEY @"JoystickMapping"
1819
#define JOYSTICK_BUTTON0_MAPPING_KEY @"JoystickButton0Mapping"
1920
#define JOYSTICK_BUTTON1_MAPPING_KEY @"JoystickButton1Mapping"
2021
#define SHOW_STATUS_BAR @"ShowStatusBar"
22+
#define LAST_UPDATE_CHECK_DATE @"LastUpdateCheckDate"
2123

2224
NSString *GameControllerNone = @"GameControllerNone";
2325
NSString *GameControllerNumericKeypad = @"GameControllerNumericKeypad";
@@ -79,6 +81,15 @@ - (void)setTakeScreenshotsBasedOnWindowSize:(BOOL)takeScreenshotsBasedOnWindowSi
7981
[[NSUserDefaults standardUserDefaults] setBool:takeScreenshotsBasedOnWindowSize forKey:TAKE_SCREENSHOTS_BASED_ON_WINDOW_SIZE];
8082
}
8183

84+
- (BOOL)automaticallyCheckForUpdates {
85+
NSNumber *value = [[NSUserDefaults standardUserDefaults] valueForKey:AUTOMATICALLY_CHECK_FOR_UPDATES];
86+
return (value == nil) ? YES : [value boolValue];
87+
}
88+
89+
- (void)setAutomaticallyCheckForUpdates:(BOOL)automaticallyCheckForUpdates {
90+
[[NSUserDefaults standardUserDefaults] setBool:automaticallyCheckForUpdates forKey:AUTOMATICALLY_CHECK_FOR_UPDATES];
91+
}
92+
8293
- (NSString *)gameController {
8394
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
8495
NSString *fullName = [defaults stringForKey:GAME_CONTROLLER_KEY];
@@ -187,6 +198,14 @@ - (void)setShowStatusBar:(BOOL)showStatusBar {
187198
[[NSUserDefaults standardUserDefaults] setBool:showStatusBar forKey:SHOW_STATUS_BAR];
188199
}
189200

201+
- (NSDate *)lastUpdateCheckDate {
202+
return [[NSUserDefaults standardUserDefaults] valueForKey:LAST_UPDATE_CHECK_DATE];
203+
}
204+
205+
- (void)setLastUpdateCheckDate:(NSDate *)date {
206+
[[NSUserDefaults standardUserDefaults] setValue:date forKey:LAST_UPDATE_CHECK_DATE];
207+
}
208+
190209
@end
191210

192211
@implementation GCController (Mariani)

source/frontends/mariani/mul.lproj/MainMenu.xcstrings

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,24 @@
181181
}
182182
}
183183
},
184+
"aV1-6h-w7i.title" : {
185+
"comment" : "Class = \"NSMenuItem\"; title = \"Check for Updates…\"; ObjectID = \"aV1-6h-w7i\";",
186+
"extractionState" : "extracted_with_value",
187+
"localizations" : {
188+
"en" : {
189+
"stringUnit" : {
190+
"state" : "new",
191+
"value" : "Check for Updates…"
192+
}
193+
},
194+
"zh-Hant" : {
195+
"stringUnit" : {
196+
"state" : "translated",
197+
"value" : "查詢更新版本⋯"
198+
}
199+
}
200+
}
201+
},
184202
"AYu-sK-qS6.title" : {
185203
"comment" : "Class = \"NSMenu\"; title = \"Main Menu\"; ObjectID = \"AYu-sK-qS6\";",
186204
"extractionState" : "extracted_with_value",

0 commit comments

Comments
 (0)