├── .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 |
--------------------------------------------------------------------------------