@@ -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 {
0 commit comments