12 Commits

Author SHA1 Message Date
relikd
e0dec3adf9 Change matching order 2020-12-18 10:59:08 +01:00
relikd
f7eb63bed9 update changelog and bump version 2020-12-18 10:43:13 +01:00
relikd
23f4f125db Support for yt /c/channel-name URLs 2020-12-18 10:37:36 +01:00
relikd
b3940f103a Workaround for issue #7 2020-11-27 17:51:27 +01:00
relikd
239527908f Fix colorUnreadIndicator not being used 2020-11-27 17:42:13 +01:00
relikd
7df70a7936 update changelog 2020-08-31 21:26:44 +02:00
relikd
e8c4c06d33 Fix an issue where indices weren't updated, related to #6 2020-08-31 21:14:13 +02:00
relikd
a9c0e64689 Fix error log 'extractOptions unknown hint identifier' 2020-08-31 20:44:41 +02:00
relikd
51dd688801 Tint with accent color 2020-01-17 22:46:07 +01:00
relikd
352428679d Bump v1.1.0 + QLOPML v1.3 2020-01-17 15:21:47 +01:00
relikd
9b20262207 Menu bar icon tint (10.14+) 2020-01-17 14:07:14 +01:00
relikd
47a0e76cb3 NS_ASSUME_NONNULL 2020-01-17 12:19:08 +01:00
52 changed files with 360 additions and 60 deletions

View File

