15 Commits

Author SHA1 Message Date
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
relikd
4c9362b42e Fix text color in About tab 2019-10-25 19:48:01 +02:00
relikd
9af191834e Fix menu flickering on macOS 10.15 2019-10-25 15:57:24 +02:00
relikd
8d2e4e4383 Fix preferences on macOS 10.15 2019-10-24 12:40:12 +02:00
relikd
473d4b6057 Update changelog 2019-10-04 12:52:22 +02:00
relikd
cee3780f71 Fix crash on macOS 10.14 2019-10-04 01:50:56 +02:00
relikd
58d7660b87 Deprecation warnings in macOS 10.14 2019-10-04 01:20:39 +02:00
54 changed files with 449 additions and 140 deletions

View File

@@ -7,7 +7,40 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
## [Unreleased]
## [1.0.0] - 2019-10-03
## [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
### Fixed
- Crash on macOS 10.14 due to a `CGColorRef` null pointer
## [1.0.0] 2019-10-03
### Added
- App Signing
- Sandboxing & hardened runtime environment
@@ -64,7 +97,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
@@ -73,7 +106,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
@@ -83,7 +116,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)
@@ -93,7 +126,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)
@@ -111,11 +144,16 @@ 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.0...HEAD
[Unreleased]: https://github.com/relikd/baRSS/compare/v1.1.2...HEAD
[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
[0.9.4]: https://github.com/relikd/baRSS/compare/v0.9.3...v0.9.4
[0.9.3]: https://github.com/relikd/baRSS/compare/v0.9.2...v0.9.3

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

@@ -31,39 +31,6 @@ But it will reuse `ETag` and `Last-Modified` headers to avoid unnecessary transm
Further, tuning the update frequently will decrease the traffic even more.
### Why create something that already existed?
First, open source is awesome!
Secondly, RSS Menu made some design decisions I didn't like.
For example, the new integrated browser window.
One thing I liked most, was the fact that feeds were opened in the default browser.
Not like 99% of the other feed readers on the market that show a separate HTML viewer window.
No rendering issues, no broken links, no content that is different from the actual news article.
I know, the whole purpose of RSS is to deliver content without the need of opening a webpage.
But for me RSS is more about being informed whenever a blog or news feed has some updated content.
E.g, subscribing to video channels without having to have an account.
### 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.
The working alpha version, written in Objective-C, had only 500 Kb.
The reason being that Swift frameworks are always packed into the final application.
Sadly, this was before Swift 5 and ABI stability.
Had I only started the project a year later…
But on the other hand, now it is macOS 10.12 compatible.
### 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).
Download & Install
------------------
@@ -71,19 +38,19 @@ Download & Install
Requires macOS Sierra (10.12) or higher.
### Easy way
Go to [releases](https://github.com/relikd/baRSS/releases) and downloaded the latest version.
Go to [releases](https://github.com/relikd/baRSS/releases) and downloaded the latest version.
Searching for the App Store release? Read this [notice](#app-store-notice).
### Build from source
You'll need Xcode and [Carthage](https://github.com/Carthage/Carthage#installing-carthage).
The latter is optional, you can build the [RSXML2](https://github.com/relikd/RSXML2) library from source instead.
The latter is optional, you can build the [RSXML2] library from source instead.
Carthage just makes it more convenient.
Download and unzip this project, navigate to the root folder and run `carthage bootstrap --platform macOS`.
Next, you need to clone [QLOPML](https://github.com/relikd/QLOPML) in the same folder where this project is.
Alternatively, you can simply delete the `QLOPML` project reference without much harm.
`QLOPML` is a Quick Look plugin for `.opml` files.
It will display the file contents whenever you hit space.
It will display the file contents whenever you hit spacebar.
That's it.
Open Xcode and build the project.
@@ -141,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
@@ -160,9 +127,61 @@ I may postpone some until demand increases …
FAQ / Q&A
---------
### App Store Notice
In the last couple of months I prepared baRSS to be released on the App Store.
With sandboxing enabled and hardened runtime environment, etc.
But, for the time being, I decided to not publish this app for political reasons.
I was not happy about some decisions made in the last weeks.
Decisions that were evaluated on monetary aspects and not on ethical considerations.
I won't support this conduct with my own money.
If you find this app somewhere on the App Store, you can be sure that it is a counterfeit.
As long as you can read this very notice, I am not responsible for the publication.
Further, I can't guarantee the App Store version wasn't modified by a malicious actor to spy on you.
### Why create something that already existed?
First, open source is awesome!
Secondly, RSS Menu made some design decisions I didn't like.
For example, the new integrated browser window.
One thing I liked most, was the fact that feeds were opened in the default browser.
Not like 99% of the other feed readers on the market that show a separate HTML viewer window.
No rendering issues, no broken links, no content that is different from the actual news article.
I know, the whole purpose of RSS is to deliver content without the need of opening a webpage.
But for me RSS is more about being informed whenever a blog or news feed has some updated content.
E.g, subscribing to video channels without having to have an account.
### 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.
The working alpha version, written in Objective-C, had only 500 Kb.
The reason being that Swift frameworks are always packed into the final application.
Sadly, this was before Swift 5 and ABI stability.
Had I only started the project a year later…
But on the other hand, now it is macOS 10.12 compatible.
### 3rd Party Libraries
This project uses a modified version of Brent Simmons' [RSXML](https://github.com/brentsimmons/RSXML) for feed parsing.
[RSXML2] is licensed under a MIT license (same as this project).
##### Trivia
- Start of project: __July 19, 2018__
- Estimated development time: __1940h+__
- Estimated development time: __1963h+__
- First prototype used __feedparser python__ library
[RSXML2]: https://github.com/relikd/RSXML2

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,12 +703,14 @@
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;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;

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*)videoImage:(NSString*)videoid;
+ (NSString*)videoImageHQ:(NSString*)videoid;
@end
NS_ASSUME_NONNULL_END

View File

@@ -80,5 +80,10 @@
return [NSString stringWithFormat:@"http://i.ytimg.com/vi/%@/hqdefault.jpg", videoid];
}
/// @return @c http://i.ytimg.com/vi/<videoid>/maxresdefault.jpg
+ (NSString*)videoImage4k:(NSString*)videoid {
return [NSString stringWithFormat:@"http://i.ytimg.com/vi/%@/maxresdefault.jpg", videoid];
}
@end

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

@@ -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,18 +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) {
const CGColorRef black = [NSColor controlTextColor].CGColor;
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; });
NSColor *c1 = UserPrefsColor(Pref_colorStatusIconTint, orange);
NSColor *c2 = UserPrefsColor(Pref_colorUnreadIndicator, [NSColor systemBlueColor]);
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, 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, [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.0</string>
<string>1.1.2</string>
<key>CFBundleURLTypes</key>
<array>
<dict>
@@ -70,7 +70,7 @@
</dict>
</array>
<key>CFBundleVersion</key>
<string>14360</string>
<string>14633</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.news</string>
<key>LSMinimumSystemVersion</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;
@@ -142,7 +142,7 @@
NSProgressIndicator *spin = [[NSProgressIndicator alloc] initWithFrame: NSMakeRect(0, 0, HEIGHT_SPINNER, HEIGHT_SPINNER)];
spin.indeterminate = YES;
spin.displayedWhenStopped = NO;
spin.style = NSProgressIndicatorSpinningStyle;
spin.style = NSProgressIndicatorStyleSpinning;
spin.controlSize = NSControlSizeSmall;
return spin;
}
@@ -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

@@ -74,7 +74,8 @@
/// Helper method to insert attributed (bold) text
- (void)str:(NSMutableAttributedString*)parent add:(NSString*)text bold:(BOOL)flag {
NSFont *font = [NSFont systemFontOfSize:NSFont.systemFontSize weight:(flag ? NSFontWeightMedium : NSFontWeightLight)];
[parent appendAttributedString:[[NSAttributedString alloc] initWithString:NonLocalized(text) attributes:@{ NSFontAttributeName : font }]];
NSDictionary *style = @{ NSFontAttributeName: font, NSForegroundColorAttributeName: [NSColor controlTextColor] };
[parent appendAttributedString:[[NSAttributedString alloc] initWithString:NonLocalized(text) attributes:style]];
}
/// Helper method to insert attributed hyperlink text

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,23 +23,27 @@
@import Cocoa;
@class ModalFeedEdit;
NS_ASSUME_NONNULL_BEGIN
@interface ModalFeedEditView : NSView
@property (weak) IBOutlet NSTextField *url;
@property (weak) IBOutlet NSProgressIndicator *spinnerURL;
@property (weak) IBOutlet NSImageView *favicon;
@property (strong) IBOutlet NSTextField *url;
@property (strong) IBOutlet NSProgressIndicator *spinnerURL;
@property (strong) IBOutlet NSImageView *favicon;
@property (weak) IBOutlet NSTextField *name;
@property (weak) IBOutlet NSProgressIndicator *spinnerName;
@property (strong) IBOutlet NSTextField *name;
@property (strong) IBOutlet NSProgressIndicator *spinnerName;
@property (weak) IBOutlet NSTextField *refreshNum;
@property (weak) IBOutlet NSPopUpButton *refreshUnit;
@property (strong) IBOutlet NSTextField *refreshNum;
@property (strong) IBOutlet NSPopUpButton *refreshUnit;
@property (weak) IBOutlet NSButton *warningButton;
@property (strong) IBOutlet NSButton *warningButton;
@property NSPopover *warningPopover;
@property (weak) IBOutlet NSTextField *warningText;
@property (weak) IBOutlet NSButton *warningReload;
@property (strong) IBOutlet NSTextField *warningText;
@property (strong) IBOutlet NSButton *warningReload;
- (instancetype)initWithController:(ModalFeedEdit*)controller NS_DESIGNATED_INITIALIZER;
- (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,10 +23,12 @@
@import Cocoa;
@class SettingsFeeds;
NS_ASSUME_NONNULL_BEGIN
@interface SettingsFeedsView : NSView
@property (weak) IBOutlet NSOutlineView *outline;
@property (weak) IBOutlet NSTextField *status;
@property (weak) IBOutlet NSProgressIndicator *spinner;
@property (strong) IBOutlet NSOutlineView *outline;
@property (strong) IBOutlet NSTextField *status;
@property (strong) IBOutlet NSProgressIndicator *spinner;
- (instancetype)initWithController:(SettingsFeeds*)delegate NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
@@ -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,12 +23,15 @@
@import Cocoa;
@class SettingsGeneral;
NS_ASSUME_NONNULL_BEGIN
@interface SettingsGeneralView : NSView
@property (weak) IBOutlet NSPopUpButton* popupHttpApplication;
@property (weak) IBOutlet NSTextField *defaultReader;
@property (strong) IBOutlet NSPopUpButton* popupHttpApplication;
@property (strong) IBOutlet NSTextField *defaultReader;
- (instancetype)initWithController:(SettingsGeneral*)controller NS_DESIGNATED_INITIALIZER;
- (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
@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

@@ -82,11 +82,6 @@
}
}
/// Get rid of everything that is not needed.
- (void)menuDidClose:(NSMenu*)menu {
[menu cleanup];
}
/// Generate items for @c FeedGroup menu.
- (void)setFeedGroups:(NSArray<FeedGroup*>*)sortedList forMenu:(NSMenu*)menu {
[menu insertDefaultHeader];
@@ -125,17 +120,21 @@
Fetch @c Feed from core data and find deepest visible @c NSMenuItem.
@warning @c item and @c feed will often mismatch.
*/
- (void)updateFeedMenuItem:(NSManagedObjectID*)oid withBlock:(void(^)(Feed *feed, NSMenuItem *item))block {
Feed *feed = [[StoreCoordinator getMainContext] objectWithID:oid];
if ([feed isKindOfClass:[Feed class]]) {
NSMenuItem *item = [self.statusItem.mainMenu deepestItemWithPath:feed.indexPath];
if (item) block(feed, item);
}
- (BOOL)findDeepest:(NSManagedObjectID*)oid feed:(Feed*__autoreleasing*)feed menuItem:(NSMenuItem*__autoreleasing*)item {
Feed *f = [[StoreCoordinator getMainContext] objectWithID:oid];
if (![f isKindOfClass:[Feed class]]) return NO;
NSMenuItem *mi = [self.statusItem.mainMenu deepestItemWithPath:f.indexPath];
if (!mi) return NO;
*feed = f;
*item = mi;
return YES;
}
/// Callback method fired when feed has been updated in the background.
- (void)articlesUpdated:(NSNotification*)notify {
[self updateFeedMenuItem:notify.object withBlock:^(Feed *feed, NSMenuItem *item) {
Feed *feed;
NSMenuItem *item;
if ([self findDeepest:notify.object feed:&feed menuItem:&item]) {
// 1. update in-memory unread count
UnreadTotal *updated = [UnreadTotal new];
updated.total = feed.articles.count;
@@ -160,15 +159,17 @@
[item setTitleCount:uct.unread];
item = item.parentItem;
}
}];
}
}
/// Callback method fired when feed icon has changed.
- (void)feedIconUpdated:(NSNotification*)notify {
[self updateFeedMenuItem:notify.object withBlock:^(Feed *feed, NSMenuItem *item) {
Feed *feed;
NSMenuItem *item;
if ([self findDeepest:notify.object feed:&feed menuItem:&item]) {
if (item.submenu.isFeedMenu)
item.image = [feed iconImage16];
}];
}
}
@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;
@@ -44,10 +45,9 @@
self = [super init];
// Show icon & prefetch unread count
self.statusItem = [NSStatusBar.systemStatusBar statusItemWithLength:NSVariableStatusItemLength];
self.statusItem.highlightMode = YES;
self.unreadCountTotal = 0;
self.statusItem.image = [NSImage imageNamed:RSSImageMenuBarIconActive];
self.statusItem.image.template = YES;
self.statusItem.button.image = [NSImage imageNamed:RSSImageMenuBarIconActive];
self.statusItem.button.image.template = YES;
// Add empty menu (will be populated once opened)
self.statusItem.menu = [[NSMenu alloc] initWithTitle:@"M"];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mainMenuWillOpen) name:NSMenuDidBeginTrackingNotification object:self.statusItem.menu];
@@ -119,11 +119,27 @@
dispatch_async(dispatch_get_main_queue(), ^{
BOOL hasNet = [UpdateScheduler allowNetworkConnection];
BOOL tint = (self.unreadCountTotal > 0 && hasNet && UserPrefsBool(Pref_globalTintMenuIcon));
self.statusItem.image = [NSImage imageNamed:(hasNet ? RSSImageMenuBarIconActive : RSSImageMenuBarIconPaused)];
self.statusItem.image.template = !tint;
self.statusItem.button.image = [NSImage imageNamed:(hasNet ? RSSImageMenuBarIconActive : RSSImageMenuBarIconPaused)];
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.title = (showCount ? [NSString stringWithFormat:@"%ld", self.unreadCountTotal] : @"");
self.statusItem.button.title = (showCount ? [NSString stringWithFormat:@"%ld", self.unreadCountTotal] : @"");
self.statusItem.button.imagePosition = (showCount ? NSImageLeft : NSImageOnly);
});
}

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;
@@ -33,7 +35,6 @@
- (NSMenuItem*)insertFeedGroupItem:(FeedGroup*)fg;
- (void)insertDefaultHeader;
// Update menu
- (void)cleanup;
- (void)setHeaderHasUnread:(BOOL)hasUnread hasRead:(BOOL)hasRead;
- (NSMenuItem*)deepestItemWithPath:(nonnull NSString*)path;
@end
@@ -43,3 +44,5 @@
- (instancetype)alternateWithTitle:(NSString*)title;
- (void)setTitleCount:(NSUInteger)count;
@end
NS_ASSUME_NONNULL_END

View File

@@ -104,12 +104,6 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
#pragma mark - Update Menu
/// Replace this menu with a clean @c NSMenu. Copy old @c title and @c delegate to new menu. @b Won't work without supermenu!
- (void)cleanup {
NSMenu *m = [[NSMenu alloc] initWithTitle:self.title];
m.delegate = self.delegate;
self.parentItem.submenu = m;
}
/// Loop over default header and enable 'OpenAllUnread' and 'TagMarkAllRead' based on unread count.
- (void)setHeaderHasUnread:(BOOL)hasUnread hasRead:(BOOL)hasRead {