diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..d6cc08f --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2018 Oleg Geier + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..74f1f93 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# baRSS – *Menu Bar RSS Reader* + +![screenshot](doc/screenshot.png) + +For nearly a decade I've been using the then free version of [RSS Menu](https://itunes.apple.com/us/app/rss-menu/id423069534). However, with the release of macOS Mojave, 32bit applications are no longer supported. + +*baRSS* is an open source community project and will be available on the AppStore soon (hopefully); free of charge. Everything is built from the ground up with a minimal footprint in mind. + + +Why is this project not written in Swift? +----------------------------------------- + +Actually, I started this project with Swift. Even without adding much functionality, the app was exceeding the 10 Mb file size. Compared to the nearly finished Alpha version with 500 Kb written in Objective-C. The reason for that, Swift frameworks are always packed into the final application. I decided that this level of encapsulation is a waste of space for such a small application. + + +3rd Party Libraries +------------------- + +This project uses a modified version of Brent Simmons [RSXML](https://github.com/brentsimmons/RSXML) for feed parsing. RSXML is licensed under a MIT license (same as this project). + + +Current project state +--------------------- + +The basic functionality is there. Manually added feeds will be downloaded and stored in an SQLite database. The complete management of feeds is there (sorting, grouping, editing, deleting). The bar menu is functional too, including unread count, URL opening and display. + + +ToDo +---- + +- [ ] Preferences + - [x] Choose favorite web browser + - [x] Show list of installed browsers + - [ ] Choose status bar icon? + - [ ] Tick mark feed items based on prefs + - [ ] Open a few links (# editable) + - [ ] Performance: Update menu partially + - [ ] Start on login + - [x] Make it system default application + - [ ] Display license info (e.g., RSXML) + - [ ] Short article names + - [ ] Import / Export (all feeds) + - [ ] Support for `.opml` format + - [ ] Append or replace + + +- [ ] Status menu + - [ ] Update menu header after mark (un)read + - [ ] Pause updates functionality + - [ ] Update all feeds functionality + + +- [ ] Edit feed + - [ ] Show statistics + - [ ] How often gets the feed updated (min, max, avg) + - [ ] Automatically choose best interval? + - [ ] Auto fix 301 Redirect or ask user + - [ ] Make `feed://` URLs clickable + - [ ] Feeds with authentication + - [ ] Show proper feed icon + - [ ] Download and store icon file + + +- [ ] Other + - [ ] App Icon + - [ ] Translate text to different languages + - [ ] Automatically update feeds with chosen interval + - [ ] Reuse ETag and Modification date + - [ ] Append only new items, keep sorting + - [ ] Delete old ones eventually + - [ ] Purge cache + - [ ] Manually or automatically + - [ ] Add something to restore a broken state + - [ ] Code Documentation (mostly methods) + - [ ] Add Sandboxing + + +- [ ] Additional features + - [ ] Sync with online services! + - [ ] Notification Center + - [ ] Sleep timer. (e.g., disable updates during working hours) + - [ ] Pure image feed? (show images directly in menu) + - [ ] Infinite storage. (load more button) + - [ ] Automatically open feed items? + - [ ] Per feed launch application (e.g., for podcasts) + - [ ] Per group setting to exclude unread count from menu bar + diff --git a/baRSS/AppHook.m b/baRSS/AppHook.m index 6946cf2..a07cdc7 100644 --- a/baRSS/AppHook.m +++ b/baRSS/AppHook.m @@ -31,8 +31,14 @@ return self; } -- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { +- (void)applicationWillFinishLaunching:(NSNotification *)notification { _barMenu = [BarMenu new]; + NSAppleEventManager *appleEventManager = [NSAppleEventManager sharedAppleEventManager]; + [appleEventManager setEventHandler:self andSelector:@selector(handleGetURLEvent:withReplyEvent:) + forEventClass:kInternetEventClass andEventID:kAEGetURL]; +} + +- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { printf("up and running\n"); // https://feeds.feedburner.com/simpledesktops } @@ -41,6 +47,12 @@ } +- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { + NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; + // TODO: Open feed edit sheet in preferences + NSLog(@"%@", url); +} + #pragma mark - Core Data stack diff --git a/baRSS/Info.plist b/baRSS/Info.plist index 4d0013d..b12a976 100644 --- a/baRSS/Info.plist +++ b/baRSS/Info.plist @@ -2,36 +2,49 @@ - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleName - $(PRODUCT_NAME) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleVersion - 1 - CFBundleExecutable - $(EXECUTABLE_NAME) - NSPrincipalClass - AppHook - CFBundlePackageType - APPL - CFBundleIconFile - - LSUIElement - - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) - NSHumanReadableCopyright - Copyright © 2018 relikd. All rights reserved. + CFBundleExecutable + $(EXECUTABLE_NAME) + CFBundleIconFile + + CFBundleIdentifier + $(PRODUCT_BUNDLE_IDENTIFIER) + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + $(PRODUCT_NAME) + CFBundlePackageType + APPL CFBundleShortVersionString 1.0 + CFBundleURLTypes + + + CFBundleTypeRole + Viewer + CFBundleURLName + de.relikd.baRSS.url.feed + CFBundleURLSchemes + + feed + + + + CFBundleVersion + 1 + LSMinimumSystemVersion + $(MACOSX_DEPLOYMENT_TARGET) + LSUIElement + NSAppTransportSecurity NSAllowsArbitraryLoads + NSHumanReadableCopyright + Copyright © 2018 relikd. Public Domain. + NSPrincipalClass + AppHook diff --git a/baRSS/Preferences/General Tab/SettingsGeneral.m b/baRSS/Preferences/General Tab/SettingsGeneral.m index 98e7c47..a8ae63f 100644 --- a/baRSS/Preferences/General Tab/SettingsGeneral.m +++ b/baRSS/Preferences/General Tab/SettingsGeneral.m @@ -23,13 +23,42 @@ #import "SettingsGeneral.h" #import "AppHook.h" #import "BarMenu.h" +#import "UserPrefs.h" + + +@interface SettingsGeneral() +@property (weak) IBOutlet NSPopUpButton *popupHttpApplication; +@property (weak) IBOutlet NSPopUpButton *popupDefaultRSSReader; +@end @implementation SettingsGeneral - (void)viewDidLoad { [super viewDidLoad]; + // Default http application for opening the feed urls + [self generateMenuForPopup:self.popupHttpApplication withScheme:@"https"]; + [self.popupHttpApplication insertItemWithTitle:NSLocalizedString(@"System Default", @"Default web browser application") atIndex:0]; + [self selectBundleID:[UserPrefs getHttpApplication] inPopup:self.popupHttpApplication]; + // Default RSS Reader application + [self generateMenuForPopup:self.popupDefaultRSSReader withScheme:@"feed"]; + [self selectBundleID:[self defaultBundleIdForScheme:@"feed"] inPopup:self.popupDefaultRSSReader]; } +#pragma mark - UI interaction with IBAction + +- (IBAction)changeHttpApplication:(NSPopUpButton *)sender { + [UserPrefs setHttpApplication:sender.selectedItem.representedObject]; +} + +- (IBAction)changeDefaultRSSReader:(NSPopUpButton *)sender { + if ([self setDefaultRSSApplication:sender.selectedItem.representedObject] == NO) { + // in case anything went wrong, restore previous selection + [self selectBundleID:[self defaultBundleIdForScheme:@"feed"] inPopup:sender]; + } +} + +// TODO: add self to login items + - (IBAction)checkmarkClicked:(NSButton*)sender { // TODO: Could be optimized by updating only the relevant parts [[(AppHook*)NSApp barMenu] rebuildMenu]; @@ -48,11 +77,111 @@ [[(AppHook*)NSApp barMenu] updateMenuHeaders:recursive]; } -- (IBAction)changeMenuItemUpdateAllHidden:(NSButton*)sender { +- (IBAction)changeMenuItemUpdateAll:(NSButton*)sender { BOOL checked = (sender.state == NSControlStateValueOn); [[(AppHook*)NSApp barMenu] setItemUpdateAllHidden:!checked]; } -// TODO: show list of installed browsers and make menu choosable +#pragma mark - Helper methods + +/** + Populate @c NSPopUpButton menu with all available application for that scheme. + + @param scheme URL scheme like @c 'feed' or @c 'https' + */ +- (void)generateMenuForPopup:(NSPopUpButton*)popup withScheme:(NSString*)scheme { + [popup removeAllItems]; + NSArray *apps = [self listOfBundleIdsForScheme:scheme]; + for (NSString *bundleID in apps) { + NSString *appName = [self applicationNameForBundleId:bundleID]; + if (!appName) + appName = bundleID; + [popup addItemWithTitle:appName]; + popup.lastItem.representedObject = bundleID; + } +} + +/** + For a given @c NSPopUpButton select the item which represents the @c bundleID. + */ +- (void)selectBundleID:(NSString*)bundleID inPopup:(NSPopUpButton*)popup { + [popup selectItemAtIndex:[popup indexOfItemWithRepresentedObject:bundleID]]; +} + +/** + Get human readable, application name from @c bundleID. + + @param bundleID as defined in @c Info.plist + @return Application name such as 'Safari' or 'baRSS' + */ +- (NSString*)applicationNameForBundleId:(NSString*)bundleID { + CFStringRef bundleIDRef = CFBridgingRetain(bundleID); + if (!bundleIDRef) + return nil; + CFArrayRef arr = LSCopyApplicationURLsForBundleIdentifier(bundleIDRef, NULL); + CFRelease(bundleIDRef); + if (!arr) + return nil; + CFDictionaryRef infoDict = NULL; + if (CFArrayGetCount(arr) > 0) + infoDict = CFBundleCopyInfoDictionaryForURL(CFArrayGetValueAtIndex(arr, 0)); + CFRelease(arr); + if (!infoDict) + return nil; + NSString *name = CFDictionaryGetValue(infoDict, kCFBundleNameKey); + CFRelease(infoDict); + return name; +} + +/** + Get a list of all installed applications supporting that URL scheme. + + @param scheme URL scheme like @c 'feed' or @c 'https' + @return Array of @c bundleIDs of installed applications supporting that url scheme. + */ +- (NSArray*)listOfBundleIdsForScheme:(NSString*)scheme { + CFStringRef schemeRef = CFBridgingRetain(scheme); + if (!schemeRef) + return nil; + CFArrayRef allHandlers = LSCopyAllHandlersForURLScheme(schemeRef); + CFRelease(schemeRef); + return (NSArray*)CFBridgingRelease(allHandlers); +} + +/** + Get current default application for provided URL scheme. (e.g., ) + + @param scheme URL scheme like @c 'feed' or @c 'https' + @return @c bundleID of default application + */ +- (NSString*)defaultBundleIdForScheme:(NSString*)scheme { + CFStringRef schemeRef = CFBridgingRetain(scheme); + if (!schemeRef) + return nil; + CFStringRef defaultHandler = LSCopyDefaultHandlerForURLScheme(schemeRef); + CFRelease(schemeRef); + return (NSString*)CFBridgingRelease(defaultHandler); +} + +/** + Sets the default application for @c feed:// urls. (system wide) + + @param bundleID as defined in @c Info.plist + @return Return @c YES if operation was successfull. @c NO otherwise. + */ +- (BOOL)setDefaultRSSApplication:(NSString*)bundleID { + CFStringRef bundleIDRef = CFBridgingRetain(bundleID); + if (!bundleIDRef) + return NO; + CFStringRef schemeRef = CFBridgingRetain(@"feed"); + if (!schemeRef) { + CFRelease(bundleIDRef); + return NO; + } + OSStatus s = LSSetDefaultHandlerForURLScheme(schemeRef, bundleIDRef); + CFRelease(schemeRef); + CFRelease(bundleIDRef); + return s == 0; +} @end diff --git a/baRSS/Preferences/General Tab/SettingsGeneral.xib b/baRSS/Preferences/General Tab/SettingsGeneral.xib index 712bcd5..e061e72 100644 --- a/baRSS/Preferences/General Tab/SettingsGeneral.xib +++ b/baRSS/Preferences/General Tab/SettingsGeneral.xib @@ -8,6 +8,8 @@ + + @@ -42,7 +44,7 @@ - + @@ -327,7 +329,7 @@ - + @@ -336,33 +338,34 @@ - + - + - + - + - + - - - + + + + @@ -401,7 +404,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/baRSS/Preferences/General Tab/UserPrefs.h b/baRSS/Preferences/General Tab/UserPrefs.h index f426f25..b5467b5 100644 --- a/baRSS/Preferences/General Tab/UserPrefs.h +++ b/baRSS/Preferences/General Tab/UserPrefs.h @@ -25,4 +25,6 @@ @interface UserPrefs : NSObject + (BOOL)defaultYES:(NSString*)key; + (BOOL)defaultNO:(NSString*)key; ++ (NSString*)getHttpApplication; ++ (void)setHttpApplication:(NSString*)bundleID; @end diff --git a/baRSS/Preferences/General Tab/UserPrefs.m b/baRSS/Preferences/General Tab/UserPrefs.m index 37f706f..9561651 100644 --- a/baRSS/Preferences/General Tab/UserPrefs.m +++ b/baRSS/Preferences/General Tab/UserPrefs.m @@ -35,4 +35,12 @@ return [[NSUserDefaults standardUserDefaults] boolForKey:key]; } ++ (NSString*)getHttpApplication { + return [[NSUserDefaults standardUserDefaults] stringForKey:@"defaultHttpApplication"]; +} + ++ (void)setHttpApplication:(NSString*)bundleID { + [[NSUserDefaults standardUserDefaults] setObject:bundleID forKey:@"defaultHttpApplication"]; +} + @end diff --git a/baRSS/Status Bar Menu/BarMenu.m b/baRSS/Status Bar Menu/BarMenu.m index 22f3426..df848f1 100644 --- a/baRSS/Status Bar Menu/BarMenu.m +++ b/baRSS/Status Bar Menu/BarMenu.m @@ -78,7 +78,7 @@ Update menu bar icon and text according to unread count and user preferences. */ - (void)updateBarIcon { - // TODO: Option: unread count in menubar, Option: highlight color, Option: icon choice + // TODO: Option: icon choice dispatch_async(dispatch_get_main_queue(), ^{ if (self.unreadCountTotal > 0 && [UserPrefs defaultYES:@"globalUnreadCount"]) { self.barItem.title = [NSString stringWithFormat:@"%d", self.unreadCountTotal]; @@ -394,9 +394,8 @@ @param urls A list of @c NSURL objects that will be opened immediatelly in bulk. */ - (void)openURLsWithPreferredBrowser:(NSArray*)urls { - // TODO: lookup preferred browser in user preferences if (urls.count == 0) return; -// [[NSWorkspace sharedWorkspace] openURLs:urls withAppBundleIdentifier:@"com.apple.Safari" options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifiers:nil]; + [[NSWorkspace sharedWorkspace] openURLs:urls withAppBundleIdentifier:[UserPrefs getHttpApplication] options:NSWorkspaceLaunchDefault additionalEventParamDescriptor:nil launchIdentifiers:nil]; } diff --git a/doc/screenshot.png b/doc/screenshot.png new file mode 100644 index 0000000..6810bcd Binary files /dev/null and b/doc/screenshot.png differ