@@ -8,19 +8,44 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
## [Unreleased]
## [1.0.2] - 2019-10-25
## [1.1.3] 2020-12-18
### Fixed
- Recognize YouTube channel URLs in the format `/c/channel-name`
## [1.1.2] 2020-11-27
### Fixed
- Fixes hidden color option for marking unread entries. Unread menu entries did use `colorStatusIconTint` instead of `colorUnreadIndicator` (thanks @tchek)
- Workaround for not displaying status bar highlight color in macOS 11.0 (issue #7)
## [1.1.1] 2020-08-31
### Fixed
- Feed indices weren't updated properly which resulted in empty feed menus (issue: #6)
## [1.1.0] 2020-01-17
### Added
- *QuickLook*: Thumbnail previews for OPML files (QLOPML v1.3)
- *Status Bar Menu*: Tint menu bar icon with Accent color (macOS 10.14+)
### Fixed
- Resolved Xcode warnings in Xcode 11
## [1.0.2] 2019-10-25
### Fixed
- *Status Bar Menu*: Preferences could not be opened on macOS 10.15
- *Status Bar Menu*: Menu flickering resulting in a hang on macOS 10.15
- *UI*: Text color in `About` tab
## [1.0.1] - 2019-10-04
## [1.0.1] 2019-10-04
### Fixed
- Crash on macOS 10.14 due to a `CGColorRef` null pointer
## [1.0.0] - 2019-10-03
## [1.0.0] 2019-10-03
### Added
- App Signing
- Sandboxing & hardened runtime environment
@@ -77,7 +102,7 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
- *UI:* Mark unread articles with blue dot, instead of tick mark
## [0.9.4] - 2019-04-02
## [0.9.4] 2019-04-02
### Fixed
- Article order got mixed up for some feeds (issue: #4)
- If multiple consecutive items reappear in the middle of the feed mark them read
@@ -86,7 +111,7 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
- *UI:* Removed checkbox `Start on login`. Use Preferences > Users > Login Items instead.
## [0.9.3] - 2019-03-14
## [0.9.3] 2019-03-14
### Added
- Changelog
- *UI:* Show body tag in article tooltip if abstract tag is empty
@@ -96,7 +121,7 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
- Fixed update for feeds where all article URLs point to the same resource (issue: #3)
## [0.9.2] - 2019-03-07
## [0.9.2] 2019-03-07
### Added
- Limit number of articles that are displayed in feed menu (issue: #2)
@@ -106,7 +131,7 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
- libxml2 will ignore lower ascii characters (`0x00``0x1F`)
## [0.9.1] - 2019-02-14
## [0.9.1] 2019-02-14
### Added
- Mark single article as un/read (hold down option key and click on article)
@@ -124,11 +149,15 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
- Remove html tags from abstract on save (not on display)
## [0.9] - 2019-02-11
## [0.9] 2019-02-11
Initial release
[Unreleased]: https://github.com/relikd/baRSS/compare/v1.0.2...HEAD
[Unreleased]: https://github.com/relikd/baRSS/compare/v1.1.3...HEAD
[1.1.3]: https://github.com/relikd/baRSS/compare/v1.1.2...v1.1.3
[1.1.2]: https://github.com/relikd/baRSS/compare/v1.1.1...v1.1.2
[1.1.1]: https://github.com/relikd/baRSS/compare/v1.1.0...v1.1.1
[1.1.0]: https://github.com/relikd/baRSS/compare/v1.0.2...v1.1.0
[1.0.2]: https://github.com/relikd/baRSS/compare/v1.0.1...v1.0.2
[1.0.1]: https://github.com/relikd/baRSS/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/relikd/baRSS/compare/v0.9.4...v1.0.0

View File

@@ -1 +1 @@
github "relikd/RSXML2" "v2.0.0"
github "relikd/RSXML2" "f1a02fabbdece48e3f7835725c65a7589d98782f"

View File

@@ -1 +1 @@
github "relikd/RSXML2" "v2.0.0"
github "relikd/RSXML2" "f1a02fabbdece48e3f7835725c65a7589d98782f"

View File

@@ -108,7 +108,7 @@ ToDo
The following list is not exhaustive but rather a collection of nice things that will be added eventually.
I may postpone some until demand increases …
- [ ] Localizations
- [ ] Localizations
- [ ] Feed generator for websites without feeds
- [ ] Automatically choose best update interval (e.g., avg)
- [ ] Sync with online services
@@ -180,7 +180,7 @@ This project uses a modified version of Brent Simmons' [RSXML](https://github.co
##### Trivia
- Start of project: __July 19, 2018__
- Estimated development time: __1953h+__
- Estimated development time: __1965h+__
- First prototype used __feedparser python__ library

View File

@@ -47,6 +47,7 @@
54BF444A22D0F4F300660096 /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 54BF444922D0F4F300660096 /* AppIcon.icns */; };
54D55D7322E624CD00057B98 /* SettingsFeeds+DragDrop.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D55D7222E624CD00057B98 /* SettingsFeeds+DragDrop.m */; };
54D857CE227C5785001BA1C8 /* RefreshStatisticsView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D857CD227C5785001BA1C8 /* RefreshStatisticsView.m */; };
54DD9F1323D1D6B000B1EAA6 /* NSColor+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54DD9F1223D1D6B000B1EAA6 /* NSColor+Ext.m */; };
54E3C02122EE076D006E2E24 /* opml-icon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 54E3C02022EE076D006E2E24 /* opml-icon.icns */; };
54E4446C2329AE0600BBF481 /* NSError+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E4446B2329AE0600BBF481 /* NSError+Ext.m */; };
54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */ = {isa = PBXBuildFile; fileRef = 54E8831E211B509D00064188 /* ModalFeedEdit.m */; };
@@ -181,6 +182,8 @@
54D857CD227C5785001BA1C8 /* RefreshStatisticsView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RefreshStatisticsView.m; sourceTree = "<group>"; };
54D857D022802309001BA1C8 /* SettingsGeneralView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SettingsGeneralView.h; sourceTree = "<group>"; };
54D857D122802309001BA1C8 /* SettingsGeneralView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SettingsGeneralView.m; sourceTree = "<group>"; };
54DD9F1123D1D6B000B1EAA6 /* NSColor+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSColor+Ext.h"; sourceTree = "<group>"; };
54DD9F1223D1D6B000B1EAA6 /* NSColor+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSColor+Ext.m"; sourceTree = "<group>"; };
54E3C02022EE076D006E2E24 /* opml-icon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = "opml-icon.icns"; sourceTree = "<group>"; };
54E4446A2329AE0600BBF481 /* NSError+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSError+Ext.h"; sourceTree = "<group>"; };
54E4446B2329AE0600BBF481 /* NSError+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSError+Ext.m"; sourceTree = "<group>"; };
@@ -226,10 +229,8 @@
544936F721F1E51E00DEE9AA /* NSCategories */ = {
isa = PBXGroup;
children = (
54B517052270E8C6006C1B29 /* NSView+Ext.h */,
54B517062270E92A006C1B29 /* NSView+Ext.m */,
54AD4E0A2301853D000AE386 /* NSString+Ext.h */,
54AD4E0B2301853D000AE386 /* NSString+Ext.m */,
54DD9F1123D1D6B000B1EAA6 /* NSColor+Ext.h */,
54DD9F1223D1D6B000B1EAA6 /* NSColor+Ext.m */,
54BB048721FD2AB500C303A5 /* NSDate+Ext.h */,
54BB048821FD2AB500C303A5 /* NSDate+Ext.m */,
54E4446A2329AE0600BBF481 /* NSError+Ext.h */,
@@ -238,6 +239,10 @@
548C6D09230C33DE003A1AAF /* NSURL+Ext.m */,
54B6F14C23155E1A002C94C9 /* NSURLRequest+Ext.h */,
54B6F14D23155E1A002C94C9 /* NSURLRequest+Ext.m */,
54AD4E0A2301853D000AE386 /* NSString+Ext.h */,
54AD4E0B2301853D000AE386 /* NSString+Ext.m */,
54B517052270E8C6006C1B29 /* NSView+Ext.h */,
54B517062270E92A006C1B29 /* NSView+Ext.m */,
);
path = NSCategories;
sourceTree = "<group>";
@@ -457,7 +462,7 @@
54ACC27421061B3B0020715F /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 0940;
LastUpgradeCheck = 1200;
ORGANIZATIONNAME = relikd;
TargetAttributes = {
54ACC27B21061B3B0020715F = {
@@ -577,6 +582,7 @@
544B011D2114EE9100386E5C /* AppHook.m in Sources */,
546FC44321189975007CC3A3 /* SettingsGeneral.m in Sources */,
546A6A2922C583390034E806 /* SettingsGeneralView.m in Sources */,
54DD9F1323D1D6B000B1EAA6 /* NSColor+Ext.m in Sources */,
54ACC29521061E270020715F /* UpdateScheduler.m in Sources */,
54BB048921FD2AB500C303A5 /* NSDate+Ext.m in Sources */,
5477D34E21233C62002BA27F /* FeedGroup+Ext.m in Sources */,
@@ -641,6 +647,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
@@ -696,6 +703,7 @@
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1000"
LastUpgradeVersion = "1200"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -27,8 +27,6 @@
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
<MacroExpansion>
<BuildableReference
BuildableIdentifier = "primary"
@@ -38,8 +36,8 @@
ReferencedContainer = "container:baRSS.xcodeproj">
</BuildableReference>
</MacroExpansion>
<AdditionalOptions>
</AdditionalOptions>
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
@@ -49,7 +47,7 @@
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
stopOnEveryMainThreadCheckerIssue = "YES"
migratedStopOnEveryIssue = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
@@ -83,8 +81,6 @@
isEnabled = "NO">
</EnvironmentVariable>
</EnvironmentVariables>
<AdditionalOptions>
</AdditionalOptions>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"

View File

@@ -24,6 +24,8 @@
#import "Feed+CoreDataClass.h"
@class RSParsedFeed;
NS_ASSUME_NONNULL_BEGIN
@interface Feed (Ext)
@property (readonly) BOOL hasIcon;
@property (nonnull, readonly) NSImage* iconImage16;
@@ -38,3 +40,5 @@
// Article properties
- (NSArray<FeedArticle*>*)sortedArticles;
@end
NS_ASSUME_NONNULL_END

View File

@@ -196,7 +196,8 @@
if (self.articles.count == 0) {
img = [NSImage imageNamed:NSImageNameCaution];
} else if (self.hasIcon) {
img = [[NSImage alloc] initByReferencingURL:[self iconPath]];
NSData* data = [[NSData alloc] initWithContentsOfURL:[self iconPath]];
img = [[NSImage alloc] initWithData:data];
} else {
img = [NSImage imageNamed:RSSImageDefaultRSSIcon];
}

View File

@@ -24,7 +24,11 @@
#import "FeedArticle+CoreDataClass.h"
@class RSParsedArticle;
NS_ASSUME_NONNULL_BEGIN
@interface FeedArticle (Ext)
+ (instancetype)newArticle:(RSParsedArticle*)entry inContext:(NSManagedObjectContext*)moc;
- (NSMenuItem*)newMenuItem;
@end
NS_ASSUME_NONNULL_END

View File

@@ -29,6 +29,7 @@ typedef NS_ENUM(int16_t, FeedGroupType) {
GROUP = 0, FEED = 1, SEPARATOR = 2
};
NS_ASSUME_NONNULL_BEGIN
@interface FeedGroup (Ext)
/// Overwrites @c type attribute with enum. Use one of: @c GROUP, @c FEED, @c SEPARATOR.
@@ -39,7 +40,7 @@ typedef NS_ENUM(int16_t, FeedGroupType) {
+ (instancetype)newGroup:(FeedGroupType)type inContext:(NSManagedObjectContext*)context;
+ (instancetype)appendToRoot:(FeedGroupType)type inContext:(NSManagedObjectContext*)moc;
- (void)setParent:(FeedGroup *)parent andSortIndex:(int32_t)sortIndex;
- (void)setParent:(nullable FeedGroup *)parent andSortIndex:(int32_t)sortIndex;
- (void)setSortIndexIfChanged:(int32_t)sortIndex;
- (void)setNameIfChanged:(nullable NSString*)name;
- (NSMenuItem*)newMenuItem;
@@ -50,3 +51,5 @@ typedef NS_ENUM(int16_t, FeedGroupType) {
// Printing
- (NSString*)readableDescription;
@end
NS_ASSUME_NONNULL_END

View File

@@ -78,7 +78,7 @@
}
/// Set @c parent and @c sortIndex. Also if type is @c FEED calculate and set @c indexPath string.
- (void)setParent:(FeedGroup *)parent andSortIndex:(int32_t)sortIndex {
- (void)setParent:(nullable FeedGroup *)parent andSortIndex:(int32_t)sortIndex {
self.parent = parent;
self.sortIndex = sortIndex;
if (self.type == FEED)
@@ -89,10 +89,11 @@
- (void)setSortIndexIfChanged:(int32_t)sortIndex {
if (self.sortIndex != sortIndex) {
self.sortIndex = sortIndex;
[self iterateSorted:NO overDescendantFeeds:^(Feed *feed, BOOL *cancel) {
[feed calculateAndSetIndexPathString];
}];
}
// Otherwise move from 0.0 -> 0 will not trigger index path update
[self iterateSorted:NO overDescendantFeeds:^(Feed *feed, BOOL *cancel) {
[feed calculateAndSetIndexPathString];
}];
}
/// Set @c name attribute but only if value differs.

View File

@@ -25,6 +25,8 @@
static int32_t const kDefaultFeedRefreshInterval = 30 * 60;
NS_ASSUME_NONNULL_BEGIN
@interface FeedMeta (Ext)
+ (instancetype)newMetaInContext:(NSManagedObjectContext*)moc;
// HTTP response
@@ -35,3 +37,5 @@ static int32_t const kDefaultFeedRefreshInterval = 30 * 60;
- (void)setRefreshIfChanged:(int32_t)refresh;
- (void)scheduleNow:(NSTimeInterval)future;
@end
NS_ASSUME_NONNULL_END

View File

@@ -22,6 +22,8 @@
@import Cocoa;
NS_ASSUME_NONNULL_BEGIN
@interface NSFetchRequest<ResultType> (Ext)
// Perform core data request and fetch data
- (NSArray<ResultType>*)fetchAllRows:(NSManagedObjectContext*)moc;
@@ -37,3 +39,5 @@
- (instancetype)sortDESC:(NSString*)key; // add .sortDescriptors -> ascending:NO
- (instancetype)addFunctionExpression:(NSString*)fn onKeyPath:(NSString*)keyPath name:(NSString*)name type:(NSAttributeType)type; // add .propertiesToFetch -> (expressionForFunction:@[expressionForKeyPath:])
@end
NS_ASSUME_NONNULL_END

View File

@@ -23,6 +23,8 @@
@import Cocoa;
#import "DBv1+CoreDataModel.h"
NS_ASSUME_NONNULL_BEGIN
@interface StoreCoordinator : NSObject
// Managing contexts
+ (NSManagedObjectContext*)getMainContext;
@@ -44,7 +46,7 @@
+ (NSArray<NSDictionary*>*)countAggregatedUnread;
// Get List Of Elements
+ (NSArray<FeedGroup*>*)sortedFeedGroupsWithParent:(id)parent inContext:(nullable NSManagedObjectContext*)moc;
+ (NSArray<FeedGroup*>*)sortedFeedGroupsWithParent:(nullable id)parent inContext:(nullable NSManagedObjectContext*)moc;
+ (Feed*)feedWithIndexPath:(nonnull NSString*)path inContext:(nullable NSManagedObjectContext*)moc;
+ (NSString*)urlForFeedWithIndexPath:(nonnull NSString*)path;
@@ -55,3 +57,5 @@
+ (void)cleanupAndShowAlert:(BOOL)flag;
+ (NSUInteger)cleanupFavicons;
@end
NS_ASSUME_NONNULL_END

View File

@@ -144,7 +144,7 @@
@param moc If @c nil perform requests on main context (ok for reading).
@return Sorted list of @c FeedGroup items where @c FeedGroup.parent @c = @c parent.
*/
+ (NSArray<FeedGroup*>*)sortedFeedGroupsWithParent:(id)parent inContext:(nullable NSManagedObjectContext*)moc {
+ (NSArray<FeedGroup*>*)sortedFeedGroupsWithParent:(nullable id)parent inContext:(nullable NSManagedObjectContext*)moc {
return [[[[FeedGroup fetchRequest] where:@"parent = %@", parent] sortASC:@"sortIndex"] fetchAllRows:moc ? moc : [self getMainContext]];
}

View File

@@ -24,9 +24,13 @@
#define ENV_LOG_YOUTUBE 1
NS_ASSUME_NONNULL_BEGIN
// TODO: Make plugins extensible? community extensions.
@interface YouTubePlugin : NSObject
+ (NSString*)feedURL:(NSURL*)url;
+ (NSString*)feedURL:(NSURL*)url data:(NSData*)html;
+ (NSString*)videoImage:(NSString*)videoid;
+ (NSString*)videoImageHQ:(NSString*)videoid;
@end
NS_ASSUME_NONNULL_END

View File

@@ -33,12 +33,13 @@
@return @c nil if @c url is not properly formatted, YouTube feed URL otherwise.
*/
+ (NSString*)feedURL:(NSURL*)url {
+ (NSString*)feedURL:(NSURL*)url data:(NSData*)html {
if (![url.host hasSuffix:@"youtube.com"]) // 'youtu.be' & 'youtube-nocookie.com' will redirect
return nil;
// https://www.youtube.com/channel/[channel-id]
// https://www.youtube.com/user/[user-name]
// https://www.youtube.com/playlist?list=[playlist-id]
// https://www.youtube.com/c/[channel-name]
#if DEBUG && ENV_LOG_YOUTUBE
printf("resolving YouTube url:\n");
printf(" ↳ %s\n", url.absoluteString.UTF8String);
@@ -62,6 +63,23 @@
break;
}
}
} else if ([type isEqualToString:@"c"]) {
NSData *m_head = [@"<meta itemprop=\"channelId\" content=\"" dataUsingEncoding:NSUTF8StringEncoding];
NSRange tmp = [html rangeOfData:m_head options:0 range:NSMakeRange(0, html.length)];
if (tmp.location == NSNotFound) {
NSData *m_json = [@"\"channelId\":\"" dataUsingEncoding:NSUTF8StringEncoding];
tmp = [html rangeOfData:m_json options:0 range:NSMakeRange(0, html.length)];
}
NSUInteger start = tmp.location + tmp.length;
NSUInteger end = html.length - start;
if (end > 50) end = 50; // no need to search till the end
NSString *substr = [[NSString alloc] initWithData:[html subdataWithRange:NSMakeRange(start, end)] encoding:NSUTF8StringEncoding];
if (substr) {
NSUInteger to = [substr rangeOfString:@"\""].location;
if (to != NSNotFound) {
found = [ytBase stringByAppendingFormat:@"?channel_id=%@", [substr substringToIndex:to]];
}
}
}
}
#if DEBUG && ENV_LOG_YOUTUBE

View File

@@ -24,6 +24,8 @@
@class Feed, RSHTMLMetadata, FeedDownload;
@protocol FaviconDownloadDelegate;
NS_ASSUME_NONNULL_BEGIN
@interface FaviconDownload : NSObject
/// @c img and @c path are @c nil if image is not valid or couldn't be downloaded.
typedef void(^FaviconDownloadBlock)(NSImage * _Nullable img, NSURL * _Nullable path);
@@ -45,3 +47,5 @@ typedef void(^FaviconDownloadBlock)(NSImage * _Nullable img, NSURL * _Nullable p
/// Called after image download. Called on error, but not if download is cancled.
- (void)faviconDownload:(FaviconDownload*)sender didFinish:(nullable NSURL*)path;
@end
NS_ASSUME_NONNULL_END

View File

@@ -147,7 +147,13 @@
return;
self.currentDownload = [[NSURLRequest requestWithURL:self.remoteURL] downloadTask:^(NSURL * _Nullable path, NSError * _Nullable error) {
if (error) path = nil; // will also nullify img
NSImage *img = path ? [[NSImage alloc] initByReferencingURL:path] : nil;
NSImage *img;
if (path) {
NSData* data = [[NSData alloc] initWithContentsOfURL:path];
img = [[NSImage alloc] initWithData:data];
} else {
img = nil;
}
if (img.valid) {
// move image to temporary destination, otherwise dataTask: will delete it.
NSString *tmpFile = NSProcessInfo.processInfo.globallyUniqueString;
@@ -166,7 +172,8 @@
if (self.canceled)
return;
NSURL *path = self.fileURL;
NSImage *img = [[NSImage alloc] initByReferencingURL:path];
NSData* data = [[NSData alloc] initWithContentsOfURL:path];
NSImage* img = [[NSImage alloc] initWithData:data];
if (!img.valid) { path = nil; img = nil; }
#if DEBUG && ENV_LOG_DOWNLOAD
printf("ICON %1.0fx%1.0f %s\n", img.size.width, img.size.height, self.remoteURL.absoluteString.UTF8String);

View File

@@ -24,6 +24,8 @@
@class RSParsedFeed, RSHTMLMetadataFeedLink, Feed, FaviconDownload;
@protocol FeedDownloadDelegate;
NS_ASSUME_NONNULL_BEGIN
/**
All properties will be parsed and stored in local variables.
This will avoid unnecessary core data operations if user decides to cancel the edit.
@@ -61,3 +63,5 @@ typedef void (^FeedDownloadBlock)(FeedDownload *sender);
/// Called after xml data is loaded and parsed. Called on error, but not if download is cancled.
- (void)feedDownloadDidFinish:(FeedDownload*)sender;
@end
NS_ASSUME_NONNULL_END

View File

@@ -178,7 +178,7 @@
}
else if (!meta || meta.feedLinks.count == 0) {
if ([xml.url.host hasSuffix:@"youtube.com"])
feedURL = [YouTubePlugin feedURL:xml.url];
feedURL = [YouTubePlugin feedURL:xml.url data:xml.data];
if (feedURL.length == 0)
self.error = [NSError feedURLNotFound:xml.url];
}

View File

@@ -28,6 +28,8 @@ typedef NS_OPTIONS(NSUInteger, OpmlFileExportOptions) {
OpmlFileExportOptionFullBackup = 1 << 2,
};
NS_ASSUME_NONNULL_BEGIN
#pragma mark - Protocols
@protocol OpmlFileImportDelegate <NSObject>
@@ -61,3 +63,5 @@ typedef NS_OPTIONS(NSUInteger, OpmlFileExportOptions) {
- (void)showExportDialog:(NSWindow*)window;
- (nullable NSError*)writeOPMLFile:(NSURL*)url withOptions:(OpmlFileExportOptions)opt;
@end
NS_ASSUME_NONNULL_END

View File

@@ -24,6 +24,8 @@
@class Feed;
NS_ASSUME_NONNULL_BEGIN
@interface UpdateScheduler : NSObject
@property (class, readonly) NSUInteger feedsInQueue;
@property (class, readonly) NSDate *dateScheduled;
@@ -46,3 +48,5 @@
+ (void)registerNetworkChangeNotification;
+ (void)unregisterNetworkChangeNotification;
@end
NS_ASSUME_NONNULL_END

View File

@@ -22,7 +22,7 @@
#import "DrawImage.h"
#import "Constants.h"
#import "UserPrefs.h"
#import "NSColor+Ext.h"
@implementation DrawSeparator
@@ -312,15 +312,11 @@ static void Register(CGFloat size, NSImageName name, NSString *description, BOOL
/// Register all icons that require custom drawing in @c ImageNamed cache
void RegisterImageViewNames(void) {
NSColor *orange = [NSColor colorWithCalibratedRed:251/255.f green:163/255.f blue:58/255.f alpha:1.f]; // #FBA33A
NSColor *c1 = UserPrefsColor(Pref_colorStatusIconTint, orange);
NSColor *c2 = UserPrefsColor(Pref_colorUnreadIndicator, [NSColor systemBlueColor]);
Register(16, RSSImageDefaultRSSIcon, NSLocalizedString(@"RSS icon", nil), ^(NSRect r) { DrawRSSGradientIcon(r, orange); return YES; });
Register(16, RSSImageDefaultRSSIcon, NSLocalizedString(@"RSS icon", nil), ^(NSRect r) { DrawRSSGradientIcon(r, [NSColor rssOrange]); return YES; });
Register(16, RSSImageSettingsGlobal, NSLocalizedString(@"Global settings", nil), ^(NSRect r) { DrawGlobalIcon(r, [NSColor controlTextColor].CGColor, NO); return YES; });
Register(16, RSSImageSettingsGroup, NSLocalizedString(@"Group settings", nil), ^(NSRect r) { DrawGroupIcon(r, [NSColor controlTextColor].CGColor, NO); return YES; });
Register(16, RSSImageSettingsFeed, NSLocalizedString(@"Feed settings", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor controlTextColor].CGColor, NO, YES); return YES; });
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; });
Register(16, RSSImageMenuBarIconActive, NSLocalizedString(@"RSS menu bar icon", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor menuBarIconColor].CGColor, YES, YES); return YES; });
Register(16, RSSImageMenuBarIconPaused, NSLocalizedString(@"RSS menu bar icon, paused", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor menuBarIconColor].CGColor, YES, NO); return YES; });
Register(14, RSSImageMenuItemUnread, NSLocalizedString(@"Unread icon", nil), ^(NSRect r) { DrawUnreadIcon(r, [NSColor unreadIndicatorColor]); return YES; });
}

View File

@@ -45,7 +45,7 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0.2</string>
<string>1.1.3</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
@@ -70,7 +70,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>14471</string>
<string>14644</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.news</string>
<key>LSMinimumSystemVersion</key>
@@ -83,7 +83,7 @@
<true/>
</dict>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2019 relikd. Public Domain.</string>
<string>Copyright © 2020 relikd.</string>
<key>NSPrincipalClass</key>
<string>AppHook</string>
<key>UTImportedTypeDeclarations</key>

View File

@@ -0,0 +1,36 @@
//
// The MIT License (MIT)
// Copyright (c) 2020 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.
@import Cocoa;
NS_ASSUME_NONNULL_BEGIN
@interface NSColor (Ext)
/** @return @c RGB(251,163,58) @c (#FBA33A) */
+ (instancetype)rssOrange;
/** @return User preferred color; default: @c rssOrange */
+ (instancetype)menuBarIconColor;
/** @return User preferred color; default: @c systemBlueColor */
+ (instancetype)unreadIndicatorColor;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,63 @@
//
// The MIT License (MIT)
// Copyright (c) 2020 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.
#import "NSColor+Ext.h"
#import "UserPrefs.h"
@implementation NSColor (Ext)
+ (instancetype)rssOrange {
static NSColor *color;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
color = [NSColor colorWithCalibratedRed:251/255.f green:163/255.f blue:58/255.f alpha:1.f]; // #FBA33A
});
return color;
}
+ (instancetype)menuBarIconColor {
static NSColor *color;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (@available(macOS 10.14, *)) {
color = UserPrefsColor(Pref_colorStatusIconTint, [NSColor controlAccentColor]);
} else {
color = UserPrefsColor(Pref_colorStatusIconTint, [self rssOrange]);
}
});
return color;
}
+ (instancetype)unreadIndicatorColor {
static NSColor *color;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (@available(macOS 10.14, *)) {
color = UserPrefsColor(Pref_colorUnreadIndicator, [NSColor controlAccentColor]);
} else {
color = UserPrefsColor(Pref_colorUnreadIndicator, [NSColor systemBlueColor]);
}
});
return color;
}
@end

View File

@@ -32,6 +32,8 @@ typedef NS_ENUM(int32_t, TimeUnitType) {
TimeUnitYears = 365 * 24 * 60 * 60
};
NS_ASSUME_NONNULL_BEGIN
@interface NSDate (Ext)
+ (NSString*)timeStringISO8601;
+ (NSString*)dayStringISO8601;
@@ -57,3 +59,5 @@ typedef NS_ENUM(int32_t, TimeUnitType) {
@interface NSDate (Statistics)
+ (NSDictionary*)refreshIntervalStatistics:(NSArray<NSDate*> *)list;
@end
NS_ASSUME_NONNULL_END

View File

@@ -25,6 +25,8 @@
/// Log error message and prepend calling class and calling method.
#define NSLogCaller(desc) { NSLog(@"%@:%@ %@", [self class], NSStringFromSelector(_cmd), desc); }
NS_ASSUME_NONNULL_BEGIN
@interface NSError (Ext)
// Generators
+ (instancetype)statusCode:(NSInteger)code reason:(nullable NSString*)reason;
@@ -35,3 +37,5 @@
- (BOOL)inCaseLog:(nullable const char*)title;
- (BOOL)inCasePresent:(NSApplication*)app;
@end
NS_ASSUME_NONNULL_END

View File

@@ -22,6 +22,8 @@
@import Cocoa;
NS_ASSUME_NONNULL_BEGIN
@interface NSString (PlainHTML)
+ (NSString*)plainTextFromHTMLData:(NSData*)data;
- (nonnull NSString*)htmlToPlainText;
@@ -30,3 +32,5 @@
@interface NSString (HexColor)
- (nullable NSColor*)hexColor;
@end
NS_ASSUME_NONNULL_END

View File

@@ -24,6 +24,8 @@
#define ENV_LOG_FILES 0
NS_ASSUME_NONNULL_BEGIN
@interface NSURL (Ext)
// Generators
+ (NSURL*)applicationSupportURL;
@@ -38,3 +40,5 @@
- (void)remove;
- (void)moveTo:(NSURL*)destination;
@end
NS_ASSUME_NONNULL_END

View File

@@ -24,8 +24,12 @@
#define ENV_LOG_DOWNLOAD 1
NS_ASSUME_NONNULL_BEGIN
@interface NSURLRequest (Ext)
+ (instancetype)withURL:(NSString*)urlStr;
- (NSURLSessionDataTask*)dataTask:(nonnull void(^)(NSData * _Nullable data, NSError * _Nullable error, NSHTTPURLResponse *response))block;
- (NSURLSessionDownloadTask*)downloadTask:(void(^)(NSURL * _Nullable path, NSError * _Nullable error))block;
@end
NS_ASSUME_NONNULL_END

View File

@@ -40,6 +40,8 @@
/// Static variable to calculate origin center coordinate in its @c superview. The value of this var isn't used.
static CGFloat const CENTER = -0.015625;
NS_ASSUME_NONNULL_BEGIN
/// Calculate @c origin.y going down from the top border of its @c superview
static inline CGFloat YFromTop(NSView *view) { return NSHeight(view.superview.frame) - NSMinY(view.frame) - view.alignmentRectInsets.bottom; }
/// @c MAX()
@@ -65,7 +67,7 @@ static inline CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.fr
+ (NSButton*)inlineButton:(NSString*)text;
+ (NSPopUpButton*)popupButton:(CGFloat)w;
// UI: Others
+ (NSImageView*)imageView:(NSImageName)name size:(CGFloat)size;
+ (NSImageView*)imageView:(nullable NSImageName)name size:(CGFloat)size;
+ (NSButton*)checkbox:(BOOL)flag;
+ (NSProgressIndicator*)activitySpinner;
+ (NSView*)radioGroup:(NSArray<NSString*>*)entries target:(id)target action:(nonnull SEL)action;
@@ -91,7 +93,7 @@ static inline CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.fr
@interface NSControl (Ext)
- (instancetype)action:(SEL)selector target:(id)target;
- (instancetype)action:(SEL)selector target:(nullable id)target;
- (instancetype)large;
- (instancetype)small;
- (instancetype)tiny;
@@ -106,3 +108,5 @@ static inline CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.fr
- (instancetype)selectable;
- (instancetype)multiline:(NSSize)size;
@end
NS_ASSUME_NONNULL_END

View File

@@ -122,7 +122,7 @@
/// Create @c ImageView with square @c size
+ (NSImageView*)imageView:(NSImageName)name size:(CGFloat)size {
+ (NSImageView*)imageView:(nullable NSImageName)name size:(CGFloat)size {
NSImageView *imgView = [[NSImageView alloc] initWithFrame: NSMakeRect(0, 0, size, size)];
if (name) imgView.image = [NSImage imageNamed:name];
return imgView;
@@ -328,7 +328,7 @@ static inline void SetFrameWidth(NSView *view, CGFloat w) {
@implementation NSControl (Ext)
/// Set @c target and @c action simultaneously
- (instancetype)action:(SEL)selector target:(id)target {
- (instancetype)action:(SEL)selector target:(nullable id)target {
self.action = selector;
self.target = target;
return self;

View File

@@ -22,6 +22,10 @@
@import Cocoa;
NS_ASSUME_NONNULL_BEGIN
@interface SettingsAppearance : NSViewController
- (void)didSelectCheckbox:(NSButton*)sender;
@end
NS_ASSUME_NONNULL_END

View File

@@ -24,6 +24,8 @@
#import "ModalSheet.h"
@class FeedGroup, ModalSheet;
NS_ASSUME_NONNULL_BEGIN
@interface ModalEditDialog : NSViewController
+ (instancetype)modalWith:(FeedGroup*)group;
- (ModalSheet*)getModalSheet;
@@ -38,3 +40,4 @@
@interface ModalGroupEdit : ModalEditDialog
@end
NS_ASSUME_NONNULL_END

View File

@@ -216,7 +216,13 @@
*/
- (void)faviconDownload:(FaviconDownload*)sender didFinish:(nullable NSURL*)path {
// Create image from favicon temporary file location or default icon if no favicon exists.
NSImage *img = path ? [[NSImage alloc] initByReferencingURL:path] : [NSImage imageNamed:RSSImageDefaultRSSIcon];
NSImage *img;
if (path) {
NSData* data = [[NSData alloc] initWithContentsOfURL:path];
img = [[NSImage alloc] initWithData:data];
} else {
img = [NSImage imageNamed:RSSImageDefaultRSSIcon];
}
self.view.favicon.image = img;
self.faviconFile = path;
[self downloadComplete];

View File

@@ -23,6 +23,8 @@
@import Cocoa;
@class ModalFeedEdit;
NS_ASSUME_NONNULL_BEGIN
@interface ModalFeedEditView : NSView
@property (strong) IBOutlet NSTextField *url;
@property (strong) IBOutlet NSProgressIndicator *spinnerURL;
@@ -43,3 +45,5 @@
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
- (nullable instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@@ -22,6 +22,8 @@
@import Cocoa;
NS_ASSUME_NONNULL_BEGIN
@protocol RefreshIntervalButtonDelegate <NSObject>
@required
/// @c sender.tag is refresh interval in seconds
@@ -34,3 +36,5 @@
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
- (nullable instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@@ -23,7 +23,11 @@
#import "SettingsFeeds.h"
#import "OpmlFile.h"
NS_ASSUME_NONNULL_BEGIN
@interface SettingsFeeds (DragDrop) <NSOutlineViewDataSource, NSFilePromiseProviderDelegate, NSPasteboardTypeOwner, OpmlFileImportDelegate, OpmlFileExportDelegate>
- (void)prepareOutlineViewForDragDrop:(NSOutlineView*)outline;
- (void)importOpmlFiles:(NSArray<NSURL*>*)files;
@end
NS_ASSUME_NONNULL_END

View File

@@ -22,10 +22,12 @@
@import Cocoa;
NS_ASSUME_NONNULL_BEGIN
/** Manages the NSOutlineView and Feed creation and editing */
@interface SettingsFeeds : NSViewController <NSOutlineViewDelegate>
@property (strong) NSTreeController *dataStore;
@property (strong) NSArray<NSTreeNode*> *currentlyDraggedNodes;
@property (strong, nullable) NSArray<NSTreeNode*> *currentlyDraggedNodes;
- (void)editSelectedItem;
- (void)doubleClickOutlineView:(NSOutlineView*)sender;
@@ -40,3 +42,5 @@
- (BOOL)endCoreDataChangeUndoEmpty:(BOOL)undoEmpty forceUndo:(BOOL)force;
- (void)restoreOrderingAndIndexPathStr:(NSArray<NSTreeNode*>*)parentsList;
@end
NS_ASSUME_NONNULL_END

View File

@@ -23,6 +23,8 @@
@import Cocoa;
@class SettingsFeeds;
NS_ASSUME_NONNULL_BEGIN
@interface SettingsFeedsView : NSView
@property (strong) IBOutlet NSOutlineView *outline;
@property (strong) IBOutlet NSTextField *status;
@@ -45,3 +47,5 @@ extern NSUserInterfaceItemIdentifier const CustomCellRefresh;
@interface SeparatorColumnCell : NSTableCellView
extern NSUserInterfaceItemIdentifier const CustomCellSeparator;
@end
NS_ASSUME_NONNULL_END

View File

@@ -22,7 +22,11 @@
@import Cocoa;
NS_ASSUME_NONNULL_BEGIN
@interface SettingsGeneral : NSViewController
- (void)changeHttpApplication:(NSPopUpButton *)sender;
- (void)clickHowToDefaults:(NSButton *)sender;
@end
NS_ASSUME_NONNULL_END

View File

@@ -23,6 +23,8 @@
@import Cocoa;
@class SettingsGeneral;
NS_ASSUME_NONNULL_BEGIN
@interface SettingsGeneralView : NSView
@property (strong) IBOutlet NSPopUpButton* popupHttpApplication;
@property (strong) IBOutlet NSTextField *defaultReader;
@@ -32,3 +34,4 @@
- (nullable instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
@end
NS_ASSUME_NONNULL_END

View File

@@ -22,6 +22,8 @@
@import Cocoa;
NS_ASSUME_NONNULL_BEGIN
@interface ModalSheet : NSPanel
@property (readonly) BOOL didTapCancel;
@@ -31,3 +33,5 @@
- (void)setDoneEnabled:(BOOL)accept;
- (void)extendContentViewBy:(CGFloat)dy;
@end
NS_ASSUME_NONNULL_END

View File

@@ -44,7 +44,7 @@
[content setFrameSize: NSMakeSize(w, h)];
// after content size, increase to window size
w += 2 * PAD_WIN;
w += 2 * (NSInteger)PAD_WIN;
h += PAD_WIN + contentOffsetY; // the second PAD_WIN is already in contentOffsetY
NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | NSWindowStyleMaskFullSizeContentView;

View File

@@ -23,7 +23,11 @@
@import Cocoa;
@class SettingsFeeds;
NS_ASSUME_NONNULL_BEGIN
@interface Preferences : NSWindow <NSWindowDelegate>
+ (instancetype)window;
- (__kindof NSViewController*)selectTab:(NSUInteger)index;
@end
NS_ASSUME_NONNULL_END

View File

@@ -23,7 +23,11 @@
@import Cocoa;
@class BarStatusItem;
NS_ASSUME_NONNULL_BEGIN
@interface BarMenu : NSObject <NSMenuDelegate>
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithStatusItem:(BarStatusItem*)statusItem NS_DESIGNATED_INITIALIZER;
@end
NS_ASSUME_NONNULL_END

View File

@@ -22,6 +22,8 @@
@import Cocoa;
NS_ASSUME_NONNULL_BEGIN
@interface BarStatusItem : NSObject
@property (weak, readonly) NSMenu *mainMenu;
@@ -32,3 +34,4 @@
- (void)showWelcomeMessage;
@end
NS_ASSUME_NONNULL_END

View File

@@ -28,6 +28,7 @@
#import "BarMenu.h"
#import "AppHook.h"
#import "NSView+Ext.h"
#import "NSColor+Ext.h"
@interface BarStatusItem()
@property (strong) BarMenu *barMenu;
@@ -119,8 +120,22 @@
BOOL hasNet = [UpdateScheduler allowNetworkConnection];
BOOL tint = (self.unreadCountTotal > 0 && hasNet && UserPrefsBool(Pref_globalTintMenuIcon));
self.statusItem.button.image = [NSImage imageNamed:(hasNet ? RSSImageMenuBarIconActive : RSSImageMenuBarIconPaused)];
self.statusItem.button.image.template = !tint;
// TODO: use macOS 10.14 contentTintColor, if (@available(macOS 10.14, *)) {} else {}
if (@available(macOS 11, *)) {
self.statusItem.button.image.template = !tint;
} else if (@available(macOS 10.14, *)) {
// There is no proper way to display tinted icon WITHOUT tinted text!
// - using alternate image instead of tint:
// icon & text stays black on highlight (but only in light mode)
// - using tint and attributed titles:
// with controlTextColor the tint is applied regardless
// with controlColor the color doesnt match (either normal or on highlight)
// also, setting attributed title kills tint on icon
self.statusItem.button.image.template = YES;
self.statusItem.button.contentTintColor = tint ? [NSColor menuBarIconColor] : nil;
} else {
self.statusItem.button.image.template = !tint;
}
BOOL showCount = (self.unreadCountTotal > 0 && UserPrefsBool(Pref_globalUnreadCount));
self.statusItem.button.title = (showCount ? [NSString stringWithFormat:@"%ld", self.unreadCountTotal] : @"");

View File

@@ -22,6 +22,8 @@
@import Cocoa;
NS_ASSUME_NONNULL_BEGIN
@interface UnreadTotal : NSObject
@property (nonatomic, assign) NSUInteger unread;
@property (nonatomic, assign) NSUInteger total;
@@ -39,3 +41,5 @@
- (UnreadTotal*)objectForKeyedSubscript:(NSString*)key;
- (void)setObject:(UnreadTotal*)obj forKeyedSubscript:(NSString*)key;
@end
NS_ASSUME_NONNULL_END

View File

@@ -23,6 +23,8 @@
@import Cocoa;
@class FeedGroup;
NS_ASSUME_NONNULL_BEGIN
@interface NSMenu (Ext)
@property (nonnull, copy, readonly) NSString *titleIndexPath;
@property (nullable, readonly) NSMenuItem* parentItem;
@@ -42,3 +44,5 @@
- (instancetype)alternateWithTitle:(NSString*)title;
- (void)setTitleCount:(NSUInteger)count;
@end
NS_ASSUME_NONNULL_END