├── .github └── FUNDING.yml ├── nonsource ├── Assets.xcassets │ ├── Contents.json │ └── AppIcon.appiconset │ │ ├── icon16x16@1x.png │ │ ├── icon16x16@2x.png │ │ ├── icon32x32@1x.png │ │ ├── icon32x32@2x.png │ │ ├── icon128x128@1x.png │ │ ├── icon128x128@2x.png │ │ ├── icon256x256@1x.png │ │ ├── icon256x256@2x.png │ │ ├── icon512x512@1x.png │ │ ├── icon512x512@2x.png │ │ └── Contents.json ├── StopTheNews.entitlements └── Info.plist ├── .gitignore ├── source ├── JJLicenseWindow.h ├── JJMainMenu.h ├── JJMainWindow.h ├── JJApplicationDelegate.h ├── main.m ├── NewsOpener.c ├── JJLicenseWindow.m ├── JJMainWindow.m ├── JJMainMenu.m └── JJApplicationDelegate.m ├── Debug.xcconfig ├── Release.xcconfig ├── LICENSE.txt ├── README.md ├── Shared.xcconfig └── StopTheNews.xcodeproj └── project.pbxproj /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | custom: https://www.paypal.me/JeffJohnsonWI 2 | -------------------------------------------------------------------------------- /nonsource/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "version" : 1, 4 | "author" : "xcode" 5 | } 6 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | DerivedData/ 3 | DEVELOPMENT_TEAM.xcconfig 4 | project.xcworkspace/ 5 | xcshareddata/ 6 | xcuserdata/ 7 | -------------------------------------------------------------------------------- /source/JJLicenseWindow.h: -------------------------------------------------------------------------------- 1 | @import Cocoa; 2 | 3 | @interface JJLicenseWindow:NSObject 4 | +(nullable NSWindow*)window; 5 | @end 6 | -------------------------------------------------------------------------------- /source/JJMainMenu.h: -------------------------------------------------------------------------------- 1 | #import "JJApplicationDelegate.h" 2 | 3 | @interface JJMainMenu:NSObject 4 | +(void)populateMainMenu; 5 | @end 6 | -------------------------------------------------------------------------------- /source/JJMainWindow.h: -------------------------------------------------------------------------------- 1 | #import "JJApplicationDelegate.h" 2 | 3 | @interface JJMainWindow:NSObject 4 | +(nonnull NSWindow*)window; 5 | @end 6 | -------------------------------------------------------------------------------- /nonsource/Assets.xcassets/AppIcon.appiconset/icon16x16@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapcat/StopTheNews/HEAD/nonsource/Assets.xcassets/AppIcon.appiconset/icon16x16@1x.png -------------------------------------------------------------------------------- /nonsource/Assets.xcassets/AppIcon.appiconset/icon16x16@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapcat/StopTheNews/HEAD/nonsource/Assets.xcassets/AppIcon.appiconset/icon16x16@2x.png -------------------------------------------------------------------------------- /nonsource/Assets.xcassets/AppIcon.appiconset/icon32x32@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapcat/StopTheNews/HEAD/nonsource/Assets.xcassets/AppIcon.appiconset/icon32x32@1x.png -------------------------------------------------------------------------------- /nonsource/Assets.xcassets/AppIcon.appiconset/icon32x32@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapcat/StopTheNews/HEAD/nonsource/Assets.xcassets/AppIcon.appiconset/icon32x32@2x.png -------------------------------------------------------------------------------- /nonsource/Assets.xcassets/AppIcon.appiconset/icon128x128@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapcat/StopTheNews/HEAD/nonsource/Assets.xcassets/AppIcon.appiconset/icon128x128@1x.png -------------------------------------------------------------------------------- /nonsource/Assets.xcassets/AppIcon.appiconset/icon128x128@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapcat/StopTheNews/HEAD/nonsource/Assets.xcassets/AppIcon.appiconset/icon128x128@2x.png -------------------------------------------------------------------------------- /nonsource/Assets.xcassets/AppIcon.appiconset/icon256x256@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapcat/StopTheNews/HEAD/nonsource/Assets.xcassets/AppIcon.appiconset/icon256x256@1x.png -------------------------------------------------------------------------------- /nonsource/Assets.xcassets/AppIcon.appiconset/icon256x256@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapcat/StopTheNews/HEAD/nonsource/Assets.xcassets/AppIcon.appiconset/icon256x256@2x.png -------------------------------------------------------------------------------- /nonsource/Assets.xcassets/AppIcon.appiconset/icon512x512@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapcat/StopTheNews/HEAD/nonsource/Assets.xcassets/AppIcon.appiconset/icon512x512@1x.png -------------------------------------------------------------------------------- /nonsource/Assets.xcassets/AppIcon.appiconset/icon512x512@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lapcat/StopTheNews/HEAD/nonsource/Assets.xcassets/AppIcon.appiconset/icon512x512@2x.png -------------------------------------------------------------------------------- /Debug.xcconfig: -------------------------------------------------------------------------------- 1 | DEBUG_INFORMATION_FORMAT = dwarf 2 | DEPLOYMENT_POSTPROCESSING = NO 3 | GCC_OPTIMIZATION_LEVEL = 0 4 | ONLY_ACTIVE_ARCH = YES 5 | STRIP_INSTALLED_PRODUCT = NO 6 | VALIDATE_PRODUCT = NO 7 | -------------------------------------------------------------------------------- /nonsource/StopTheNews.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /Release.xcconfig: -------------------------------------------------------------------------------- 1 | DEBUG_INFORMATION_FORMAT = dwarf-with-dsym 2 | DEPLOYMENT_POSTPROCESSING = YES 3 | GCC_OPTIMIZATION_LEVEL = s 4 | ONLY_ACTIVE_ARCH = NO 5 | STRIP_INSTALLED_PRODUCT = YES 6 | STRIP_STYLE = all 7 | VALIDATE_PRODUCT = YES 8 | -------------------------------------------------------------------------------- /source/JJApplicationDelegate.h: -------------------------------------------------------------------------------- 1 | @import Cocoa; 2 | @import UserNotifications; 3 | 4 | @interface JJApplicationDelegate:NSObject 5 | -(void)openLicense:(nullable id)sender; 6 | -(void)openMacAppStore:(nullable id)sender; 7 | -(void)openMainWindow:(nullable id)sender; 8 | -(void)openWebSite:(nullable id)sender; 9 | @end 10 | 11 | extern NSString*_Null_unspecified JJApplicationName; 12 | -------------------------------------------------------------------------------- /source/main.m: -------------------------------------------------------------------------------- 1 | #import "JJApplicationDelegate.h" 2 | 3 | int main(int argc, const char* argv[]) { 4 | @autoreleasepool { 5 | NSApplication* application = [NSApplication sharedApplication]; 6 | JJApplicationDelegate*NS_VALID_UNTIL_END_OF_SCOPE delegate = [[JJApplicationDelegate alloc] init]; 7 | [application setDelegate:delegate]; 8 | [application run]; 9 | [application setDelegate:nil]; 10 | } 11 | return EXIT_SUCCESS; 12 | } 13 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | 1. StopTheNews is Copyright © 2019 Jeff Johnson. All rights reserved. 2 | 3 | 2. You may make copies of the StopTheNews source code and redistribute copies of the source code. 4 | 5 | 3. All copies of the source code must include this license file unmodified. 6 | 7 | 4. You may modify the source code, but you must not modify or remove this license file. 8 | 9 | 5. You must not charge money for the unmodified or modified source code. 10 | 11 | 6. You may compile the unmodified or modified source code and run the compiled products yourself. 12 | 13 | 7. You must not redistribute the compiled products of the unmodified or modified source code without express written permission from Jeff Johnson. 14 | 15 | 8. The spirit of this license agreement is that Jeff Johnson provides StopTheNews to the public for free, and nobody is to make money from selling Jeff's work as if it were their own. -------------------------------------------------------------------------------- /nonsource/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "icon16x16@1x.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "icon16x16@2x.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "icon32x32@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "icon32x32@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "icon128x128@1x.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "icon128x128@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "icon256x256@1x.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "icon256x256@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "icon512x512@1x.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "icon512x512@2x.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } -------------------------------------------------------------------------------- /source/NewsOpener.c: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | int main(int argc, const char* argv[]) { 4 | if (argc != 2) { 5 | fprintf(stderr, "Usage: NewsOpener [url]\n"); 6 | return EXIT_FAILURE; 7 | } 8 | 9 | uint8_t buffer; 10 | ssize_t numberOfBytes; 11 | do { 12 | numberOfBytes = read(STDIN_FILENO, &buffer, 1); 13 | } while (numberOfBytes < 0 && errno == EINTR); 14 | if (numberOfBytes < 0) { 15 | perror("read"); 16 | return EXIT_FAILURE; 17 | } else if (numberOfBytes > 0) { 18 | fprintf(stderr, "Unexpected numberOfBytes: %li\n", numberOfBytes); 19 | return EXIT_FAILURE; 20 | } 21 | 22 | const char* arg = argv[1]; 23 | CFURLRef newsURL = CFURLCreateWithBytes(NULL, (const UInt8*)arg, (CFIndex)strlen(arg), kCFStringEncodingUTF8, NULL); 24 | if (newsURL == NULL) { 25 | fprintf(stderr, "Non-URL string: %s\n", arg); 26 | return EXIT_FAILURE; 27 | } 28 | CFMutableArrayRef itemURLs = CFArrayCreateMutable(NULL, 1, NULL); 29 | CFArrayAppendValue(itemURLs, newsURL); 30 | 31 | CFURLRef appURL = CFURLCreateWithFileSystemPath(NULL, CFSTR("/System/Applications/News.app"), kCFURLPOSIXPathStyle, true); 32 | if (appURL == NULL) { 33 | fprintf(stderr, "Could not create News app from path!\n"); 34 | CFRelease(itemURLs); 35 | CFRelease(newsURL); 36 | return EXIT_FAILURE; 37 | } 38 | 39 | LSLaunchURLSpec launchSpec; 40 | bzero(&launchSpec, sizeof(LSLaunchURLSpec)); 41 | launchSpec.appURL = appURL; 42 | launchSpec.itemURLs = itemURLs; 43 | OSStatus status = LSOpenFromURLSpec(&launchSpec, NULL); 44 | if (status != noErr) 45 | fprintf(stderr, "LSOpenFromURLSpec: %i\n", status); 46 | 47 | CFRelease(newsURL); 48 | return EXIT_SUCCESS; 49 | } 50 | -------------------------------------------------------------------------------- /nonsource/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AppIdentifierPrefix 6 | ZL6BUSYGB3. 7 | com.apple.developer.associated-domains 8 | 9 | CFBundleDevelopmentRegion 10 | $(DEVELOPMENT_LANGUAGE) 11 | CFBundleExecutable 12 | $(EXECUTABLE_NAME) 13 | CFBundleIdentifier 14 | $(PRODUCT_BUNDLE_IDENTIFIER) 15 | CFBundleInfoDictionaryVersion 16 | 6.0 17 | CFBundleName 18 | $(PRODUCT_NAME) 19 | CFBundlePackageType 20 | APPL 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Viewer 26 | CFBundleURLName 27 | com.apple.NewsCustomScheme 28 | CFBundleURLSchemes 29 | 30 | applenews 31 | applenewss 32 | 33 | LSHandlerRank 34 | Default 35 | LSIsAppleDefaultForScheme 36 | 37 | 38 | 39 | CFBundleShortVersionString 40 | $(STOPTHENEWS_SHORT_VERSION) 41 | CFBundleVersion 42 | $(STOPTHENEWS_VERSION) 43 | LSMinimumSystemVersion 44 | $(MACOSX_DEPLOYMENT_TARGET) 45 | NSHumanReadableCopyright 46 | © 2019 Jeff Johnson. All rights reserved. 47 | NSPrincipalClass 48 | NSApplication 49 | NSSupportsSuddenTermination 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /source/JJLicenseWindow.m: -------------------------------------------------------------------------------- 1 | #import "JJLicenseWindow.h" 2 | 3 | @implementation JJLicenseWindow 4 | 5 | +(nullable NSWindow*)window { 6 | NSURL* url = [[NSBundle mainBundle] URLForResource:@"LICENSE" withExtension:@"txt"]; 7 | if (url == nil) { 8 | NSLog(@"LICENSE.txt not found"); 9 | return nil; 10 | } 11 | NSError* error = nil; 12 | NSString* license = [NSString stringWithContentsOfURL:url encoding:NSUTF8StringEncoding error:&error]; 13 | if (license == nil) { 14 | NSLog(@"LICENSE.txt error: %@", error); 15 | return nil; 16 | } 17 | 18 | NSTextField* label = [NSTextField wrappingLabelWithString:license]; 19 | [label setTranslatesAutoresizingMaskIntoConstraints:NO]; 20 | 21 | NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; 22 | NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0.0, 0.0, 630.0, 100.0) styleMask:style backing:NSBackingStoreBuffered defer:YES]; 23 | [window setExcludedFromWindowsMenu:YES]; 24 | [window setReleasedWhenClosed:NO]; // Necessary under ARC to avoid a crash. 25 | [window setTabbingMode:NSWindowTabbingModeDisallowed]; 26 | [window setTitle:NSLocalizedString(@"License", nil)]; 27 | 28 | NSView* contentView = [window contentView]; 29 | [contentView addSubview:label]; 30 | [NSLayoutConstraint activateConstraints:@[ 31 | [[label topAnchor] constraintEqualToAnchor:[contentView topAnchor] constant:15.0], 32 | [[label bottomAnchor] constraintEqualToAnchor:[contentView bottomAnchor] constant:-15.0], 33 | [[label leadingAnchor] constraintEqualToAnchor:[contentView leadingAnchor] constant:15.0], 34 | [[label trailingAnchor] constraintEqualToAnchor:[contentView trailingAnchor] constant:-15.0], 35 | [[label widthAnchor] constraintEqualToConstant:600.0] 36 | ]]; 37 | 38 | [window makeKeyAndOrderFront:nil]; 39 | [window center]; // Wait until after makeKeyAndOrderFront so the window sizes properly first 40 | 41 | return window; 42 | } 43 | 44 | @end 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # StopTheNews 2 | 3 | StopTheNews is an app for macOS (10.14 Mojave or later) that automatically handles Apple News URLs instead of News app. If you allow Safari to open an Apple News URL in StopTheNews, the original article will then open in Safari instead of News app. StopTheNews also works with Safari Technology Preview, if that is your default web browser. 4 | 5 | StopTheNews version 3.0 and earlier also handled Mac App Store URLs instead of App Store app. On macOS 11 Big Sur and later, this is now handled by a separate app, [Stop The Mac App Store](https://github.com/lapcat/StopTheMacAppStore). 6 | 7 | ## Installing 8 | 9 | 1. On macOS Big Sur, download the [latest release](https://github.com/lapcat/StopTheNews/releases/latest). On macOS Catalina or Mojave, download [version 2.2](https://github.com/lapcat/StopTheNews/releases/tag/v2.2). 10 | 2. Unzip the downloaded `.zip` file. 11 | 3. Move `StopTheNews.app` to your Applications folder. 12 | 4. Open `StopTheNews.app`. 13 | 5. Quit `StopTheNews.app`. 14 | 15 | ## Uninstalling 16 | 17 | 1. Move `StopTheNews.app` to the Trash. 18 | 19 | ## Known Issues 20 | 21 | - Some Apple News pages don't provide the original article URL. The articles are designed to be displayed inline on `apple.news`. In these cases, StopTheNews offers the option to open the article in News app. 22 | 23 | ## Building 24 | 25 | Building StopTheNews from source requires Xcode 10 or later. 26 | 27 | Before building, you need to create a file named `DEVELOPMENT_TEAM.xcconfig` in the project folder (the same folder as `Shared.xcconfig`). This file is excluded from version control by the project's `.gitignore` file, and it's not referenced in the Xcode project either. The file specifies the build setting for your Development Team, which is needed by Xcode to code sign the app. The entire contents of the file should be of the following format: 28 | ``` 29 | DEVELOPMENT_TEAM = [Your TeamID] 30 | ``` 31 | 32 | ## Author 33 | 34 | [Jeff Johnson](https://lapcatsoftware.com/) 35 | 36 | To support the author, you can [PayPal.Me](https://www.paypal.me/JeffJohnsonWI) or buy the Safari extension StopTheMadness in the [Mac App Store](https://apps.apple.com/app/stopthemadness/id1376402589?mt=12). 37 | 38 | ## Copyright 39 | 40 | StopTheNews is Copyright © 2019 Jeff Johnson. All rights reserved. 41 | 42 | ## License 43 | 44 | See the [LICENSE.txt](LICENSE.txt) file for details. 45 | -------------------------------------------------------------------------------- /source/JJMainWindow.m: -------------------------------------------------------------------------------- 1 | #import "JJMainWindow.h" 2 | 3 | @implementation JJMainWindow 4 | 5 | +(nonnull NSWindow*)window { 6 | NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable; 7 | NSWindow* window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0.0, 0.0, 480.0, 300.0) styleMask:style backing:NSBackingStoreBuffered defer:YES]; 8 | [window setExcludedFromWindowsMenu:YES]; 9 | [window setReleasedWhenClosed:NO]; // Necessary under ARC to avoid a crash. 10 | [window setTabbingMode:NSWindowTabbingModeDisallowed]; 11 | [window setTitle:JJApplicationName]; 12 | NSView* contentView = [window contentView]; 13 | 14 | NSString* intro = NSLocalizedString(@"StopTheNews registers itself as the default handler for Apple News URLs instead of the News app.\n\nIf you allow Safari to open an Apple News URL in StopTheNews, the original article will then open in Safari (or Safari Technology Preview, if that is your default web browser).\n\nYou don't need to keep StopTheNews running. Safari will automatically launch StopTheNews, and StopTheNews will automatically terminate itself.\n\nStopTheNews is free and open source. To support the developer, please consider buying the Safari extension StopTheMadness in the Mac App Store. Thanks!", nil); 15 | NSTextField* label = [NSTextField wrappingLabelWithString:intro]; 16 | [label setTranslatesAutoresizingMaskIntoConstraints:NO]; 17 | [contentView addSubview:label]; 18 | 19 | NSButton* buyButton = [[NSButton alloc] init]; 20 | [buyButton setButtonType:NSButtonTypeMomentaryLight]; 21 | [buyButton setBezelStyle:NSBezelStyleRounded]; 22 | [buyButton setTitle:NSLocalizedString(@"Mac App Store", nil)]; 23 | [buyButton setAction:@selector(openMacAppStore:)]; 24 | [buyButton setTranslatesAutoresizingMaskIntoConstraints:NO]; 25 | [contentView addSubview:buyButton]; 26 | [window setDefaultButtonCell:[buyButton cell]]; 27 | [window setInitialFirstResponder:buyButton]; 28 | 29 | [NSLayoutConstraint activateConstraints:@[ 30 | [[label topAnchor] constraintEqualToAnchor:[contentView topAnchor] constant:15.0], 31 | [[label leadingAnchor] constraintEqualToAnchor:[contentView leadingAnchor] constant:15.0], 32 | [[label trailingAnchor] constraintEqualToAnchor:[contentView trailingAnchor] constant:-15.0], 33 | [[label widthAnchor] constraintEqualToConstant:450.0], 34 | [[buyButton topAnchor] constraintEqualToAnchor:[label bottomAnchor] constant:15.0], 35 | [[buyButton bottomAnchor] constraintEqualToAnchor:[contentView bottomAnchor] constant:-15.0], 36 | [[buyButton trailingAnchor] constraintEqualToAnchor:[contentView trailingAnchor] constant:-15.0] 37 | ]]; 38 | 39 | [window makeKeyAndOrderFront:nil]; 40 | [window center]; // Wait until after makeKeyAndOrderFront so the window sizes properly first 41 | 42 | return window; 43 | } 44 | 45 | @end 46 | -------------------------------------------------------------------------------- /Shared.xcconfig: -------------------------------------------------------------------------------- 1 | #include "DEVELOPMENT_TEAM.xcconfig" 2 | //Create the file DEVELOPMENT_TEAM.xcconfig in the project directory 3 | //with the following build setting: 4 | //DEVELOPMENT_TEAM = [Your TeamID] 5 | 6 | STOPTHENEWS_SHORT_VERSION = 4.2 7 | STOPTHENEWS_VERSION = 9004.2 8 | 9 | ALWAYS_SEARCH_USER_PATHS = NO 10 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon 11 | CLANG_ENABLE_MODULES = YES 12 | CLANG_ENABLE_OBJC_ARC = YES 13 | CLANG_MODULES_AUTOLINK = YES 14 | CODE_SIGN_ENTITLEMENTS = nonsource/StopTheNews.entitlements 15 | CODE_SIGN_IDENTITY = Mac Developer 16 | CODE_SIGN_STYLE = Automatic 17 | COMBINE_HIDPI_IMAGES = YES 18 | COPY_PHASE_STRIP = NO 19 | DEPLOYMENT_LOCATION = NO 20 | ENABLE_HARDENED_RUNTIME = YES 21 | ENABLE_STRICT_OBJC_MSGSEND = YES 22 | GCC_GENERATE_DEBUGGING_SYMBOLS = YES 23 | GCC_NO_COMMON_BLOCKS = YES 24 | GCC_SYMBOLS_PRIVATE_EXTERN = YES 25 | INFOPLIST_EXPAND_BUILD_SETTINGS = YES 26 | INFOPLIST_FILE = nonsource/Info.plist 27 | MACH_O_TYPE = mh_execute 28 | MACOSX_DEPLOYMENT_TARGET = 10.16 29 | PRODUCT_BUNDLE_IDENTIFIER = com.apple.news 30 | SDKROOT = macosx 31 | WRAPPER_EXTENSION = app 32 | 33 | //Warnings 34 | WARNING_CFLAGS = -Wall -Wextra -Wno-unused-parameter 35 | CLANG_WARN_ASSIGN_ENUM = YES 36 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES 37 | CLANG_WARN_BOOL_CONVERSION = YES 38 | CLANG_WARN_COMMA = YES 39 | CLANG_WARN_CONSTANT_CONVERSION = YES 40 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR 41 | CLANG_WARN_DOCUMENTATION_COMMENTS = YES 42 | CLANG_WARN_EMPTY_BODY = YES 43 | CLANG_WARN_ENUM_CONVERSION = YES 44 | CLANG_WARN_IMPLICIT_SIGN_CONVERSION = YES 45 | CLANG_WARN_INFINITE_RECURSION = YES 46 | CLANG_WARN_INT_CONVERSION = YES 47 | CLANG_WARN_OBJC_IMPLICIT_ATOMIC_PROPERTIES = YES 48 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = NO 49 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR 50 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES 51 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES 52 | CLANG_WARN_STRICT_PROTOTYPES = YES 53 | CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES 54 | CLANG_WARN_SUSPICIOUS_MOVE = YES 55 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 56 | CLANG_WARN_UNREACHABLE_CODE = YES_AGGRESSIVE 57 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 58 | GCC_WARN_ABOUT_DEPRECATED_FUNCTIONS = YES 59 | GCC_WARN_ABOUT_MISSING_FIELD_INITIALIZERS = YES 60 | GCC_WARN_ABOUT_MISSING_NEWLINE = YES 61 | GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES 62 | GCC_WARN_ABOUT_POINTER_SIGNEDNESS = YES 63 | GCC_WARN_ABOUT_RETURN_TYPE = YES 64 | GCC_WARN_ALLOW_INCOMPLETE_PROTOCOL = YES 65 | GCC_WARN_CHECK_SWITCH_STATEMENTS = YES 66 | GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES 67 | GCC_WARN_MISSING_PARENTHESES = YES 68 | GCC_WARN_SIGN_COMPARE = YES 69 | GCC_WARN_TYPECHECK_CALLS_TO_PRINTF = YES 70 | GCC_WARN_UNDECLARED_SELECTOR = YES 71 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE 72 | GCC_WARN_UNUSED_FUNCTION = YES 73 | GCC_WARN_UNUSED_LABEL = YES 74 | GCC_WARN_UNUSED_PARAMETER = NO 75 | GCC_WARN_UNUSED_VALUE = YES 76 | GCC_WARN_UNUSED_VARIABLE = YES 77 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES 78 | -------------------------------------------------------------------------------- /source/JJMainMenu.m: -------------------------------------------------------------------------------- 1 | #import "JJMainMenu.h" 2 | 3 | // Apparently these aren't declared anywhere 4 | @interface NSObject(JJMainMenu) 5 | -(void)redo:(nullable id)sender; 6 | -(void)undo:(nullable id)sender; 7 | @end 8 | 9 | @implementation JJMainMenu 10 | 11 | +(void)populateMainMenu { 12 | NSMenu* mainMenu = [[NSMenu alloc] initWithTitle:@"Main Menu"]; 13 | 14 | NSMenuItem* menuItem; 15 | NSMenu* submenu; 16 | 17 | // The titles of the menu items are for identification purposes only and shouldn't be localized. 18 | // The strings in the menu bar come from the submenu titles, 19 | // except for the application menu, whose title is ignored at runtime. 20 | menuItem = [mainMenu addItemWithTitle:@"Application" action:NULL keyEquivalent:@""]; 21 | submenu = [[NSMenu alloc] initWithTitle:@"Application"]; 22 | [self populateApplicationMenu:submenu]; 23 | [mainMenu setSubmenu:submenu forItem:menuItem]; 24 | 25 | menuItem = [mainMenu addItemWithTitle:@"Edit" action:NULL keyEquivalent:@""]; 26 | submenu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Edit", @"The Edit menu")]; 27 | [self populateEditMenu:submenu]; 28 | [mainMenu setSubmenu:submenu forItem:menuItem]; 29 | 30 | menuItem = [mainMenu addItemWithTitle:@"Window" action:NULL keyEquivalent:@""]; 31 | submenu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Window", @"The Window menu")]; 32 | [self populateWindowMenu:submenu]; 33 | [mainMenu setSubmenu:submenu forItem:menuItem]; 34 | [NSApp setWindowsMenu:submenu]; 35 | 36 | menuItem = [mainMenu addItemWithTitle:@"Help" action:NULL keyEquivalent:@""]; 37 | submenu = [[NSMenu alloc] initWithTitle:NSLocalizedString(@"Help", @"The Help menu")]; 38 | [self populateHelpMenu:submenu]; 39 | [mainMenu setSubmenu:submenu forItem:menuItem]; 40 | 41 | [NSApp setMainMenu:mainMenu]; 42 | } 43 | 44 | +(void)populateApplicationMenu:(NSMenu*)menu { 45 | NSMenuItem* menuItem; 46 | 47 | menuItem = [menu addItemWithTitle:[NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"About", nil), JJApplicationName] 48 | action:@selector(orderFrontStandardAboutPanel:) 49 | keyEquivalent:@""]; 50 | [menuItem setTarget:NSApp]; 51 | 52 | [menu addItem:[NSMenuItem separatorItem]]; 53 | 54 | menuItem = [menu addItemWithTitle:NSLocalizedString(@"Services", nil) 55 | action:NULL 56 | keyEquivalent:@""]; 57 | NSMenu* servicesMenu = [[NSMenu alloc] initWithTitle:@"Services"]; 58 | [menu setSubmenu:servicesMenu forItem:menuItem]; 59 | [NSApp setServicesMenu:servicesMenu]; 60 | 61 | [menu addItem:[NSMenuItem separatorItem]]; 62 | 63 | menuItem = [menu addItemWithTitle:[NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"Hide", nil), JJApplicationName] 64 | action:@selector(hide:) 65 | keyEquivalent:@"h"]; 66 | [menuItem setTarget:NSApp]; 67 | 68 | menuItem = [menu addItemWithTitle:NSLocalizedString(@"Hide Others", nil) 69 | action:@selector(hideOtherApplications:) 70 | keyEquivalent:@"h"]; 71 | [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand | NSEventModifierFlagOption]; 72 | [menuItem setTarget:NSApp]; 73 | 74 | menuItem = [menu addItemWithTitle:NSLocalizedString(@"Show All", nil) 75 | action:@selector(unhideAllApplications:) 76 | keyEquivalent:@""]; 77 | [menuItem setTarget:NSApp]; 78 | 79 | [menu addItem:[NSMenuItem separatorItem]]; 80 | 81 | menuItem = [menu addItemWithTitle:[NSString stringWithFormat:@"%@ %@", NSLocalizedString(@"Quit", nil), JJApplicationName] 82 | action:@selector(terminate:) 83 | keyEquivalent:@"q"]; 84 | [menuItem setTarget:NSApp]; 85 | } 86 | 87 | +(void)populateEditMenu:(NSMenu*)menu { 88 | NSMenuItem* menuItem; 89 | 90 | [menu addItemWithTitle:NSLocalizedString(@"Undo", nil) 91 | action:@selector(undo:) 92 | keyEquivalent:@"z"]; 93 | 94 | [menu addItemWithTitle:NSLocalizedString(@"Redo", nil) 95 | action:@selector(redo:) 96 | keyEquivalent:@"Z"]; 97 | 98 | [menu addItem:[NSMenuItem separatorItem]]; 99 | 100 | [menu addItemWithTitle:NSLocalizedString(@"Cut", nil) 101 | action:@selector(cut:) 102 | keyEquivalent:@"x"]; 103 | 104 | [menu addItemWithTitle:NSLocalizedString(@"Copy", nil) 105 | action:@selector(copy:) 106 | keyEquivalent:@"c"]; 107 | 108 | [menu addItemWithTitle:NSLocalizedString(@"Paste", nil) 109 | action:@selector(paste:) 110 | keyEquivalent:@"v"]; 111 | 112 | menuItem = [menu addItemWithTitle:NSLocalizedString(@"Paste and Match Style", nil) 113 | action:@selector(pasteAsPlainText:) 114 | keyEquivalent:@"V"]; 115 | [menuItem setKeyEquivalentModifierMask:NSEventModifierFlagCommand | NSEventModifierFlagOption]; 116 | 117 | [menu addItemWithTitle:NSLocalizedString(@"Delete", nil) 118 | action:@selector(delete:) 119 | keyEquivalent:[NSString stringWithFormat:@"%C", (unichar)NSBackspaceCharacter]]; 120 | 121 | [menu addItemWithTitle:NSLocalizedString(@"Select All", nil) 122 | action:@selector(selectAll:) 123 | keyEquivalent:@"a"]; 124 | 125 | [menu addItem:[NSMenuItem separatorItem]]; 126 | 127 | menuItem = [menu addItemWithTitle:NSLocalizedString(@"Find", nil) 128 | action:NULL 129 | keyEquivalent:@""]; 130 | NSMenu* findMenu = [[NSMenu alloc] initWithTitle:@"Find"]; 131 | [self populateFindMenu:findMenu]; 132 | [menu setSubmenu:findMenu forItem:menuItem]; 133 | 134 | menuItem = [menu addItemWithTitle:NSLocalizedString(@"Spelling", nil) 135 | action:NULL 136 | keyEquivalent:@""]; 137 | NSMenu* spellingMenu = [[NSMenu alloc] initWithTitle:@"Spelling"]; 138 | [self populateSpellingMenu:spellingMenu]; 139 | [menu setSubmenu:spellingMenu forItem:menuItem]; 140 | } 141 | 142 | +(void)populateFindMenu:(NSMenu*)menu { 143 | NSMenuItem* menuItem; 144 | 145 | menuItem = [menu addItemWithTitle:NSLocalizedString(@"Find…", nil) 146 | action:@selector(performFindPanelAction:) 147 | keyEquivalent:@"f"]; 148 | [menuItem setTag:NSFindPanelActionShowFindPanel]; 149 | 150 | menuItem = [menu addItemWithTitle:NSLocalizedString(@"Find Next", nil) 151 | action:@selector(performFindPanelAction:) 152 | keyEquivalent:@"g"]; 153 | [menuItem setTag:NSFindPanelActionNext]; 154 | 155 | menuItem = [menu addItemWithTitle:NSLocalizedString(@"Find Previous", nil) 156 | action:@selector(performFindPanelAction:) 157 | keyEquivalent:@"G"]; 158 | [menuItem setTag:NSFindPanelActionPrevious]; 159 | 160 | menuItem = [menu addItemWithTitle:NSLocalizedString(@"Use Selection for Find", nil) 161 | action:@selector(performFindPanelAction:) 162 | keyEquivalent:@"e"]; 163 | [menuItem setTag:NSFindPanelActionSetFindString]; 164 | 165 | [menu addItemWithTitle:NSLocalizedString(@"Jump to Selection", nil) 166 | action:@selector(centerSelectionInVisibleArea:) 167 | keyEquivalent:@"j"]; 168 | } 169 | 170 | +(void)populateSpellingMenu:(NSMenu*)menu { 171 | [menu addItemWithTitle:NSLocalizedString(@"Spelling…", nil) 172 | action:@selector(showGuessPanel:) 173 | keyEquivalent:@":"]; 174 | 175 | [menu addItemWithTitle:NSLocalizedString(@"Check Spelling", nil) 176 | action:@selector(checkSpelling:) 177 | keyEquivalent:@";"]; 178 | 179 | [menu addItemWithTitle:NSLocalizedString(@"Check Spelling as You Type", nil) 180 | action:@selector(toggleContinuousSpellChecking:) 181 | keyEquivalent:@""]; 182 | } 183 | 184 | +(void)populateWindowMenu:(NSMenu*)menu { 185 | [menu addItemWithTitle:NSLocalizedString(@"Close Window", nil) 186 | action:@selector(performClose:) 187 | keyEquivalent:@"w"]; 188 | [menu addItemWithTitle:NSLocalizedString(@"Minimize", nil) 189 | action:@selector(performMiniaturize:) 190 | keyEquivalent:@"m"]; 191 | 192 | [menu addItem:[NSMenuItem separatorItem]]; 193 | 194 | [menu addItemWithTitle:NSLocalizedString(@"Bring All to Front", nil) 195 | action:@selector(arrangeInFront:) 196 | keyEquivalent:@""]; 197 | 198 | [menu addItem:[NSMenuItem separatorItem]]; 199 | 200 | [menu addItemWithTitle:JJApplicationName 201 | action:@selector(openMainWindow:) 202 | keyEquivalent:@""]; 203 | } 204 | 205 | +(void)populateHelpMenu:(NSMenu*)menu { 206 | [menu addItemWithTitle:[NSString stringWithFormat:@"%@ %@", JJApplicationName, NSLocalizedString(@"Web Site", nil)] 207 | action:@selector(openWebSite:) 208 | keyEquivalent:@""]; 209 | 210 | [menu addItemWithTitle:NSLocalizedString(@"License", nil) 211 | action:@selector(openLicense:) 212 | keyEquivalent:@""]; 213 | } 214 | 215 | @end 216 | -------------------------------------------------------------------------------- /source/JJApplicationDelegate.m: -------------------------------------------------------------------------------- 1 | #import "JJApplicationDelegate.h" 2 | 3 | #import "JJLicenseWindow.h" 4 | #import "JJMainMenu.h" 5 | #import "JJMainWindow.h" 6 | 7 | NSString* JJApplicationName; 8 | 9 | @implementation JJApplicationDelegate { 10 | BOOL _didOpenURLs; 11 | NSUInteger _urlCount; 12 | NSWindow* _licenseWindow; 13 | NSWindow* _mainWindow; 14 | NSMutableArray* _tasks; 15 | } 16 | 17 | #pragma mark Private 18 | 19 | -(void)openNewsWithURLString:(nonnull NSString*)absoluteString { 20 | NSString* newsOpener = [[NSBundle mainBundle] pathForAuxiliaryExecutable:@"NewsOpener"]; 21 | if (newsOpener == nil) { 22 | NSLog(@"Missing NewsOpener executable!"); 23 | return; 24 | } 25 | 26 | NSTask* task = [[NSTask alloc] init]; 27 | [task setLaunchPath:newsOpener]; 28 | [task setArguments:@[absoluteString]]; 29 | [task setStandardInput:[NSPipe pipe]]; 30 | 31 | @try { 32 | [task launch]; 33 | } @catch (NSException* exception) { 34 | NSLog(@"NewsOpener exception: %@", [exception reason]); 35 | task = nil; 36 | } 37 | if (task != nil) { 38 | if (![task isRunning]) { 39 | NSLog(@"NewsOpener terminationStatus: %i", [task terminationStatus]); 40 | } else { 41 | if (_tasks == nil) 42 | _tasks = [[NSMutableArray alloc] init]; 43 | [_tasks addObject:task]; 44 | } 45 | } 46 | } 47 | 48 | -(void)dataTaskDidFinishWithURL:(nonnull NSURL*)url message:(nullable NSString*)message { 49 | dispatch_async(dispatch_get_main_queue(), ^{ 50 | if (message == nil) { 51 | --_urlCount; 52 | if (_urlCount == 0) 53 | [self performSelector:@selector(terminateIfNecessary) withObject:nil afterDelay:2.0]; 54 | [[NSWorkspace sharedWorkspace] openURL:url]; 55 | } else { 56 | NSString* absoluteString = [url absoluteString]; 57 | NSAlert* alert = [[NSAlert alloc] init]; 58 | [alert setMessageText:NSLocalizedString(@"Original Article Not Found", nil)]; 59 | NSString* informativeText = [NSString stringWithFormat:@"%@\n\n%@\n\nOpen the URL in News app instead?", absoluteString, message]; 60 | [alert setInformativeText:informativeText]; 61 | [alert addButtonWithTitle:NSLocalizedString(@"News app", nil)]; 62 | [alert addButtonWithTitle:NSLocalizedString(@"Cancel", nil)]; 63 | NSModalResponse modalResponse = [alert runModal]; 64 | if (modalResponse == NSAlertFirstButtonReturn) { 65 | [self openNewsWithURLString:absoluteString]; 66 | } 67 | --_urlCount; 68 | if (_urlCount == 0) 69 | [self terminateIfNecessary]; 70 | } 71 | }); 72 | } 73 | 74 | -(void)terminateIfNecessary { 75 | NSArray* windows = [NSApp windows]; 76 | for (NSWindow* window in windows) { 77 | if ([window isVisible]) 78 | return; // Don't terminate if there are visible windows 79 | } 80 | [NSApp terminate:nil]; 81 | } 82 | 83 | -(void)openMacAppStoreURL:(nonnull NSURL*)url { 84 | [[NSWorkspace sharedWorkspace] openURLs:@[url] withAppBundleIdentifier:@"com.apple.AppStore" options:NSWorkspaceLaunchAsync additionalEventParamDescriptor:nil launchIdentifiers:nil]; 85 | } 86 | 87 | #pragma mark 88 | 89 | -(void)userNotificationCenter:(nonnull UNUserNotificationCenter*)notificationCenter didReceiveNotificationResponse:(nonnull UNNotificationResponse*)response withCompletionHandler:(nonnull void(^)(void))completionHandler { 90 | _didOpenURLs = YES; 91 | NSString* actionIdentifier = [response actionIdentifier]; 92 | if (![actionIdentifier isEqualToString:UNNotificationDismissActionIdentifier]) { 93 | UNNotification* notification = [response notification]; 94 | UNNotificationRequest* request = [notification request]; 95 | UNNotificationContent* content = [request content]; 96 | NSDictionary* userInfo = [content userInfo]; 97 | NSDictionary* news = userInfo[@"news"]; 98 | if (news != nil && [news isKindOfClass:[NSDictionary class]]) { 99 | NSString* cu = news[@"cu"]; 100 | if (cu != nil && [cu isKindOfClass:[NSString class]] && [cu hasPrefix:@"http"]) { 101 | NSURL* url = [NSURL URLWithString:cu]; 102 | if (url != nil) { 103 | [[NSWorkspace sharedWorkspace] openURL:url]; 104 | completionHandler(); 105 | return; 106 | } 107 | } 108 | NSString* aid = news[@"aid"]; // article id 109 | if (aid != nil && [aid isKindOfClass:[NSString class]]) { 110 | NSURL* newsURL = [NSURL URLWithString:[@"applenewss:/" stringByAppendingString:aid]]; 111 | if (newsURL) { 112 | [self application:NSApp openURLs:@[newsURL]]; 113 | completionHandler(); 114 | return; 115 | } 116 | } 117 | } 118 | } 119 | completionHandler(); 120 | [self terminateIfNecessary]; 121 | } 122 | 123 | #pragma mark NSApplicationDelegate 124 | 125 | -(void)applicationWillFinishLaunching:(nonnull NSNotification *)notification { 126 | JJApplicationName = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleName"]; 127 | if (JJApplicationName == nil) { 128 | NSLog(@"CFBundleName nil!"); 129 | JJApplicationName = @"StopTheNews"; 130 | } 131 | [JJMainMenu populateMainMenu]; 132 | 133 | [[UNUserNotificationCenter currentNotificationCenter] setDelegate:self]; 134 | } 135 | 136 | -(void)applicationWillTerminate:(nonnull NSNotification*)notification { 137 | [[UNUserNotificationCenter currentNotificationCenter] setDelegate:nil]; 138 | } 139 | 140 | -(void)applicationDidFinishLaunching:(nonnull NSNotification*)notification { 141 | if (_didOpenURLs) 142 | return; 143 | 144 | static NSString*_Nonnull const FirstRunWindowShown = @"FirstRunWindowShown"; 145 | NSUserDefaults* standardUserDefaults = [NSUserDefaults standardUserDefaults]; 146 | if ([standardUserDefaults boolForKey:FirstRunWindowShown]) 147 | return; 148 | [standardUserDefaults setBool:YES forKey:FirstRunWindowShown]; 149 | 150 | NSString* defaultHandler = CFBridgingRelease(LSCopyDefaultHandlerForURLScheme(CFSTR("macappstore"))); // LSCopyDefaultHandlerForURLScheme is deprecated but so what. 151 | if (defaultHandler != nil && [defaultHandler caseInsensitiveCompare:@"com.apple.news"] == NSOrderedSame) { 152 | // StopTheNews 3.0 set itself as the default Mac App Store link handler. 153 | // This is bad when StopTheNews is uninstalled, because then Apple News becomes the default Mac App Store link handler, so set it back to App Store app. 154 | CFStringRef bundleID = CFSTR("com.apple.AppStore"); 155 | NSArray* schemes = @[@"macappstore", @"macappstores"]; 156 | OSStatus status; 157 | for (NSString* scheme in schemes) { 158 | status = LSSetDefaultHandlerForURLScheme((__bridge CFStringRef _Nonnull)scheme, bundleID); 159 | if (status != noErr) 160 | NSLog(@"LSSetDefaultHandlerForURLScheme %@ failed: %i", scheme, status); 161 | } 162 | } 163 | 164 | [self openMainWindow:nil]; 165 | } 166 | 167 | -(void)applicationDidResignActive:(nonnull NSNotification*)notification { 168 | if (_urlCount > 0) 169 | return; 170 | 171 | [self terminateIfNecessary]; 172 | } 173 | 174 | -(void)application:(nonnull NSApplication*)application openURLs:(nonnull NSArray*)urls { 175 | _didOpenURLs = YES; 176 | _urlCount += [urls count]; 177 | NSURLSession* session = [NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration ephemeralSessionConfiguration]]; 178 | for (NSURL* url in urls) { 179 | NSString* absoluteString = [url absoluteString]; 180 | if (absoluteString == nil) { 181 | [self dataTaskDidFinishWithURL:url message:NSLocalizedString(@"The URL has no absolute string.", nil)]; 182 | } else { 183 | NSString* newString; 184 | if ([absoluteString hasPrefix:@"applenews://"]) { 185 | newString = [absoluteString stringByReplacingCharactersInRange:NSMakeRange(0, [@"applenews://" length]) withString:@"https://apple.news"]; 186 | } else if ([absoluteString hasPrefix:@"applenewss:"]) { 187 | newString = [absoluteString stringByReplacingCharactersInRange:NSMakeRange(0, [@"applenewss:" length]) withString:@"https://apple.news"]; 188 | } else if ([absoluteString hasPrefix:@"https://apple.news/"]) { 189 | newString = absoluteString; 190 | } else { 191 | [self dataTaskDidFinishWithURL:url message:nil]; 192 | continue; 193 | } 194 | NSURL* newURL = [NSURL URLWithString:newString]; 195 | if (newURL == nil) { 196 | [self dataTaskDidFinishWithURL:url message:NSLocalizedString(@"Cannot generate an Apple News URL.", nil)]; 197 | } else { 198 | NSURLSessionDataTask* dataTask = [session dataTaskWithURL:newURL completionHandler:^(NSData*_Nullable data, NSURLResponse*_Nullable response, NSError*_Nullable error) { 199 | if (data == nil) { 200 | if (response == nil) { 201 | [self dataTaskDidFinishWithURL:url message:[error localizedDescription]]; 202 | } else { 203 | NSString* message = [NSString stringWithFormat:@"URL Response: %@\n\n%@", response, [error localizedDescription]]; 204 | [self dataTaskDidFinishWithURL:url message:message]; 205 | } 206 | } else { 207 | NSString* html = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; 208 | if (html == nil) { 209 | [self dataTaskDidFinishWithURL:url message:NSLocalizedString(@"The data was not UTF-8.", nil)]; 210 | } else { 211 | NSRange range = [html rangeOfString:@"redirectToUrlAfterTimeout(\""]; 212 | NSUInteger location = range.location; 213 | if (location == NSNotFound) { 214 | range = [html rangeOfString:@"redirectToUrlAfterTimeout("]; 215 | location = range.location; 216 | if (location == NSNotFound) { 217 | [self dataTaskDidFinishWithURL:url message:NSLocalizedString(@"Cannot find the function redirectToUrlAfterTimeout().", nil)]; 218 | } else { 219 | [self dataTaskDidFinishWithURL:url message:NSLocalizedString(@"This article appears to be written exclusively for Apple News. No original article is listed.", nil)]; 220 | } 221 | } else { 222 | NSUInteger length = range.length; 223 | NSScanner* scanner = [NSScanner scannerWithString:html]; 224 | [scanner setScanLocation:location + length]; 225 | [scanner setCharactersToBeSkipped:nil]; 226 | NSString* originalURLString; 227 | if (![scanner scanUpToString:@"\"" intoString:&originalURLString]) { 228 | [self dataTaskDidFinishWithURL:url message:NSLocalizedString(@"Cannot find the end of the original URL.", nil)]; 229 | } else { 230 | NSURL* originalURL = [NSURL URLWithString:originalURLString]; 231 | if (originalURL == nil) { 232 | [self dataTaskDidFinishWithURL:url message:NSLocalizedString(@"The original URL is improperly formatted.", nil)]; 233 | } else { 234 | [self dataTaskDidFinishWithURL:originalURL message:nil]; 235 | } 236 | } 237 | } 238 | } 239 | } 240 | }]; 241 | [dataTask setPriority:1.0]; 242 | [dataTask resume]; 243 | } 244 | } 245 | } 246 | } 247 | 248 | #pragma mark JJApplicationDelegate 249 | 250 | -(void)windowWillClose:(nonnull NSNotification*)notification { 251 | id object = [notification object]; 252 | [[NSNotificationCenter defaultCenter] removeObserver:self name:[notification name] object:object]; 253 | if (object == _licenseWindow) 254 | _licenseWindow = nil; 255 | else if (object == _mainWindow) 256 | _mainWindow = nil; 257 | } 258 | 259 | -(void)openLicense:(nullable id)sender { 260 | if (_licenseWindow != nil) { 261 | [_licenseWindow makeKeyAndOrderFront:self]; 262 | } else { 263 | _licenseWindow = [JJLicenseWindow window]; 264 | if (_licenseWindow != nil) 265 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowWillClose:) name:NSWindowWillCloseNotification object:_licenseWindow]; 266 | } 267 | } 268 | 269 | -(void)openMacAppStore:(id)sender { 270 | NSURL* url = [NSURL URLWithString:@"macappstore://itunes.apple.com/app/stopthemadness/id1376402589"]; 271 | if (url != nil) 272 | [self openMacAppStoreURL:url]; 273 | else 274 | NSLog(@"MAS URL nil!"); 275 | } 276 | 277 | -(void)openMainWindow:(nullable id)sender { 278 | if (_mainWindow != nil) { 279 | [_mainWindow makeKeyAndOrderFront:self]; 280 | } else { 281 | _mainWindow = [JJMainWindow window]; 282 | [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowWillClose:) name:NSWindowWillCloseNotification object:_mainWindow]; 283 | } 284 | } 285 | 286 | -(void)openWebSite:(nullable id)sender { 287 | NSURL* url = [NSURL URLWithString:@"https://github.com/lapcat/StopTheNews"]; 288 | if (url != nil) 289 | [[NSWorkspace sharedWorkspace] openURL:url]; 290 | else 291 | NSLog(@"Support URL nil!"); 292 | } 293 | 294 | @end 295 | -------------------------------------------------------------------------------- /StopTheNews.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 51; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 5214863C260ECCC8005210E2 /* NewsOpener.c in Sources */ = {isa = PBXBuildFile; fileRef = 5214863B260ECCC8005210E2 /* NewsOpener.c */; }; 11 | 522DB9C4227CCB540013DFF5 /* JJMainMenu.m in Sources */ = {isa = PBXBuildFile; fileRef = 522DB9C3227CCB540013DFF5 /* JJMainMenu.m */; }; 12 | 5232475D227CB982004010F5 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 52324756227CB982004010F5 /* Assets.xcassets */; }; 13 | 5232475F227CB982004010F5 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 5232475B227CB982004010F5 /* main.m */; }; 14 | 52324760227CB982004010F5 /* JJApplicationDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 5232475C227CB982004010F5 /* JJApplicationDelegate.m */; }; 15 | 52699C04227D2878006F536D /* LICENSE.txt in Resources */ = {isa = PBXBuildFile; fileRef = 52699C03227D2878006F536D /* LICENSE.txt */; }; 16 | 527FDD52260EB4FF00B1C9FD /* NewsOpener in CopyFiles */ = {isa = PBXBuildFile; fileRef = 527FDD47260EB10200B1C9FD /* NewsOpener */; }; 17 | 52989F99227DD2F2006A4E38 /* JJMainWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 52989F98227DD2F2006A4E38 /* JJMainWindow.m */; }; 18 | 52CECB54227D26DD00E69FB4 /* JJLicenseWindow.m in Sources */ = {isa = PBXBuildFile; fileRef = 52CECB53227D26DD00E69FB4 /* JJLicenseWindow.m */; }; 19 | /* End PBXBuildFile section */ 20 | 21 | /* Begin PBXContainerItemProxy section */ 22 | 527FDD4F260EB4E200B1C9FD /* PBXContainerItemProxy */ = { 23 | isa = PBXContainerItemProxy; 24 | containerPortal = 52E05A9B227CB87C008791E2 /* Project object */; 25 | proxyType = 1; 26 | remoteGlobalIDString = 527FDD46260EB10200B1C9FD; 27 | remoteInfo = NewsOpener; 28 | }; 29 | /* End PBXContainerItemProxy section */ 30 | 31 | /* Begin PBXCopyFilesBuildPhase section */ 32 | 527FDD51260EB4EB00B1C9FD /* CopyFiles */ = { 33 | isa = PBXCopyFilesBuildPhase; 34 | buildActionMask = 2147483647; 35 | dstPath = ""; 36 | dstSubfolderSpec = 6; 37 | files = ( 38 | 527FDD52260EB4FF00B1C9FD /* NewsOpener in CopyFiles */, 39 | ); 40 | runOnlyForDeploymentPostprocessing = 0; 41 | }; 42 | /* End PBXCopyFilesBuildPhase section */ 43 | 44 | /* Begin PBXFileReference section */ 45 | 5214863B260ECCC8005210E2 /* NewsOpener.c */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.c; path = NewsOpener.c; sourceTree = ""; }; 46 | 522DB9C2227CCB540013DFF5 /* JJMainMenu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JJMainMenu.h; sourceTree = ""; }; 47 | 522DB9C3227CCB540013DFF5 /* JJMainMenu.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JJMainMenu.m; sourceTree = ""; }; 48 | 52324756227CB982004010F5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 49 | 52324757227CB982004010F5 /* StopTheNews.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = StopTheNews.entitlements; sourceTree = ""; }; 50 | 52324758227CB982004010F5 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | 5232475A227CB982004010F5 /* JJApplicationDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = JJApplicationDelegate.h; sourceTree = ""; }; 52 | 5232475B227CB982004010F5 /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 53 | 5232475C227CB982004010F5 /* JJApplicationDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = JJApplicationDelegate.m; sourceTree = ""; }; 54 | 52324761227CBAE9004010F5 /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; 55 | 52324762227CBAE9004010F5 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = ""; }; 56 | 52324763227CBAE9004010F5 /* Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; 57 | 52545808227DE38C00B9863A /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = ""; }; 58 | 52699C03227D2878006F536D /* LICENSE.txt */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; path = LICENSE.txt; sourceTree = ""; }; 59 | 527FDD47260EB10200B1C9FD /* NewsOpener */ = {isa = PBXFileReference; explicitFileType = "compiled.mach-o.executable"; includeInIndex = 0; path = NewsOpener; sourceTree = BUILT_PRODUCTS_DIR; }; 60 | 52989F97227DD2F2006A4E38 /* JJMainWindow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JJMainWindow.h; sourceTree = ""; }; 61 | 52989F98227DD2F2006A4E38 /* JJMainWindow.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JJMainWindow.m; sourceTree = ""; }; 62 | 52CECB52227D26DD00E69FB4 /* JJLicenseWindow.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = JJLicenseWindow.h; sourceTree = ""; }; 63 | 52CECB53227D26DD00E69FB4 /* JJLicenseWindow.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = JJLicenseWindow.m; sourceTree = ""; }; 64 | 52E05AA3227CB87C008791E2 /* StopTheNews.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = StopTheNews.app; sourceTree = BUILT_PRODUCTS_DIR; }; 65 | /* End PBXFileReference section */ 66 | 67 | /* Begin PBXGroup section */ 68 | 52324755227CB982004010F5 /* nonsource */ = { 69 | isa = PBXGroup; 70 | children = ( 71 | 52324756227CB982004010F5 /* Assets.xcassets */, 72 | 52324758227CB982004010F5 /* Info.plist */, 73 | 52324757227CB982004010F5 /* StopTheNews.entitlements */, 74 | ); 75 | path = nonsource; 76 | sourceTree = SOURCE_ROOT; 77 | }; 78 | 52324759227CB982004010F5 /* source */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 5214863B260ECCC8005210E2 /* NewsOpener.c */, 82 | 5232475A227CB982004010F5 /* JJApplicationDelegate.h */, 83 | 5232475C227CB982004010F5 /* JJApplicationDelegate.m */, 84 | 52CECB52227D26DD00E69FB4 /* JJLicenseWindow.h */, 85 | 52CECB53227D26DD00E69FB4 /* JJLicenseWindow.m */, 86 | 522DB9C2227CCB540013DFF5 /* JJMainMenu.h */, 87 | 522DB9C3227CCB540013DFF5 /* JJMainMenu.m */, 88 | 52989F97227DD2F2006A4E38 /* JJMainWindow.h */, 89 | 52989F98227DD2F2006A4E38 /* JJMainWindow.m */, 90 | 5232475B227CB982004010F5 /* main.m */, 91 | ); 92 | path = source; 93 | sourceTree = SOURCE_ROOT; 94 | }; 95 | 52E05A9A227CB87C008791E2 = { 96 | isa = PBXGroup; 97 | children = ( 98 | 52545808227DE38C00B9863A /* README.md */, 99 | 52699C03227D2878006F536D /* LICENSE.txt */, 100 | 52324763227CBAE9004010F5 /* Debug.xcconfig */, 101 | 52324761227CBAE9004010F5 /* Release.xcconfig */, 102 | 52324762227CBAE9004010F5 /* Shared.xcconfig */, 103 | 52324755227CB982004010F5 /* nonsource */, 104 | 52324759227CB982004010F5 /* source */, 105 | 52E05AA4227CB87C008791E2 /* Products */, 106 | ); 107 | sourceTree = ""; 108 | }; 109 | 52E05AA4227CB87C008791E2 /* Products */ = { 110 | isa = PBXGroup; 111 | children = ( 112 | 52E05AA3227CB87C008791E2 /* StopTheNews.app */, 113 | 527FDD47260EB10200B1C9FD /* NewsOpener */, 114 | ); 115 | name = Products; 116 | sourceTree = ""; 117 | }; 118 | /* End PBXGroup section */ 119 | 120 | /* Begin PBXNativeTarget section */ 121 | 527FDD46260EB10200B1C9FD /* NewsOpener */ = { 122 | isa = PBXNativeTarget; 123 | buildConfigurationList = 527FDD4B260EB10200B1C9FD /* Build configuration list for PBXNativeTarget "NewsOpener" */; 124 | buildPhases = ( 125 | 527FDD43260EB10200B1C9FD /* Sources */, 126 | ); 127 | buildRules = ( 128 | ); 129 | dependencies = ( 130 | ); 131 | name = NewsOpener; 132 | productName = NewsOpener; 133 | productReference = 527FDD47260EB10200B1C9FD /* NewsOpener */; 134 | productType = "com.apple.product-type.tool"; 135 | }; 136 | 52E05AA2227CB87C008791E2 /* StopTheNews */ = { 137 | isa = PBXNativeTarget; 138 | buildConfigurationList = 52E05AB4227CB87D008791E2 /* Build configuration list for PBXNativeTarget "StopTheNews" */; 139 | buildPhases = ( 140 | 52E05A9F227CB87C008791E2 /* Sources */, 141 | 52E05AA1227CB87C008791E2 /* Resources */, 142 | 527FDD51260EB4EB00B1C9FD /* CopyFiles */, 143 | ); 144 | buildRules = ( 145 | ); 146 | dependencies = ( 147 | 527FDD50260EB4E200B1C9FD /* PBXTargetDependency */, 148 | ); 149 | name = StopTheNews; 150 | productName = StopTheNews; 151 | productReference = 52E05AA3227CB87C008791E2 /* StopTheNews.app */; 152 | productType = "com.apple.product-type.application"; 153 | }; 154 | /* End PBXNativeTarget section */ 155 | 156 | /* Begin PBXProject section */ 157 | 52E05A9B227CB87C008791E2 /* Project object */ = { 158 | isa = PBXProject; 159 | attributes = { 160 | LastUpgradeCheck = 9999; 161 | ORGANIZATIONNAME = "Jeff Johnson"; 162 | TargetAttributes = { 163 | 527FDD46260EB10200B1C9FD = { 164 | CreatedOnToolsVersion = 11.3.1; 165 | }; 166 | 52E05AA2227CB87C008791E2 = { 167 | CreatedOnToolsVersion = 10.1; 168 | }; 169 | }; 170 | }; 171 | buildConfigurationList = 52E05A9E227CB87C008791E2 /* Build configuration list for PBXProject "StopTheNews" */; 172 | compatibilityVersion = "Xcode 10.0"; 173 | developmentRegion = en; 174 | hasScannedForEncodings = 0; 175 | knownRegions = ( 176 | en, 177 | Base, 178 | ); 179 | mainGroup = 52E05A9A227CB87C008791E2; 180 | productRefGroup = 52E05AA4227CB87C008791E2 /* Products */; 181 | projectDirPath = ""; 182 | projectRoot = ""; 183 | targets = ( 184 | 52E05AA2227CB87C008791E2 /* StopTheNews */, 185 | 527FDD46260EB10200B1C9FD /* NewsOpener */, 186 | ); 187 | }; 188 | /* End PBXProject section */ 189 | 190 | /* Begin PBXResourcesBuildPhase section */ 191 | 52E05AA1227CB87C008791E2 /* Resources */ = { 192 | isa = PBXResourcesBuildPhase; 193 | buildActionMask = 2147483647; 194 | files = ( 195 | 52699C04227D2878006F536D /* LICENSE.txt in Resources */, 196 | 5232475D227CB982004010F5 /* Assets.xcassets in Resources */, 197 | ); 198 | runOnlyForDeploymentPostprocessing = 0; 199 | }; 200 | /* End PBXResourcesBuildPhase section */ 201 | 202 | /* Begin PBXSourcesBuildPhase section */ 203 | 527FDD43260EB10200B1C9FD /* Sources */ = { 204 | isa = PBXSourcesBuildPhase; 205 | buildActionMask = 2147483647; 206 | files = ( 207 | 5214863C260ECCC8005210E2 /* NewsOpener.c in Sources */, 208 | ); 209 | runOnlyForDeploymentPostprocessing = 0; 210 | }; 211 | 52E05A9F227CB87C008791E2 /* Sources */ = { 212 | isa = PBXSourcesBuildPhase; 213 | buildActionMask = 2147483647; 214 | files = ( 215 | 52324760227CB982004010F5 /* JJApplicationDelegate.m in Sources */, 216 | 52CECB54227D26DD00E69FB4 /* JJLicenseWindow.m in Sources */, 217 | 52989F99227DD2F2006A4E38 /* JJMainWindow.m in Sources */, 218 | 522DB9C4227CCB540013DFF5 /* JJMainMenu.m in Sources */, 219 | 5232475F227CB982004010F5 /* main.m in Sources */, 220 | ); 221 | runOnlyForDeploymentPostprocessing = 0; 222 | }; 223 | /* End PBXSourcesBuildPhase section */ 224 | 225 | /* Begin PBXTargetDependency section */ 226 | 527FDD50260EB4E200B1C9FD /* PBXTargetDependency */ = { 227 | isa = PBXTargetDependency; 228 | target = 527FDD46260EB10200B1C9FD /* NewsOpener */; 229 | targetProxy = 527FDD4F260EB4E200B1C9FD /* PBXContainerItemProxy */; 230 | }; 231 | /* End PBXTargetDependency section */ 232 | 233 | /* Begin XCBuildConfiguration section */ 234 | 527FDD4C260EB10200B1C9FD /* Debug */ = { 235 | isa = XCBuildConfiguration; 236 | baseConfigurationReference = 52324763227CBAE9004010F5 /* Debug.xcconfig */; 237 | buildSettings = { 238 | PRODUCT_NAME = "$(TARGET_NAME)"; 239 | SKIP_INSTALL = YES; 240 | }; 241 | name = Debug; 242 | }; 243 | 527FDD4D260EB10200B1C9FD /* Release */ = { 244 | isa = XCBuildConfiguration; 245 | baseConfigurationReference = 52324761227CBAE9004010F5 /* Release.xcconfig */; 246 | buildSettings = { 247 | PRODUCT_NAME = "$(TARGET_NAME)"; 248 | SKIP_INSTALL = YES; 249 | }; 250 | name = Release; 251 | }; 252 | 52E05AB2227CB87D008791E2 /* Debug */ = { 253 | isa = XCBuildConfiguration; 254 | baseConfigurationReference = 52324762227CBAE9004010F5 /* Shared.xcconfig */; 255 | buildSettings = { 256 | }; 257 | name = Debug; 258 | }; 259 | 52E05AB3227CB87D008791E2 /* Release */ = { 260 | isa = XCBuildConfiguration; 261 | baseConfigurationReference = 52324762227CBAE9004010F5 /* Shared.xcconfig */; 262 | buildSettings = { 263 | }; 264 | name = Release; 265 | }; 266 | 52E05AB5227CB87D008791E2 /* Debug */ = { 267 | isa = XCBuildConfiguration; 268 | baseConfigurationReference = 52324763227CBAE9004010F5 /* Debug.xcconfig */; 269 | buildSettings = { 270 | PRODUCT_NAME = StopTheNews; 271 | }; 272 | name = Debug; 273 | }; 274 | 52E05AB6227CB87D008791E2 /* Release */ = { 275 | isa = XCBuildConfiguration; 276 | baseConfigurationReference = 52324761227CBAE9004010F5 /* Release.xcconfig */; 277 | buildSettings = { 278 | PRODUCT_NAME = StopTheNews; 279 | }; 280 | name = Release; 281 | }; 282 | /* End XCBuildConfiguration section */ 283 | 284 | /* Begin XCConfigurationList section */ 285 | 527FDD4B260EB10200B1C9FD /* Build configuration list for PBXNativeTarget "NewsOpener" */ = { 286 | isa = XCConfigurationList; 287 | buildConfigurations = ( 288 | 527FDD4C260EB10200B1C9FD /* Debug */, 289 | 527FDD4D260EB10200B1C9FD /* Release */, 290 | ); 291 | defaultConfigurationIsVisible = 0; 292 | defaultConfigurationName = Release; 293 | }; 294 | 52E05A9E227CB87C008791E2 /* Build configuration list for PBXProject "StopTheNews" */ = { 295 | isa = XCConfigurationList; 296 | buildConfigurations = ( 297 | 52E05AB2227CB87D008791E2 /* Debug */, 298 | 52E05AB3227CB87D008791E2 /* Release */, 299 | ); 300 | defaultConfigurationIsVisible = 0; 301 | defaultConfigurationName = Release; 302 | }; 303 | 52E05AB4227CB87D008791E2 /* Build configuration list for PBXNativeTarget "StopTheNews" */ = { 304 | isa = XCConfigurationList; 305 | buildConfigurations = ( 306 | 52E05AB5227CB87D008791E2 /* Debug */, 307 | 52E05AB6227CB87D008791E2 /* Release */, 308 | ); 309 | defaultConfigurationIsVisible = 0; 310 | defaultConfigurationName = Release; 311 | }; 312 | /* End XCConfigurationList section */ 313 | }; 314 | rootObject = 52E05A9B227CB87C008791E2 /* Project object */; 315 | } 316 | --------------------------------------------------------------------------------