diff --git a/CHANGELOG.md b/CHANGELOG.md index d58f223..338ef06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2 - *Settings, General*: [Auxiliary application](https://github.com/relikd/URL-Scheme-Defaults) for changing default feed reader - *UI:* Accessibility hints for most UI elements - *UI*: Show welcome message upon first usage (empty db) +- *UI*: Custom colors via user defaults plist (bar icon tint & unread indicator) - Welcome message also adds Github releases feed - Config URL scheme `barss:` with `open/preferences`, `config/fixcache`, and `backup/show` diff --git a/README.md b/README.md index fb10d03..316b083 100644 --- a/README.md +++ b/README.md @@ -95,25 +95,36 @@ If you prefer the optimized release version go to `Product > Archive`. Hidden options -------------- -1) When holding down the option key, the menu will show an item to open only a few unread items at a time. +This listing contains of options that have no UI that can be configured. +Most likely, you wouldn't ever stumble upon these if not reading this chapter. +**Note:** To reset an option run `defaults delete de.relikd.baRSS {KEY}`, where `{KEY}` is an option from below. + + +1. If you hold down the option key and click on an article item, you can mark a single item (un-)read. + +2. When holding down the option key, the menu will show an item to open only a few unread items at a time. This number can be changed with the following Terminal command (default: 10): +``` +defaults write de.relikd.baRSS openFewLinksLimit -int 10 +``` -```defaults write de.relikd.baRSS openFewLinksLimit -int 10``` - - -2) If you hold down the option key and click on an article item, you can mark a single item (un-)read. - - -3) In preferences you can choose to show 'Short article names'. This will limit the number of displayed characters to 60 (default). +3. In preferences you can choose to show 'Short article names'. This will limit the number of displayed characters to 60 (default). With this Terminal command you can customize this limit: +``` +defaults write de.relikd.baRSS shortArticleNamesLimit -int 50 +``` -```defaults write de.relikd.baRSS shortArticleNamesLimit -int 50``` - - -4) Limit the number of displayed articles per feed menu. +4. Limit the number of displayed articles per feed menu. **Note:** displayed unread count may be different than the unread items inside ('Open unread' will open hidden items too). +``` +defaults write de.relikd.baRSS articlesInMenuLimit -int 40 +``` -```defaults write de.relikd.baRSS articlesInMenuLimit -int 40``` +5. You can change the appearance of colors throughout the application. E.g., The tint color of the menu bar icon and the color of the blue dot of unread articles. +``` +defaults write de.relikd.baRSS colorStatusIconTint -string "#37F" +defaults write de.relikd.baRSS colorUnreadTickMark -string "#FBA33A" +``` diff --git a/baRSS/Helper/DrawImage.h b/baRSS/Helper/DrawImage.h index 1585e14..e2ea804 100644 --- a/baRSS/Helper/DrawImage.h +++ b/baRSS/Helper/DrawImage.h @@ -22,14 +22,6 @@ @import Cocoa; -@interface NSColor (RandomColor) -/// just for testing purposes -+ (NSColor*)randomColor; -/// RGB color with (251, 163, 58) -+ (NSColor*)rssOrange; -@end - - /// Draw separator line in @c NSOutlineView IB_DESIGNABLE @interface DrawSeparator : NSView diff --git a/baRSS/Helper/DrawImage.m b/baRSS/Helper/DrawImage.m index 7887157..d335499 100644 --- a/baRSS/Helper/DrawImage.m +++ b/baRSS/Helper/DrawImage.m @@ -22,20 +22,7 @@ #import "DrawImage.h" #import "Constants.h" - -@implementation NSColor (RandomColor) -/// @return Color with random R, G, B values for testing purposes -+ (NSColor*)randomColor { - return [NSColor colorWithRed:(arc4random()%50+20)/100.0 - green:(arc4random()%50+20)/100.0 - blue:(arc4random()%50+20)/100.0 - alpha:1]; -} -/// @return Orange color that is typically used for RSS -+ (NSColor*)rssOrange { - return [NSColor colorWithCalibratedRed:251/255.0 green:163/255.0 blue:58/255.0 alpha:1.0]; -} -@end +#import "UserPrefs.h" @implementation DrawSeparator @@ -278,14 +265,14 @@ static void DrawRSSIcon(CGRect r, CGColorRef color, BOOL background, BOOL connec } /// Draw RSS icon (with orange gradient, corner @c 0.4, white radio waves) -static void DrawRSSGradientIcon(CGRect r) { +static void DrawRSSGradientIcon(CGRect r, NSColor *color) { const CGFloat size = ShorterSide(r.size); CGContextRef c = NSGraphicsContext.currentContext.CGContext; DrawRoundedFrame(c, r, NSColor.whiteColor.CGColor, YES, 0.4, 1.0, 0.7); // Gradient CGContextSaveGState(c); CGContextClip(c); - DrawGradient(c, size, [NSColor rssOrange]); + DrawGradient(c, size, color); CGContextRestoreGState(c); // Bars AddRSSIconPath(c, size, YES); @@ -297,7 +284,8 @@ static void DrawUnreadIcon(CGRect r, NSColor *color) { CGFloat size = ShorterSide(r.size) / 2.0; CGContextRef c = NSGraphicsContext.currentContext.CGContext; CGMutablePathRef path = CGPathCreateMutable(); - SetContentScale(c, r.size, 0.8); + SetContentScale(c, r.size, 0.7); + CGContextTranslateCTM(c, 0, size * -0.15); // align with baseline of menu item text CGContextSetFillColorWithColor(c, color.CGColor); PathAddRing(path, size, size * 0.7); @@ -325,12 +313,17 @@ static void Register(CGFloat size, NSImageName name, NSString *description, BOOL /// Register all icons that require custom drawing in @c ImageNamed cache void RegisterImageViewNames(void) { const CGColorRef black = [NSColor controlTextColor].CGColor; - Register(16, RSSImageDefaultRSSIcon, NSLocalizedString(@"RSS icon", nil), ^(NSRect r) { DrawRSSGradientIcon(r); return YES; }); + NSColor* const orange = [NSColor colorWithCalibratedRed:251/255.f green:163/255.f blue:58/255.f alpha:1.f]; // #FBA33A + + Register(16, RSSImageDefaultRSSIcon, NSLocalizedString(@"RSS icon", nil), ^(NSRect r) { DrawRSSGradientIcon(r, orange); return YES; }); Register(16, RSSImageSettingsGlobal, NSLocalizedString(@"Global settings", nil), ^(NSRect r) { DrawGlobalIcon(r, black, NO); return YES; }); Register(16, RSSImageSettingsGroup, NSLocalizedString(@"Group settings", nil), ^(NSRect r) { DrawGroupIcon(r, black, NO); return YES; }); Register(16, RSSImageSettingsFeed, NSLocalizedString(@"Feed settings", nil), ^(NSRect r) { DrawRSSIcon(r, black, NO, YES); return YES; }); - Register(16, RSSImageMenuBarIconActive, NSLocalizedString(@"RSS menu bar icon", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor rssOrange].CGColor, YES, YES); return YES; }); - Register(16, RSSImageMenuBarIconPaused, NSLocalizedString(@"RSS menu bar icon, paused", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor rssOrange].CGColor, YES, NO); return YES; }); - Register(12, RSSImageMenuItemUnread, NSLocalizedString(@"Unread icon", nil), ^(NSRect r) { DrawUnreadIcon(r, [NSColor systemBlueColor]); return YES; }); - // TODO: user selected color for rss bar icon & unread dot + + NSColor *c1 = [UserPrefs defaultColor:orange forKey:@"colorStatusIconTint"]; + NSColor *c2 = [UserPrefs defaultColor:[NSColor systemBlueColor] forKey:@"colorUnreadTickMark"]; + + Register(16, RSSImageMenuBarIconActive, NSLocalizedString(@"RSS menu bar icon", nil), ^(NSRect r) { DrawRSSIcon(r, c1.CGColor, YES, YES); return YES; }); + Register(16, RSSImageMenuBarIconPaused, NSLocalizedString(@"RSS menu bar icon, paused", nil), ^(NSRect r) { DrawRSSIcon(r, c1.CGColor, YES, NO); return YES; }); + Register(14, RSSImageMenuItemUnread, NSLocalizedString(@"Unread icon", nil), ^(NSRect r) { DrawUnreadIcon(r, c2); return YES; }); } diff --git a/baRSS/Helper/UserPrefs.h b/baRSS/Helper/UserPrefs.h index 87902d6..d1826ca 100644 --- a/baRSS/Helper/UserPrefs.h +++ b/baRSS/Helper/UserPrefs.h @@ -33,9 +33,10 @@ + (BOOL)openURLsWithPreferredBrowser:(NSArray*)urls; // Hidden Plist Properties -+ (NSUInteger)openFewLinksLimit; // Change with: 'defaults write de.relikd.baRSS openFewLinksLimit -int 10' -+ (NSUInteger)shortArticleNamesLimit; // Change with: 'defaults write de.relikd.baRSS shortArticleNamesLimit -int 50' -+ (NSUInteger)articlesInMenuLimit; // Change with: 'defaults write de.relikd.baRSS articlesInMenuLimit -int 40' ++ (NSUInteger)openFewLinksLimit; // Change with: defaults write de.relikd.baRSS openFewLinksLimit -int 10 ++ (NSUInteger)shortArticleNamesLimit; // Change with: defaults write de.relikd.baRSS shortArticleNamesLimit -int 50 ++ (NSUInteger)articlesInMenuLimit; // Change with: defaults write de.relikd.baRSS articlesInMenuLimit -int 40 ++ (NSColor*)defaultColor:(NSColor*)defaultColor forKey:(NSString*)key; // Change with: defaults write de.relikd.baRSS {KEY} -string "#FBA33A" // Application Info Plist + (NSString*)appName; diff --git a/baRSS/Helper/UserPrefs.m b/baRSS/Helper/UserPrefs.m index c535b7c..42bef45 100644 --- a/baRSS/Helper/UserPrefs.m +++ b/baRSS/Helper/UserPrefs.m @@ -22,6 +22,7 @@ #import "UserPrefs.h" #import "StoreCoordinator.h" +#import "NSString+Ext.h" @implementation UserPrefs @@ -84,6 +85,18 @@ /// Default: @c 40 + (NSUInteger)articlesInMenuLimit { return [self defaultUInt:40 forKey:@"articlesInMenuLimit"]; } +/// @return Returns @c defaultColor if defaults value couldn't be parsed or wasn't modified by user. ++ (NSColor*)defaultColor:(NSColor*)defaultColor forKey:(NSString*)key { + NSString *colorStr = [[NSUserDefaults standardUserDefaults] stringForKey:key]; + if (colorStr) { + NSColor *color = [colorStr hexColor]; + if (color) return color; + NSLog(@"Error reading defaults '%@'. Hex color '%@' is invalid. It should be of the form #RBG or #RRGGBB.", key, colorStr); + [[NSUserDefaults standardUserDefaults] removeObjectForKey:key]; + } + return defaultColor; +} + #pragma mark - Application Info Plist diff --git a/baRSS/Info.plist b/baRSS/Info.plist index fc997f9..6635c69 100644 --- a/baRSS/Info.plist +++ b/baRSS/Info.plist @@ -70,7 +70,7 @@ CFBundleVersion - 13723 + 13931 LSApplicationCategoryType public.app-category.news LSMinimumSystemVersion diff --git a/baRSS/NSCategories/NSError+Ext.h b/baRSS/NSCategories/NSError+Ext.h index 7f8f282..94cd1f8 100644 --- a/baRSS/NSCategories/NSError+Ext.h +++ b/baRSS/NSCategories/NSError+Ext.h @@ -28,8 +28,9 @@ @interface NSError (Ext) // Generators + (instancetype)statusCode:(NSInteger)code reason:(nullable NSString*)reason; -+ (instancetype)canceledByUser; + (instancetype)feedURLNotFound:(NSURL*)url; ++ (instancetype)canceledByUser; +//+ (instancetype)formattingError:(NSString*)description; // User notification - (BOOL)inCaseLog:(nullable const char*)title; - (BOOL)inCasePresent:(NSApplication*)app; diff --git a/baRSS/NSCategories/NSError+Ext.m b/baRSS/NSCategories/NSError+Ext.m index 31f7b40..0fa14e3 100644 --- a/baRSS/NSCategories/NSError+Ext.m +++ b/baRSS/NSCategories/NSError+Ext.m @@ -115,17 +115,23 @@ static const char* CodeDescription(NSInteger code) { return [self errorWithDomain:NSURLErrorDomain code:errCode userInfo:info]; } -/// Generate @c NSError for user canceled operation. With title "Operation canceled.". -+ (instancetype)canceledByUser { - NSDictionary *info = @{ NSLocalizedDescriptionKey: NSLocalizedString(@"Operation canceled.", nil) }; - return [self errorWithDomain:NSURLErrorDomain code:NSURLErrorCancelled userInfo:info]; -} - /// Generate @c NSError for webpages that don't contain feed urls. + (instancetype)feedURLNotFound:(NSURL*)url { return RSXMLMakeErrorWrongParser(RSXMLErrorExpectingFeed, RSXMLErrorExpectingHTML, url); } +/// Generate @c NSError for user canceled operation. With title "Operation was canceled." ++ (instancetype)canceledByUser { + return [NSError errorWithDomain:NSCocoaErrorDomain code:NSUserCancelledError userInfo:nil]; +} + +/*// Generate @c NSError for invalid or malformed input. With title "The value is invalid." ++ (instancetype)formattingError:(NSString*)description { + NSDictionary *info = nil; + if (description) info = @{ NSLocalizedRecoverySuggestionErrorKey: description }; + return [NSError errorWithDomain:NSCocoaErrorDomain code:NSFormattingError userInfo:info]; +}*/ + // --------------------------------------------------------------- // | MARK: - User notification // --------------------------------------------------------------- diff --git a/baRSS/NSCategories/NSString+Ext.h b/baRSS/NSCategories/NSString+Ext.h index 79cb81d..2fad26f 100644 --- a/baRSS/NSCategories/NSString+Ext.h +++ b/baRSS/NSCategories/NSString+Ext.h @@ -22,7 +22,11 @@ @import Cocoa; -@interface NSString (Ext) +@interface NSString (PlainHTML) + (NSString*)plainTextFromHTMLData:(NSData*)data; - (nonnull NSString*)htmlToPlainText; @end + +@interface NSString (HexColor) +- (nullable NSColor*)hexColor; +@end diff --git a/baRSS/NSCategories/NSString+Ext.m b/baRSS/NSCategories/NSString+Ext.m index 4555752..e66e3e9 100644 --- a/baRSS/NSCategories/NSString+Ext.m +++ b/baRSS/NSCategories/NSString+Ext.m @@ -22,7 +22,7 @@ #import "NSString+Ext.h" -@implementation NSString (Ext) +@implementation NSString (PlainHTML) /// Init string with @c NSUTF8StringEncoding and call @c htmlToPlainText + (NSString*)plainTextFromHTMLData:(NSData*)data { @@ -112,3 +112,51 @@ static inline BOOL CLOSE(NSString *tag, NSString *match) { } @end + + + +@implementation NSString (HexColor) + +/** + Color from hex string with format: @c #[0x|0X]([A]RGB|[AA]RRGGBB) + + @return @c nil if string is not properly formatted. + */ +- (nullable NSColor*)hexColor { + if ([self characterAtIndex:0] != '#') // must start with '#' + return nil; + + NSScanner *scanner = [NSScanner scannerWithString:self]; + scanner.scanLocation = 1; + unsigned int value; + if (![scanner scanHexInt:&value]) + return nil; + + NSUInteger len = scanner.scanLocation - 1; // -'#' + if (len > 1 && ([self characterAtIndex:2] == 'x' || [self characterAtIndex:3] == 'X')) + len -= 2; // ignore '0x'RRGGBB + + unsigned int r = 0, g = 0, b = 0, a = 255; + switch (len) { + case 4: // #ARGB + // ignore alpha for now + // a = (value >> 8) & 0xF0; a = a | (a >> 4); + case 3: // #RGB + r = (value >> 4) & 0xF0; r = r | (r >> 4); + g = (value) & 0xF0; g = g | (g >> 4); + b = (value) & 0x0F; b = b | (b << 4); + break; + case 8: // #AARRGGBB + // a = (value >> 24) & 0xFF; + case 6: // #RRGGBB + r = (value >> 16) & 0xFF; + g = (value >> 8) & 0xFF; + b = (value) & 0xFF; + break; + default: + return nil; + } + return [NSColor colorWithCalibratedRed:r/255.f green:g/255.f blue:b/255.f alpha:a/255.f]; +} + +@end