Compare commits
50 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5894b12c1d | ||
|
|
0700eebb13 | ||
|
|
4c4a133fe2 | ||
|
|
ccca329630 | ||
|
|
831159904c | ||
|
|
cf3e9e4b4a | ||
|
|
184e5c0882 | ||
|
|
575d1eaec8 | ||
|
|
0c481d18dd | ||
|
|
c281573044 | ||
|
|
d164c6bcb0 | ||
|
|
9f4de8fc8d | ||
|
|
c099c32cca | ||
|
|
bdf9d11853 | ||
|
|
c14af92289 | ||
|
|
b6978662fc | ||
|
|
89f90ddb11 | ||
|
|
0b6a338fa3 | ||
|
|
3235bffdca | ||
|
|
0a23819428 | ||
|
|
def174c65f | ||
|
|
e63d6c5784 | ||
|
|
46fa898807 | ||
|
|
63509faef6 | ||
|
|
7047d99205 | ||
|
|
614e4abb50 | ||
|
|
20835cd155 | ||
|
|
ba76f6a206 | ||
|
|
256fd55d32 | ||
|
|
4eb2248142 | ||
|
|
6ef23ef599 | ||
|
|
f65c5b9546 | ||
|
|
9c3814b470 | ||
|
|
131bfaa14d | ||
|
|
fc6c3a3df2 | ||
|
|
f2bdc5b555 | ||
|
|
060f538240 | ||
|
|
5eed090e9c | ||
|
|
f7872c4f80 | ||
|
|
0fdb8d9ccc | ||
|
|
5d7242cc73 | ||
|
|
b846319335 | ||
|
|
82e9365272 | ||
|
|
839eee7d39 | ||
|
|
f577ec1ec2 | ||
|
|
df0b5b1c91 | ||
|
|
86f5abde0c | ||
|
|
02759ba0be | ||
|
|
3189015ce1 | ||
|
|
6cf86d3bf8 |
96
CHANGELOG.md
96
CHANGELOG.md
@@ -5,7 +5,64 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
|
||||
## [Unreleased]
|
||||
## [1.5.3] – 2025-10-29
|
||||
### Fixed
|
||||
- *Notifications:* Use user-provided feed title instead of server provided title
|
||||
|
||||
|
||||
## [1.5.2] – 2025-10-29
|
||||
### Added
|
||||
- *Notifications:* Reply with "Open in background", "Mark read & dismiss", or "Open but keep unread"
|
||||
|
||||
|
||||
## [1.5.1] – 2025-10-27
|
||||
### Fixed
|
||||
- *Status Bar Menu:* Simplified options for "Show only unread"
|
||||
|
||||
|
||||
## [1.5.0] – 2025-10-27
|
||||
### Added
|
||||
- *UI:* Notifications
|
||||
|
||||
|
||||
## [1.4.1] – 2025-07-29
|
||||
### Fixed
|
||||
- Re-compiled because previous certificate was revoked (again!)
|
||||
|
||||
|
||||
## [1.4.0] – 2025-07-23
|
||||
### Added
|
||||
- *QuickLook:* Updated to new extension framework
|
||||
|
||||
|
||||
## [1.3.2] – 2025-07-23
|
||||
### Fixed
|
||||
- Previous version did not run on macOS 10.15
|
||||
|
||||
|
||||
## [1.3.1] – 2025-07-21
|
||||
### Fixed
|
||||
- *Status Bar Menu:* Always recreate main menu (hopefully fixes #13)
|
||||
- *Status Bar Menu:* Enable global mark read menu items on background update
|
||||
- *Status Bar Menu:* Keyboard navigation over alternate items ("Open a few") (fixes #15)
|
||||
- *Status Bar Menu:* Alternate item ("Open a few") was displayed as normal menu item in macOS 15
|
||||
- *UI:* Welcome message was displayed at the bottom left corner
|
||||
- *UI:* Tooltip will not remove preceding whitespace if html starts with a list
|
||||
- Update Xcode build flags
|
||||
|
||||
|
||||
## [1.3.0] – 2025-06-24
|
||||
### Added
|
||||
- *Adding feed:* Regex Converter for websites without RSS feed (hold down option key during edit)
|
||||
|
||||
### Fixed
|
||||
- *Adding feed:* Keep aspect ratio of favicon inside button (related to fix in v1.2.3)
|
||||
|
||||
|
||||
## [1.2.3] – 2025-06-09
|
||||
### Fixed
|
||||
- *Adding feed:* Favicon size inside button
|
||||
- *DB:* Feeds with changing urls -> use guid for unique check
|
||||
|
||||
|
||||
## [1.2.2] – 2023-06-18
|
||||
@@ -25,7 +82,7 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
|
||||
|
||||
## [1.2.0] – 2022-10-01
|
||||
### Added
|
||||
- *UI*: Add option to hide read articles (show only unread)
|
||||
- *UI:* Add option to hide read articles (show only unread)
|
||||
|
||||
|
||||
## [1.1.3] – 2020-12-18
|
||||
@@ -46,8 +103,8 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
|
||||
|
||||
## [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+)
|
||||
- *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
|
||||
@@ -55,9 +112,9 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
|
||||
|
||||
## [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
|
||||
- *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
|
||||
@@ -100,7 +157,7 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
|
||||
- *Settings, Feeds:* Status info with accurate download count (instead of `Updating feeds …`)
|
||||
- *Settings, Feeds:* Status info shows `No network connection` and `Updates paused`
|
||||
- *Settings, Feeds:* After feed edit, run update scheduler immediately
|
||||
- *Status Bar Menu*: Feed title is updated properly
|
||||
- *Status Bar Menu:* Feed title is updated properly
|
||||
- *UI:* If an error occurs, show document URL (path to file or web url)
|
||||
- Comparison of existing articles with nonexistent guid and link
|
||||
- Don't mark articles read if opening URLs failed
|
||||
@@ -112,12 +169,12 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
|
||||
- *Adding feed:* Refresh interval hotkeys set to: `⌘1` … `⌘6`
|
||||
- *Settings, Feeds:* Single add button for feeds, groups, and separators
|
||||
- *Settings, Feeds:* Always append new items at the end
|
||||
- *Settings, General*: Moved `Fix cache` button to `About` text section
|
||||
- *Settings, General*: Changing default feed reader is prohibited within sandbox
|
||||
- *Settings, General*: [Auxiliary application](https://github.com/relikd/URL-Scheme-Defaults) for changing default feed reader
|
||||
- *Status Bar Menu*: Show `(no title)` instead of `(error)`
|
||||
- *Status Bar Menu*: `Update all feeds` will show error alert for broken URLs
|
||||
- *DB*: Dropping table `FeedIcon` in favor of image files cache
|
||||
- *Settings, General:* Moved `Fix cache` button to `About` text section
|
||||
- *Settings, General:* Changing default feed reader is prohibited within sandbox
|
||||
- *Settings, General:* [Auxiliary application](https://github.com/relikd/URL-Scheme-Defaults) for changing default feed reader
|
||||
- *Status Bar Menu:* Show `(no title)` instead of `(error)`
|
||||
- *Status Bar Menu:* `Update all feeds` will show error alert for broken URLs
|
||||
- *DB:* Dropping table `FeedIcon` in favor of image files cache
|
||||
- *UI:* Interface builder files replaced with code equivalent
|
||||
- *UI:* Mark unread articles with blue dot, instead of tick mark
|
||||
|
||||
@@ -173,7 +230,16 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
|
||||
Initial release
|
||||
|
||||
|
||||
[Unreleased]: https://github.com/relikd/baRSS/compare/v1.2.2...HEAD
|
||||
[1.5.3]: https://github.com/relikd/baRSS/compare/v1.5.2...v1.5.3
|
||||
[1.5.2]: https://github.com/relikd/baRSS/compare/v1.5.1...v1.5.2
|
||||
[1.5.1]: https://github.com/relikd/baRSS/compare/v1.5.0...v1.5.1
|
||||
[1.5.0]: https://github.com/relikd/baRSS/compare/v1.4.1...v1.5.0
|
||||
[1.4.1]: https://github.com/relikd/baRSS/compare/v1.4.0...v1.4.1
|
||||
[1.4.0]: https://github.com/relikd/baRSS/compare/v1.3.2...v1.4.0
|
||||
[1.3.2]: https://github.com/relikd/baRSS/compare/v1.3.1...v1.3.2
|
||||
[1.3.1]: https://github.com/relikd/baRSS/compare/v1.3.0...v1.3.1
|
||||
[1.3.0]: https://github.com/relikd/baRSS/compare/v1.2.3...v1.3.0
|
||||
[1.2.3]: https://github.com/relikd/baRSS/compare/v1.2.2...v1.2.3
|
||||
[1.2.2]: https://github.com/relikd/baRSS/compare/v1.2.1...v1.2.2
|
||||
[1.2.1]: https://github.com/relikd/baRSS/compare/v1.2.0...v1.2.1
|
||||
[1.2.0]: https://github.com/relikd/baRSS/compare/v1.1.3...v1.2.0
|
||||
|
||||
21
QLOPML/Base.lproj/PreviewViewController.xib
Normal file
21
QLOPML/Base.lproj/PreviewViewController.xib
Normal file
@@ -0,0 +1,21 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="11762" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="11762"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="PreviewViewController" customModuleProvider="">
|
||||
<connections>
|
||||
<outlet property="view" destination="c22-O7-iKe" id="NRM-P4-wb6"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<customView id="c22-O7-iKe" userLabel="Preview View">
|
||||
<rect key="frame" x="0.0" y="0.0" width="480" height="272"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMinY="YES"/>
|
||||
</customView>
|
||||
</objects>
|
||||
</document>
|
||||
44
QLOPML/Info.plist
Normal file
44
QLOPML/Info.plist
Normal file
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>QLOPML</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>$(EXECUTABLE_NAME)</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>$(MACOSX_DEPLOYMENT_TARGET)</string>
|
||||
<key>NSExtension</key>
|
||||
<dict>
|
||||
<key>NSExtensionAttributes</key>
|
||||
<dict>
|
||||
<key>QLSupportedContentTypes</key>
|
||||
<array>
|
||||
<string>org.opml.opml</string>
|
||||
</array>
|
||||
<key>QLSupportsSearchableItems</key>
|
||||
<false/>
|
||||
</dict>
|
||||
<key>NSExtensionPointIdentifier</key>
|
||||
<string>com.apple.quicklook.preview</string>
|
||||
<key>NSExtensionPrincipalClass</key>
|
||||
<string>PreviewViewController</string>
|
||||
</dict>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2025 relikd.</string>
|
||||
</dict>
|
||||
</plist>
|
||||
5
QLOPML/PreviewViewController.h
Normal file
5
QLOPML/PreviewViewController.h
Normal file
@@ -0,0 +1,5 @@
|
||||
#import <Cocoa/Cocoa.h>
|
||||
|
||||
@interface PreviewViewController : NSViewController
|
||||
|
||||
@end
|
||||
29
QLOPML/PreviewViewController.m
Normal file
29
QLOPML/PreviewViewController.m
Normal file
@@ -0,0 +1,29 @@
|
||||
#import "PreviewViewController.h"
|
||||
#import <Quartz/Quartz.h>
|
||||
#import <WebKit/WebKit.h>
|
||||
#include "opml-lib.h"
|
||||
|
||||
@interface PreviewViewController () <QLPreviewingController>
|
||||
@end
|
||||
|
||||
@implementation PreviewViewController
|
||||
|
||||
- (NSString *)nibName {
|
||||
return @"PreviewViewController";
|
||||
}
|
||||
|
||||
- (void)preparePreviewOfFileAtURL:(NSURL *)url completionHandler:(void (^)(NSError * _Nullable))handler {
|
||||
NSData *data = generateHTMLData(url, [NSBundle mainBundle], NO);
|
||||
// sure, we could use `WKWebView`, but that requires the `com.apple.security.network.client` entitlement
|
||||
#pragma clang diagnostic ignored "-Wdeprecated"
|
||||
WebView *web = [[WebView alloc] initWithFrame:self.view.bounds];
|
||||
#pragma clang diagnostic pop
|
||||
web.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
[self.view addSubview:web];
|
||||
// [web.mainFrame loadHTMLString:html baseURL:nil];
|
||||
[web.mainFrame loadData:data MIMEType:@"text/html" textEncodingName:@"UTF-8" baseURL:nil];
|
||||
handler(nil);
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
10
QLOPML/QLOPML.entitlements
Normal file
10
QLOPML/QLOPML.entitlements
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.files.user-selected.read-only</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
7
QLOPML/opml-lib.h
Normal file
7
QLOPML/opml-lib.h
Normal file
@@ -0,0 +1,7 @@
|
||||
#ifndef opml_lib_h
|
||||
#define opml_lib_h
|
||||
|
||||
NSData* generateHTMLData(NSURL *url, NSBundle *bundle, BOOL thumb);
|
||||
//void renderThumbnail(CFURLRef url, CFBundleRef bundle, CGContextRef context, CGSize maxSize);
|
||||
|
||||
#endif /* opml_lib_h */
|
||||
116
QLOPML/opml-lib.m
Normal file
116
QLOPML/opml-lib.m
Normal file
@@ -0,0 +1,116 @@
|
||||
#import <Foundation/Foundation.h>
|
||||
#import <AppKit/AppKit.h>
|
||||
//#import <WebKit/WebKit.h>
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// |
|
||||
// | OPML renderer
|
||||
// |
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
NSXMLElement* make(NSString *tag, NSString *text, NSXMLElement *parent) {
|
||||
NSXMLElement *div = [NSXMLElement elementWithName:tag];
|
||||
if (text) div.stringValue = text;
|
||||
[parent addChild:div];
|
||||
return div;
|
||||
}
|
||||
|
||||
void attribute(NSXMLElement *parent, NSString *key, NSString *value) {
|
||||
[parent addAttribute:[NSXMLElement attributeWithName:key stringValue:value]];
|
||||
}
|
||||
|
||||
NSXMLElement* section(NSString *title, NSString *container, NSXMLElement *parent) {
|
||||
make(@"h3", title, parent);
|
||||
NSXMLElement *div = make(container, nil, parent);
|
||||
attribute(div, @"class", @"section");
|
||||
return div;
|
||||
}
|
||||
|
||||
void appendNode(NSXMLElement *child, NSXMLElement *parent, Boolean thumb) {
|
||||
|
||||
if ([child.name isEqualToString:@"head"]) {
|
||||
if (thumb)
|
||||
return;
|
||||
NSXMLElement *dl = section(@"Metadata:", @"dl", parent);
|
||||
for (NSXMLElement *head in child.children) {
|
||||
make(@"dt", head.name, dl);
|
||||
make(@"dd", head.stringValue, dl);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if ([child.name isEqualToString:@"body"]) {
|
||||
parent = thumb ? make(@"ul", nil, parent) : section(@"Content:", @"ul", parent);
|
||||
|
||||
} else if ([child.name isEqualToString:@"outline"]) {
|
||||
if ([child attributeForName:@"separator"].stringValue) {
|
||||
make(@"hr", nil, parent);
|
||||
} else {
|
||||
NSString *desc = [child attributeForName:@"title"].stringValue;
|
||||
if (!desc || desc.length == 0)
|
||||
desc = [child attributeForName:@"text"].stringValue;
|
||||
// refreshInterval
|
||||
NSXMLElement *li = make(@"li", desc, parent);
|
||||
if (!thumb) {
|
||||
NSString *xmlUrl = [child attributeForName:@"xmlUrl"].stringValue;
|
||||
if (xmlUrl && xmlUrl.length > 0) {
|
||||
[li addChild:[NSXMLNode textWithStringValue:@" — "]];
|
||||
attribute(make(@"a", xmlUrl, li), @"href", xmlUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (child.childCount > 0) {
|
||||
parent = make(@"ul", nil, parent);
|
||||
}
|
||||
}
|
||||
for (NSXMLElement *c in child.children) {
|
||||
appendNode(c, parent, thumb);
|
||||
}
|
||||
}
|
||||
|
||||
NSData* generateHTMLData(NSURL *url, NSBundle *bundle, BOOL thumb) {
|
||||
NSError *err;
|
||||
NSXMLDocument *doc = [[NSXMLDocument alloc] initWithContentsOfURL:url options:0 error:&err];
|
||||
if (err || !doc) {
|
||||
printf("ERROR: %s\n", err.description.UTF8String);
|
||||
return nil;
|
||||
}
|
||||
|
||||
NSXMLElement *html = [NSXMLElement elementWithName:@"html"];
|
||||
NSXMLElement *head = make(@"head", nil, html);
|
||||
make(@"title", @"OPML file", head);
|
||||
|
||||
NSString *cssPath = [bundle pathForResource:thumb ? @"style-thumb" : @"style" ofType:@"css"];
|
||||
NSString *data = [NSString stringWithContentsOfFile:cssPath encoding:NSUTF8StringEncoding error:nil];
|
||||
make(@"style", data, head);
|
||||
|
||||
NSXMLElement *body = make(@"body", nil, html);
|
||||
|
||||
for (NSXMLElement *child in doc.children) {
|
||||
appendNode(child, body, thumb);
|
||||
}
|
||||
NSXMLDocument *xml = [NSXMLDocument documentWithRootElement:html];
|
||||
return [xml XMLDataWithOptions:NSXMLNodePrettyPrint | NSXMLNodeCompactEmptyElement];
|
||||
}
|
||||
|
||||
|
||||
/*void renderThumbnail(CFURLRef url, CFBundleRef bundle, CGContextRef context, CGSize maxSize) {
|
||||
NSData *data = generateHTMLData((__bridge NSURL*)url, bundle, true);
|
||||
if (data) {
|
||||
CGRect rect = CGRectMake(0, 0, 600, 800);
|
||||
float scale = maxSize.height / rect.size.height;
|
||||
|
||||
WebView *webView = [[WebView alloc] initWithFrame:rect];
|
||||
[webView.mainFrame.frameView scaleUnitSquareToSize:CGSizeMake(scale, scale)];
|
||||
[webView.mainFrame.frameView setAllowsScrolling:NO];
|
||||
[webView.mainFrame loadData:data MIMEType:@"text/html" textEncodingName:@"utf-8" baseURL:nil];
|
||||
|
||||
while ([webView isLoading])
|
||||
CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, true);
|
||||
[webView display];
|
||||
|
||||
NSGraphicsContext *gc = [NSGraphicsContext graphicsContextWithGraphicsPort:(void *)context
|
||||
flipped:webView.isFlipped];
|
||||
[webView displayRectIgnoringOpacity:webView.bounds inContext:gc];
|
||||
}
|
||||
}*/
|
||||
12
QLOPML/style.css
Normal file
12
QLOPML/style.css
Normal file
@@ -0,0 +1,12 @@
|
||||
|
||||
* { font-family: Courier; }
|
||||
body { padding: 30px; background-color: #AAA; color: black; }
|
||||
dd, li, hr { font-weight: bold; line-height: 1.5em; }
|
||||
ul { list-style-type: none; padding-bottom: 1em; }
|
||||
a { font-size: 0.75em; color: #FBA43A; }
|
||||
.section { padding: 1em 1.5em; border-radius: 7px; background-color: #EEE; }
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
body { background-color: #555; color: white; }
|
||||
.section { background-color: #222; }
|
||||
}
|
||||
46
README.md
46
README.md
@@ -1,4 +1,4 @@
|
||||
[](#download--install)
|
||||
[](#download--install)
|
||||
[](https://github.com/relikd/baRSS/releases)
|
||||
[](https://github.com/relikd/baRSS/releases)
|
||||
[](LICENSE)
|
||||
@@ -35,7 +35,7 @@ Further, tuning the update frequently will decrease the traffic even more.
|
||||
Download & Install
|
||||
------------------
|
||||
|
||||
Requires macOS Sierra (10.12) or higher.
|
||||
Requires macOS Mojave (10.14) or higher.
|
||||
|
||||
### Easy way
|
||||
Go to [releases](https://github.com/relikd/baRSS/releases) and downloaded the latest version.
|
||||
@@ -64,40 +64,59 @@ If you prefer the optimized release version go to `Product > Archive`.
|
||||
Hidden options
|
||||
--------------
|
||||
|
||||
### Launch on start / reboot
|
||||
|
||||
baRSS has no option to launch it on start.
|
||||
However, you can still add the application to auto boot by adding it to the system login items:
|
||||
|
||||
`System Preferences > User > Login Items` (macOS 10-12)
|
||||
`System Preferences > General > Login Items & Extensions` (macOS 13+)
|
||||
|
||||
|
||||
### UI options
|
||||
|
||||
1. If you hold down the option key and click on an article item, you can mark a single item (un-)read without opening it.
|
||||
|
||||
2. To add websites without RSS feed you can use the regex converter.
|
||||
Hold down the option key in the feed edit modal and click the red regex button.
|
||||
Though, admittedly, this is for experts only.
|
||||
I still have to find a nice user-friendly way to achieve this.
|
||||
|
||||
|
||||
### CLI options
|
||||
|
||||
The following options have no UI equivalent and must be configured in Terminal.
|
||||
Most likely, you will never 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 without opening it.
|
||||
|
||||
2. When holding down the option key, the menu will show an item to open only a few unread items at a time.
|
||||
1. 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
|
||||
```
|
||||
|
||||
3. In preferences you can choose to show 'Short article names'.
|
||||
2. 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
|
||||
```
|
||||
|
||||
4. Limit the number of displayed articles per feed menu.
|
||||
3. Limit the number of displayed articles per feed menu.
|
||||
**Note:** displayed unread count may be different than the unread items inside. 'Open all unread' will open hidden items too.
|
||||
```
|
||||
defaults write de.relikd.baRSS articlesInMenuLimit -int 40
|
||||
```
|
||||
|
||||
5. You can change the appearance of colors throughout the application.
|
||||
4. 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 unread articles dot.
|
||||
```
|
||||
defaults write de.relikd.baRSS colorStatusIconTint -string "#37F"
|
||||
defaults write de.relikd.baRSS colorUnreadIndicator -string "#FBA33A"
|
||||
```
|
||||
|
||||
6. To backup your list of subscribed feeds, here is a one-liner:
|
||||
5. To backup your list of subscribed feeds, here is a one-liner:
|
||||
```
|
||||
open barss:backup && cp "$HOME/Library/Containers/de.relikd.baRSS/Data/Library/Application Support/baRSS/backup/feeds_latest.opml" "$HOME/Desktop/baRSS_backup_$(date "+%Y-%m-%d").opml"
|
||||
```
|
||||
@@ -107,15 +126,14 @@ open barss:backup && cp "$HOME/Library/Containers/de.relikd.baRSS/Data/Library/A
|
||||
ToDo
|
||||
----
|
||||
|
||||
The following list is not exhaustive but rather a collection of nice things that will be added eventually.
|
||||
I will postpone the development until demand increases …
|
||||
The following list is a collection of ideas that may be added if people request it.
|
||||
|
||||
- [ ] Localizations
|
||||
- [ ] Feed generator for websites without feeds
|
||||
- [x] Feed generator for websites without feeds
|
||||
- [ ] Automatically choose best update interval (e.g., avg)
|
||||
- [ ] Sync with online services
|
||||
- [ ] Feeds with authentication
|
||||
- [ ] Notification Center
|
||||
- [x] Notification Center
|
||||
- [ ] Distraction Mode
|
||||
- [ ] Distract less: Sleep timer. (e.g., disable updates during working hours)
|
||||
- [ ] Distract more: Automatically open feed items
|
||||
@@ -174,7 +192,7 @@ This project uses a modified version of Brent Simmons' [RSXML] for feed parsing.
|
||||
##### Trivia
|
||||
|
||||
- Start of project: __July 19, 2018__
|
||||
- Estimated development time: __1970h+__
|
||||
- Estimated development time: __2053h+__
|
||||
- First prototype used __feedparser python__ library
|
||||
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
archiveVersion = 1;
|
||||
classes = {
|
||||
};
|
||||
objectVersion = 50;
|
||||
objectVersion = 54;
|
||||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
@@ -12,9 +12,17 @@
|
||||
54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54195885218E1BDB00581B79 /* NSMenu+Ext.m */; };
|
||||
541C67C32255470B004D2CE6 /* SettingsAppearance.m in Sources */ = {isa = PBXBuildFile; fileRef = 541C67C22255470B004D2CE6 /* SettingsAppearance.m */; };
|
||||
54209E942117325100F3B5EF /* DrawImage.m in Sources */ = {isa = PBXBuildFile; fileRef = 54209E932117325100F3B5EF /* DrawImage.m */; };
|
||||
54229F552E02491A0019ACB0 /* TinySVG.m in Sources */ = {isa = PBXBuildFile; fileRef = 54229F542E02491A0019ACB0 /* TinySVG.m */; };
|
||||
54253C7F2C47303A00742695 /* RegexConverter+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54253C7E2C47303A00742695 /* RegexConverter+Ext.m */; };
|
||||
54253C932C49BFCD00742695 /* RegexConverterModal.m in Sources */ = {isa = PBXBuildFile; fileRef = 54253C8A2C49A92400742695 /* RegexConverterModal.m */; };
|
||||
54253C942C49BFDC00742695 /* RegexConverterController.m in Sources */ = {isa = PBXBuildFile; fileRef = 54253C872C49A6A800742695 /* RegexConverterController.m */; };
|
||||
54253C952C49BFE400742695 /* RegexConverterView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54253C842C47369000742695 /* RegexConverterView.m */; };
|
||||
544B011A2114B41200386E5C /* ModalSheet.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B01192114B41200386E5C /* ModalSheet.m */; };
|
||||
544B011D2114EE9100386E5C /* AppHook.m in Sources */ = {isa = PBXBuildFile; fileRef = 544B011C2114EE9100386E5C /* AppHook.m */; };
|
||||
544F5A752E30EFC700674F81 /* style.css in Resources */ = {isa = PBXBuildFile; fileRef = 544F5A722E30EFC700674F81 /* style.css */; };
|
||||
544F5A762E30EFC700674F81 /* opml-lib.m in Sources */ = {isa = PBXBuildFile; fileRef = 544F5A702E30EFC700674F81 /* opml-lib.m */; };
|
||||
54501010230E9C8600F0B165 /* FeedDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 5450100F230E9C8600F0B165 /* FeedDownload.m */; };
|
||||
5469E13C2EA90C6C00D46CE7 /* NotifyEndpoint.m in Sources */ = {isa = PBXBuildFile; fileRef = 5469E13B2EA90C6C00D46CE7 /* NotifyEndpoint.m */; };
|
||||
546A6A2922C583390034E806 /* SettingsGeneralView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D857D122802309001BA1C8 /* SettingsGeneralView.m */; };
|
||||
546A6A2C22C584AF0034E806 /* SettingsAppearanceView.m in Sources */ = {isa = PBXBuildFile; fileRef = 546A6A2A22C584AF0034E806 /* SettingsAppearanceView.m */; };
|
||||
546A6A2F22C585580034E806 /* SettingsAboutView.m in Sources */ = {isa = PBXBuildFile; fileRef = 546A6A2D22C585580034E806 /* SettingsAboutView.m */; };
|
||||
@@ -30,12 +38,15 @@
|
||||
5496B511214D6275003ED4ED /* UserPrefs.m in Sources */ = {isa = PBXBuildFile; fileRef = 5496B510214D6275003ED4ED /* UserPrefs.m */; };
|
||||
54A07A7F220E04CF00082C51 /* NSFetchRequest+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54A07A7E220E04CF00082C51 /* NSFetchRequest+Ext.m */; };
|
||||
54A07A82220E723D00082C51 /* MapUnreadTotal.m in Sources */ = {isa = PBXBuildFile; fileRef = 54A07A81220E723D00082C51 /* MapUnreadTotal.m */; };
|
||||
54A2D63922EF81A4007C61F3 /* QLOPML.qlgenerator in CopyFiles */ = {isa = PBXBuildFile; fileRef = 54A2D63822EF8193007C61F3 /* QLOPML.qlgenerator */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; };
|
||||
54ACC28C21061B3C0020715F /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC28B21061B3C0020715F /* main.m */; };
|
||||
54ACC29521061E270020715F /* UpdateScheduler.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29421061E270020715F /* UpdateScheduler.m */; };
|
||||
54ACC29821061FBA0020715F /* Preferences.m in Sources */ = {isa = PBXBuildFile; fileRef = 54ACC29721061FBA0020715F /* Preferences.m */; };
|
||||
54AD4E0C2301853D000AE386 /* NSString+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54AD4E0B2301853D000AE386 /* NSString+Ext.m */; };
|
||||
54AD4EE72305B17D000AE386 /* container-migration.plist in Resources */ = {isa = PBXBuildFile; fileRef = 54AD4EE62305B17D000AE386 /* container-migration.plist */; };
|
||||
54AD90EA2E30C48400160925 /* Quartz.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 54AD90E92E30C48400160925 /* Quartz.framework */; };
|
||||
54AD90EE2E30C48400160925 /* PreviewViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = 54AD90ED2E30C48400160925 /* PreviewViewController.m */; };
|
||||
54AD90F12E30C48400160925 /* PreviewViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 54AD90EF2E30C48400160925 /* PreviewViewController.xib */; };
|
||||
54AD90F72E30C48400160925 /* QLOPML.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 54AD90E72E30C48400160925 /* QLOPML.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
|
||||
54B51704226DC339006C1B29 /* ModalFeedEditView.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B51703226DC339006C1B29 /* ModalFeedEditView.m */; };
|
||||
54B517072270E990006C1B29 /* NSView+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B517062270E92A006C1B29 /* NSView+Ext.m */; };
|
||||
54B6F14A231551B3002C94C9 /* FaviconDownload.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B6F149231551B3002C94C9 /* FaviconDownload.m */; };
|
||||
@@ -44,6 +55,7 @@
|
||||
54B749E0220636200022CC6D /* FeedArticle+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54B749DF220635CD0022CC6D /* FeedArticle+Ext.m */; };
|
||||
54BB048921FD2AB500C303A5 /* NSDate+Ext.m in Sources */ = {isa = PBXBuildFile; fileRef = 54BB048821FD2AB500C303A5 /* NSDate+Ext.m */; };
|
||||
54BF444A22D0F4F300660096 /* AppIcon.icns in Resources */ = {isa = PBXBuildFile; fileRef = 54BF444922D0F4F300660096 /* AppIcon.icns */; };
|
||||
54D10DDB2C6E930F0008F621 /* RegexFeed.m in Sources */ = {isa = PBXBuildFile; fileRef = 54D10DDA2C6E930F0008F621 /* RegexFeed.m */; };
|
||||
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 */; };
|
||||
@@ -72,11 +84,11 @@
|
||||
remoteGlobalIDString = 84F22C171B52DDEA000060CE;
|
||||
remoteInfo = RSXML2Tests;
|
||||
};
|
||||
54A2D63722EF8193007C61F3 /* PBXContainerItemProxy */ = {
|
||||
54AD90F42E30C48400160925 /* PBXContainerItemProxy */ = {
|
||||
isa = PBXContainerItemProxy;
|
||||
containerPortal = 54A2D62E22EF8183007C61F3 /* QLOPML.xcodeproj */;
|
||||
proxyType = 2;
|
||||
remoteGlobalIDString = 540A649822EE78B200470937;
|
||||
containerPortal = 54ACC27421061B3B0020715F /* Project object */;
|
||||
proxyType = 1;
|
||||
remoteGlobalIDString = 54AD90E62E30C48400160925;
|
||||
remoteInfo = QLOPML;
|
||||
};
|
||||
/* End PBXContainerItemProxy section */
|
||||
@@ -93,23 +105,15 @@
|
||||
name = "Embed Frameworks";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
544DCCBC212A2B5A002DBC46 /* CopyFiles */ = {
|
||||
54AD90F62E30C48400160925 /* Embed App Extensions */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = "";
|
||||
dstSubfolderSpec = 16;
|
||||
dstSubfolderSpec = 13;
|
||||
files = (
|
||||
54AD90F72E30C48400160925 /* QLOPML.appex in Embed App Extensions */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
54CE4D4522EF509400E89C16 /* CopyFiles */ = {
|
||||
isa = PBXCopyFilesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
dstPath = Contents/Library/QuickLook;
|
||||
dstSubfolderSpec = 1;
|
||||
files = (
|
||||
54A2D63922EF81A4007C61F3 /* QLOPML.qlgenerator in CopyFiles */,
|
||||
);
|
||||
name = "Embed App Extensions";
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXCopyFilesBuildPhase section */
|
||||
@@ -127,12 +131,27 @@
|
||||
541C67C22255470B004D2CE6 /* SettingsAppearance.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SettingsAppearance.m; sourceTree = "<group>"; };
|
||||
54209E922117325100F3B5EF /* DrawImage.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = DrawImage.h; sourceTree = "<group>"; };
|
||||
54209E932117325100F3B5EF /* DrawImage.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = DrawImage.m; sourceTree = "<group>"; };
|
||||
54229F532E02491A0019ACB0 /* TinySVG.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = TinySVG.h; sourceTree = "<group>"; };
|
||||
54229F542E02491A0019ACB0 /* TinySVG.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = TinySVG.m; sourceTree = "<group>"; };
|
||||
54253C7A2C47303A00742695 /* RegexConverter+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "RegexConverter+Ext.h"; sourceTree = "<group>"; };
|
||||
54253C7E2C47303A00742695 /* RegexConverter+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "RegexConverter+Ext.m"; sourceTree = "<group>"; };
|
||||
54253C832C47368F00742695 /* RegexConverterView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RegexConverterView.h; sourceTree = "<group>"; };
|
||||
54253C842C47369000742695 /* RegexConverterView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RegexConverterView.m; sourceTree = "<group>"; };
|
||||
54253C872C49A6A800742695 /* RegexConverterController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RegexConverterController.m; sourceTree = "<group>"; };
|
||||
54253C882C49A6A800742695 /* RegexConverterController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RegexConverterController.h; sourceTree = "<group>"; };
|
||||
54253C8A2C49A92400742695 /* RegexConverterModal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RegexConverterModal.m; sourceTree = "<group>"; };
|
||||
54253C8B2C49A92400742695 /* RegexConverterModal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RegexConverterModal.h; sourceTree = "<group>"; };
|
||||
544B01182114B41200386E5C /* ModalSheet.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ModalSheet.h; sourceTree = "<group>"; };
|
||||
544B01192114B41200386E5C /* ModalSheet.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ModalSheet.m; sourceTree = "<group>"; };
|
||||
544B011B2114EE9100386E5C /* AppHook.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppHook.h; sourceTree = "<group>"; };
|
||||
544B011C2114EE9100386E5C /* AppHook.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppHook.m; sourceTree = "<group>"; };
|
||||
544F5A6F2E30EFC700674F81 /* opml-lib.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "opml-lib.h"; sourceTree = "<group>"; };
|
||||
544F5A702E30EFC700674F81 /* opml-lib.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "opml-lib.m"; sourceTree = "<group>"; };
|
||||
544F5A722E30EFC700674F81 /* style.css */ = {isa = PBXFileReference; lastKnownFileType = text.css; path = style.css; sourceTree = "<group>"; };
|
||||
5450100E230E9C8600F0B165 /* FeedDownload.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FeedDownload.h; sourceTree = "<group>"; };
|
||||
5450100F230E9C8600F0B165 /* FeedDownload.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FeedDownload.m; sourceTree = "<group>"; };
|
||||
5469E13A2EA90C6C00D46CE7 /* NotifyEndpoint.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = NotifyEndpoint.h; sourceTree = "<group>"; };
|
||||
5469E13B2EA90C6C00D46CE7 /* NotifyEndpoint.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = NotifyEndpoint.m; sourceTree = "<group>"; };
|
||||
546A6A2A22C584AF0034E806 /* SettingsAppearanceView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsAppearanceView.m; sourceTree = "<group>"; };
|
||||
546A6A2B22C584AF0034E806 /* SettingsAppearanceView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsAppearanceView.h; sourceTree = "<group>"; };
|
||||
546A6A2D22C585580034E806 /* SettingsAboutView.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsAboutView.m; sourceTree = "<group>"; };
|
||||
@@ -159,7 +178,6 @@
|
||||
54A07A7E220E04CF00082C51 /* NSFetchRequest+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSFetchRequest+Ext.m"; sourceTree = "<group>"; };
|
||||
54A07A80220E723D00082C51 /* MapUnreadTotal.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = MapUnreadTotal.h; sourceTree = "<group>"; };
|
||||
54A07A81220E723D00082C51 /* MapUnreadTotal.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = MapUnreadTotal.m; sourceTree = "<group>"; };
|
||||
54A2D62E22EF8183007C61F3 /* QLOPML.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = QLOPML.xcodeproj; path = ../QLOPML/QLOPML.xcodeproj; sourceTree = "<group>"; };
|
||||
54ACC27C21061B3B0020715F /* baRSS Beta.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "baRSS Beta.app"; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54ACC28321061B3B0020715F /* DBv1.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = DBv1.xcdatamodel; sourceTree = "<group>"; };
|
||||
54ACC28A21061B3C0020715F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
@@ -172,6 +190,13 @@
|
||||
54AD4E0B2301853D000AE386 /* NSString+Ext.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "NSString+Ext.m"; sourceTree = "<group>"; };
|
||||
54AD4EE42305AF60000AE386 /* baRSS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = baRSS.entitlements; sourceTree = "<group>"; };
|
||||
54AD4EE62305B17D000AE386 /* container-migration.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "container-migration.plist"; sourceTree = "<group>"; };
|
||||
54AD90E72E30C48400160925 /* QLOPML.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = QLOPML.appex; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
54AD90E92E30C48400160925 /* Quartz.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Quartz.framework; path = System/Library/Frameworks/Quartz.framework; sourceTree = SDKROOT; };
|
||||
54AD90EC2E30C48400160925 /* PreviewViewController.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PreviewViewController.h; sourceTree = "<group>"; };
|
||||
54AD90ED2E30C48400160925 /* PreviewViewController.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PreviewViewController.m; sourceTree = "<group>"; };
|
||||
54AD90F02E30C48400160925 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/PreviewViewController.xib; sourceTree = "<group>"; };
|
||||
54AD90F22E30C48400160925 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
54AD90F32E30C48400160925 /* QLOPML.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = QLOPML.entitlements; sourceTree = "<group>"; };
|
||||
54B51702226DC339006C1B29 /* ModalFeedEditView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ModalFeedEditView.h; sourceTree = "<group>"; };
|
||||
54B51703226DC339006C1B29 /* ModalFeedEditView.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ModalFeedEditView.m; sourceTree = "<group>"; };
|
||||
54B517052270E8C6006C1B29 /* NSView+Ext.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NSView+Ext.h"; sourceTree = "<group>"; };
|
||||
@@ -187,6 +212,8 @@
|
||||
54BB048721FD2AB500C303A5 /* NSDate+Ext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSDate+Ext.h"; sourceTree = "<group>"; };
|
||||
54BB048821FD2AB500C303A5 /* NSDate+Ext.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = "NSDate+Ext.m"; sourceTree = "<group>"; };
|
||||
54BF444922D0F4F300660096 /* AppIcon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = AppIcon.icns; sourceTree = "<group>"; };
|
||||
54D10DD92C6E930F0008F621 /* RegexFeed.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RegexFeed.h; sourceTree = "<group>"; };
|
||||
54D10DDA2C6E930F0008F621 /* RegexFeed.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = RegexFeed.m; sourceTree = "<group>"; };
|
||||
54D55D7122E624CD00057B98 /* SettingsFeeds+DragDrop.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "SettingsFeeds+DragDrop.h"; sourceTree = "<group>"; };
|
||||
54D55D7222E624CD00057B98 /* SettingsFeeds+DragDrop.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = "SettingsFeeds+DragDrop.m"; sourceTree = "<group>"; };
|
||||
54D857CC227C5785001BA1C8 /* RefreshStatisticsView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = RefreshStatisticsView.h; sourceTree = "<group>"; };
|
||||
@@ -219,6 +246,14 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
54AD90E42E30C48400160925 /* Frameworks */ = {
|
||||
isa = PBXFrameworksBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
54AD90EA2E30C48400160925 /* Quartz.framework in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXFrameworksBuildPhase section */
|
||||
|
||||
/* Begin PBXGroup section */
|
||||
@@ -237,6 +272,21 @@
|
||||
path = "Status Bar Menu";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54253C862C49A5A900742695 /* Regex Editor */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54253C8B2C49A92400742695 /* RegexConverterModal.h */,
|
||||
54253C8A2C49A92400742695 /* RegexConverterModal.m */,
|
||||
54253C882C49A6A800742695 /* RegexConverterController.h */,
|
||||
54253C872C49A6A800742695 /* RegexConverterController.m */,
|
||||
54253C832C47368F00742695 /* RegexConverterView.h */,
|
||||
54253C842C47369000742695 /* RegexConverterView.m */,
|
||||
54D10DD92C6E930F0008F621 /* RegexFeed.h */,
|
||||
54D10DDA2C6E930F0008F621 /* RegexFeed.m */,
|
||||
);
|
||||
path = "Regex Editor";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
544936F721F1E51E00DEE9AA /* NSCategories */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -258,6 +308,15 @@
|
||||
path = NSCategories;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
5469E1372EA90C3500D46CE7 /* Notifications */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
5469E13A2EA90C6C00D46CE7 /* NotifyEndpoint.h */,
|
||||
5469E13B2EA90C6C00D46CE7 /* NotifyEndpoint.m */,
|
||||
);
|
||||
path = Notifications;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
546FC44D2118B357007CC3A3 /* Preferences */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -295,28 +354,23 @@
|
||||
5477D34D21233C62002BA27F /* FeedGroup+Ext.m */,
|
||||
540F704321B6C16C0022E69D /* FeedMeta+Ext.h */,
|
||||
540F704421B6C16C0022E69D /* FeedMeta+Ext.m */,
|
||||
54253C7A2C47303A00742695 /* RegexConverter+Ext.h */,
|
||||
54253C7E2C47303A00742695 /* RegexConverter+Ext.m */,
|
||||
54B749DE220635BE0022CC6D /* FeedArticle+Ext.h */,
|
||||
54B749DF220635CD0022CC6D /* FeedArticle+Ext.m */,
|
||||
);
|
||||
path = "Core Data";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54A2D63422EF8193007C61F3 /* Products */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54A2D63822EF8193007C61F3 /* QLOPML.qlgenerator */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54ACC27321061B3B0020715F = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
540CD14821C094A2004AB594 /* README.md */,
|
||||
54892F1D2235285700271CBA /* CHANGELOG.md */,
|
||||
54ACC27E21061B3B0020715F /* baRSS */,
|
||||
54A2D62E22EF8183007C61F3 /* QLOPML.xcodeproj */,
|
||||
5483295E2A3CDB22000688B9 /* RSXML2.xcodeproj */,
|
||||
54AD90EB2E30C48400160925 /* QLOPML */,
|
||||
54AD90E82E30C48400160925 /* Frameworks */,
|
||||
54ACC27D21061B3B0020715F /* Products */,
|
||||
);
|
||||
sourceTree = "<group>";
|
||||
@@ -325,6 +379,7 @@
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54ACC27C21061B3B0020715F /* baRSS Beta.app */,
|
||||
54AD90E72E30C48400160925 /* QLOPML.appex */,
|
||||
);
|
||||
name = Products;
|
||||
sourceTree = "<group>";
|
||||
@@ -339,8 +394,10 @@
|
||||
54E9CF2F225913850023696F /* Helper */,
|
||||
544936F721F1E51E00DEE9AA /* NSCategories */,
|
||||
541A90EF21257D4F002680A6 /* Status Bar Menu */,
|
||||
5469E1372EA90C3500D46CE7 /* Notifications */,
|
||||
54A07A8322105E0800082C51 /* Core Data */,
|
||||
54AD4E04230084FD000AE386 /* Feed Import */,
|
||||
54253C862C49A5A900742695 /* Regex Editor */,
|
||||
546FC44D2118B357007CC3A3 /* Preferences */,
|
||||
54ACC28A21061B3C0020715F /* Info.plist */,
|
||||
54F7101322EE0DDA006985D1 /* Artwork */,
|
||||
@@ -368,6 +425,29 @@
|
||||
path = "Feed Import";
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54AD90E82E30C48400160925 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
54AD90E92E30C48400160925 /* Quartz.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54AD90EB2E30C48400160925 /* QLOPML */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
544F5A6F2E30EFC700674F81 /* opml-lib.h */,
|
||||
544F5A702E30EFC700674F81 /* opml-lib.m */,
|
||||
54AD90EC2E30C48400160925 /* PreviewViewController.h */,
|
||||
54AD90ED2E30C48400160925 /* PreviewViewController.m */,
|
||||
54AD90EF2E30C48400160925 /* PreviewViewController.xib */,
|
||||
54AD90F22E30C48400160925 /* Info.plist */,
|
||||
54AD90F32E30C48400160925 /* QLOPML.entitlements */,
|
||||
544F5A722E30EFC700674F81 /* style.css */,
|
||||
);
|
||||
path = QLOPML;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
54D857CF228022AB001BA1C8 /* General Tab */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
@@ -429,6 +509,8 @@
|
||||
54209E932117325100F3B5EF /* DrawImage.m */,
|
||||
54910065233A4D4000858AE2 /* URLScheme.h */,
|
||||
54910066233A4D4000858AE2 /* URLScheme.m */,
|
||||
54229F532E02491A0019ACB0 /* TinySVG.h */,
|
||||
54229F542E02491A0019ACB0 /* TinySVG.m */,
|
||||
);
|
||||
path = Helper;
|
||||
sourceTree = "<group>";
|
||||
@@ -453,27 +535,44 @@
|
||||
54ACC27921061B3B0020715F /* Frameworks */,
|
||||
54ACC27A21061B3B0020715F /* Resources */,
|
||||
544DCCBB212A2B4D002DBC46 /* Embed Frameworks */,
|
||||
54CE4D4522EF509400E89C16 /* CopyFiles */,
|
||||
544DCCBC212A2B5A002DBC46 /* CopyFiles */,
|
||||
543964EE2215C27B0016AAA3 /* ShellScript */,
|
||||
54FB05D12305BFAB00A088AD /* ShellScript */,
|
||||
54FB05D12305BFAB00A088AD /* dynamic app name in db migration */,
|
||||
54AD90F62E30C48400160925 /* Embed App Extensions */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
54AD90F52E30C48400160925 /* PBXTargetDependency */,
|
||||
);
|
||||
name = baRSS;
|
||||
productName = baRRS;
|
||||
productReference = 54ACC27C21061B3B0020715F /* baRSS Beta.app */;
|
||||
productType = "com.apple.product-type.application";
|
||||
};
|
||||
54AD90E62E30C48400160925 /* QLOPML */ = {
|
||||
isa = PBXNativeTarget;
|
||||
buildConfigurationList = 54AD90F82E30C48400160925 /* Build configuration list for PBXNativeTarget "QLOPML" */;
|
||||
buildPhases = (
|
||||
54AD90E32E30C48400160925 /* Sources */,
|
||||
54AD90E42E30C48400160925 /* Frameworks */,
|
||||
54AD90E52E30C48400160925 /* Resources */,
|
||||
);
|
||||
buildRules = (
|
||||
);
|
||||
dependencies = (
|
||||
);
|
||||
name = QLOPML;
|
||||
productName = QLOPML;
|
||||
productReference = 54AD90E72E30C48400160925 /* QLOPML.appex */;
|
||||
productType = "com.apple.product-type.app-extension";
|
||||
};
|
||||
/* End PBXNativeTarget section */
|
||||
|
||||
/* Begin PBXProject section */
|
||||
54ACC27421061B3B0020715F /* Project object */ = {
|
||||
isa = PBXProject;
|
||||
attributes = {
|
||||
LastUpgradeCheck = 1200;
|
||||
BuildIndependentTargetsInParallel = YES;
|
||||
LastUpgradeCheck = 1640;
|
||||
ORGANIZATIONNAME = relikd;
|
||||
TargetAttributes = {
|
||||
54ACC27B21061B3B0020715F = {
|
||||
@@ -491,6 +590,9 @@
|
||||
};
|
||||
};
|
||||
};
|
||||
54AD90E62E30C48400160925 = {
|
||||
CreatedOnToolsVersion = 12.4;
|
||||
};
|
||||
};
|
||||
};
|
||||
buildConfigurationList = 54ACC27721061B3B0020715F /* Build configuration list for PBXProject "baRSS" */;
|
||||
@@ -505,10 +607,6 @@
|
||||
productRefGroup = 54ACC27D21061B3B0020715F /* Products */;
|
||||
projectDirPath = "";
|
||||
projectReferences = (
|
||||
{
|
||||
ProductGroup = 54A2D63422EF8193007C61F3 /* Products */;
|
||||
ProjectRef = 54A2D62E22EF8183007C61F3 /* QLOPML.xcodeproj */;
|
||||
},
|
||||
{
|
||||
ProductGroup = 5483295F2A3CDB22000688B9 /* Products */;
|
||||
ProjectRef = 5483295E2A3CDB22000688B9 /* RSXML2.xcodeproj */;
|
||||
@@ -517,6 +615,7 @@
|
||||
projectRoot = "";
|
||||
targets = (
|
||||
54ACC27B21061B3B0020715F /* baRSS */,
|
||||
54AD90E62E30C48400160925 /* QLOPML */,
|
||||
);
|
||||
};
|
||||
/* End PBXProject section */
|
||||
@@ -536,13 +635,6 @@
|
||||
remoteRef = 548329662A3CDB22000688B9 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
54A2D63822EF8193007C61F3 /* QLOPML.qlgenerator */ = {
|
||||
isa = PBXReferenceProxy;
|
||||
fileType = wrapper.cfbundle;
|
||||
path = QLOPML.qlgenerator;
|
||||
remoteRef = 54A2D63722EF8193007C61F3 /* PBXContainerItemProxy */;
|
||||
sourceTree = BUILT_PRODUCTS_DIR;
|
||||
};
|
||||
/* End PBXReferenceProxy section */
|
||||
|
||||
/* Begin PBXResourcesBuildPhase section */
|
||||
@@ -556,28 +648,21 @@
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
54AD90E52E30C48400160925 /* Resources */ = {
|
||||
isa = PBXResourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
54AD90F12E30C48400160925 /* PreviewViewController.xib in Resources */,
|
||||
544F5A752E30EFC700674F81 /* style.css in Resources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXResourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXShellScriptBuildPhase section */
|
||||
543964EE2215C27B0016AAA3 /* ShellScript */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
inputFileListPaths = (
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
shellPath = /bin/sh;
|
||||
shellScript = "# https://crunchybagel.com/auto-incrementing-build-numbers-in-xcode/\nbuildNumber=$(/usr/libexec/PlistBuddy -c \"Print CFBundleVersion\" \"${PROJECT_DIR}/${INFOPLIST_FILE}\")\nbuildNumber=$(($buildNumber + 1))\n/usr/libexec/PlistBuddy -c \"Set :CFBundleVersion $buildNumber\" \"${PROJECT_DIR}/${INFOPLIST_FILE}\"\n";
|
||||
};
|
||||
54FB05D12305BFAB00A088AD /* ShellScript */ = {
|
||||
54FB05D12305BFAB00A088AD /* dynamic app name in db migration */ = {
|
||||
isa = PBXShellScriptBuildPhase;
|
||||
alwaysOutOfDate = 1;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
);
|
||||
@@ -585,6 +670,7 @@
|
||||
);
|
||||
inputPaths = (
|
||||
);
|
||||
name = "dynamic app name in db migration";
|
||||
outputFileListPaths = (
|
||||
);
|
||||
outputPaths = (
|
||||
@@ -601,8 +687,10 @@
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
54253C932C49BFCD00742695 /* RegexConverterModal.m in Sources */,
|
||||
54B51704226DC339006C1B29 /* ModalFeedEditView.m in Sources */,
|
||||
54AD4E0C2301853D000AE386 /* NSString+Ext.m in Sources */,
|
||||
54D10DDB2C6E930F0008F621 /* RegexFeed.m in Sources */,
|
||||
546A6A2C22C584AF0034E806 /* SettingsAppearanceView.m in Sources */,
|
||||
54E9CF32225914300023696F /* SettingsAbout.m in Sources */,
|
||||
54B749E0220636200022CC6D /* FeedArticle+Ext.m in Sources */,
|
||||
@@ -614,6 +702,7 @@
|
||||
54DD9F1323D1D6B000B1EAA6 /* NSColor+Ext.m in Sources */,
|
||||
54ACC29521061E270020715F /* UpdateScheduler.m in Sources */,
|
||||
54BB048921FD2AB500C303A5 /* NSDate+Ext.m in Sources */,
|
||||
54253C7F2C47303A00742695 /* RegexConverter+Ext.m in Sources */,
|
||||
5477D34E21233C62002BA27F /* FeedGroup+Ext.m in Sources */,
|
||||
54B6F14A231551B3002C94C9 /* FaviconDownload.m in Sources */,
|
||||
54E4446C2329AE0600BBF481 /* NSError+Ext.m in Sources */,
|
||||
@@ -628,6 +717,7 @@
|
||||
54195886218E1BDB00581B79 /* NSMenu+Ext.m in Sources */,
|
||||
54910067233A4D4000858AE2 /* URLScheme.m in Sources */,
|
||||
54F6025D21C1D4170006D338 /* OpmlFile.m in Sources */,
|
||||
54229F552E02491A0019ACB0 /* TinySVG.m in Sources */,
|
||||
5496B511214D6275003ED4ED /* UserPrefs.m in Sources */,
|
||||
546A6A2F22C585580034E806 /* SettingsAboutView.m in Sources */,
|
||||
54B517072270E990006C1B29 /* NSView+Ext.m in Sources */,
|
||||
@@ -636,18 +726,49 @@
|
||||
541C67C32255470B004D2CE6 /* SettingsAppearance.m in Sources */,
|
||||
54B749DA2204A85C0022CC6D /* BarStatusItem.m in Sources */,
|
||||
546FC43D21188AD5007CC3A3 /* SettingsFeeds.m in Sources */,
|
||||
54253C952C49BFE400742695 /* RegexConverterView.m in Sources */,
|
||||
548C6D0A230C33DE003A1AAF /* NSURL+Ext.m in Sources */,
|
||||
54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */,
|
||||
54195883218A061100581B79 /* Feed+Ext.m in Sources */,
|
||||
5469E13C2EA90C6C00D46CE7 /* NotifyEndpoint.m in Sources */,
|
||||
54501010230E9C8600F0B165 /* FeedDownload.m in Sources */,
|
||||
54209E942117325100F3B5EF /* DrawImage.m in Sources */,
|
||||
54253C942C49BFDC00742695 /* RegexConverterController.m in Sources */,
|
||||
54FE73D021220DEC003EAC65 /* StoreCoordinator.m in Sources */,
|
||||
54A07A7F220E04CF00082C51 /* NSFetchRequest+Ext.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
54AD90E32E30C48400160925 /* Sources */ = {
|
||||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
544F5A762E30EFC700674F81 /* opml-lib.m in Sources */,
|
||||
54AD90EE2E30C48400160925 /* PreviewViewController.m in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
/* End PBXSourcesBuildPhase section */
|
||||
|
||||
/* Begin PBXTargetDependency section */
|
||||
54AD90F52E30C48400160925 /* PBXTargetDependency */ = {
|
||||
isa = PBXTargetDependency;
|
||||
target = 54AD90E62E30C48400160925 /* QLOPML */;
|
||||
targetProxy = 54AD90F42E30C48400160925 /* PBXContainerItemProxy */;
|
||||
};
|
||||
/* End PBXTargetDependency section */
|
||||
|
||||
/* Begin PBXVariantGroup section */
|
||||
54AD90EF2E30C48400160925 /* PreviewViewController.xib */ = {
|
||||
isa = PBXVariantGroup;
|
||||
children = (
|
||||
54AD90F02E30C48400160925 /* Base */,
|
||||
);
|
||||
name = PreviewViewController.xib;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
/* End PBXVariantGroup section */
|
||||
|
||||
/* Begin XCBuildConfiguration section */
|
||||
54ACC28E21061B3C0020715F /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
@@ -684,7 +805,10 @@
|
||||
CLANG_WARN_UNREACHABLE_CODE = YES;
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 16785;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = dwarf;
|
||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
ENABLE_TESTABILITY = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -698,7 +822,8 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.12;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.5.3;
|
||||
MTL_ENABLE_DEBUG_INFO = YES;
|
||||
ONLY_ACTIVE_ARCH = YES;
|
||||
SDKROOT = macosx;
|
||||
@@ -741,7 +866,10 @@
|
||||
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
|
||||
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
|
||||
COPY_PHASE_STRIP = NO;
|
||||
CURRENT_PROJECT_VERSION = 16785;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
|
||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
||||
ENABLE_NS_ASSERTIONS = NO;
|
||||
ENABLE_STRICT_OBJC_MSGSEND = YES;
|
||||
GCC_C_LANGUAGE_STANDARD = gnu11;
|
||||
@@ -752,7 +880,8 @@
|
||||
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
|
||||
GCC_WARN_UNUSED_FUNCTION = YES;
|
||||
GCC_WARN_UNUSED_VARIABLE = YES;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.12;
|
||||
MACOSX_DEPLOYMENT_TARGET = 10.14;
|
||||
MARKETING_VERSION = 1.5.3;
|
||||
MTL_ENABLE_DEBUG_INFO = NO;
|
||||
SDKROOT = macosx;
|
||||
};
|
||||
@@ -774,12 +903,9 @@
|
||||
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES;
|
||||
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = baRSS/baRSS.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Mac Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
||||
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
@@ -807,7 +933,6 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS.beta;
|
||||
PRODUCT_NAME = "$(TARGET_NAME) Beta";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
|
||||
};
|
||||
name = Debug;
|
||||
@@ -828,12 +953,9 @@
|
||||
CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES;
|
||||
CLANG_WARN_SUSPICIOUS_IMPLICIT_CONVERSION = YES;
|
||||
CODE_SIGN_ENTITLEMENTS = baRSS/baRSS.entitlements;
|
||||
CODE_SIGN_IDENTITY = "Mac Developer";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
DEVELOPMENT_TEAM = UY657LKNHJ;
|
||||
EMBED_ASSET_PACKS_IN_PRODUCT_BUNDLE = NO;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
ENABLE_USER_SCRIPT_SANDBOXING = NO;
|
||||
FRAMEWORK_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"$(PROJECT_DIR)",
|
||||
@@ -861,7 +983,47 @@
|
||||
);
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
PROVISIONING_PROFILE_SPECIFIER = "";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
54AD90F92E30C48400160925 /* Debug */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = QLOPML/QLOPML.entitlements;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
"$(inherited)",
|
||||
);
|
||||
INFOPLIST_FILE = QLOPML/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS.beta.QLOPML;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
54AD90FA2E30C48400160925 /* Release */ = {
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_ENTITLEMENTS = QLOPML/QLOPML.entitlements;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
INFOPLIST_FILE = QLOPML/Info.plist;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
"@executable_path/../Frameworks",
|
||||
"@executable_path/../../../../Frameworks",
|
||||
);
|
||||
MTL_FAST_MATH = YES;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS.QLOPML;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SKIP_INSTALL = YES;
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
@@ -886,6 +1048,15 @@
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
54AD90F82E30C48400160925 /* Build configuration list for PBXNativeTarget "QLOPML" */ = {
|
||||
isa = XCConfigurationList;
|
||||
buildConfigurations = (
|
||||
54AD90F92E30C48400160925 /* Debug */,
|
||||
54AD90FA2E30C48400160925 /* Release */,
|
||||
);
|
||||
defaultConfigurationIsVisible = 0;
|
||||
defaultConfigurationName = Release;
|
||||
};
|
||||
/* End XCConfigurationList section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Scheme
|
||||
LastUpgradeVersion = "1240"
|
||||
version = "1.3">
|
||||
LastUpgradeVersion = "1640"
|
||||
version = "1.8">
|
||||
<BuildAction
|
||||
parallelizeBuildables = "YES"
|
||||
buildImplicitDependencies = "YES">
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#import "StoreCoordinator.h"
|
||||
#import "SettingsFeeds+DragDrop.h"
|
||||
#import "URLScheme.h"
|
||||
#import "NotifyEndpoint.h"
|
||||
#import "NSURL+Ext.h"
|
||||
#import "NSError+Ext.h"
|
||||
|
||||
@@ -37,12 +38,18 @@
|
||||
[_statusItem asyncReloadUnreadCount];
|
||||
[UpdateScheduler registerNetworkChangeNotification]; // will call update scheduler
|
||||
if ([StoreCoordinator isEmpty]) {
|
||||
[_statusItem showWelcomeMessage];
|
||||
// stupid macOS bugs ... status-bar-menu-item frame is zero without delay
|
||||
// [_statusItem showWelcomeMessage];
|
||||
[_statusItem performSelector:@selector(showWelcomeMessage) withObject:nil afterDelay:.2];
|
||||
[UpdateScheduler autoDownloadAndParseUpdateURL];
|
||||
} else {
|
||||
// mostly for version migration 0.9.4 ~> 1.0 (favicon storage)
|
||||
if (initial) [UpdateScheduler updateAllFavicons];
|
||||
}
|
||||
|
||||
// Notifications are disabled by default so this wont trigger for first app launch.
|
||||
// Also, this will register the notification delegate and respond to click & open feed.
|
||||
[NotifyEndpoint activate];
|
||||
}
|
||||
|
||||
- (void)applicationWillTerminate:(NSNotification *)aNotification {
|
||||
|
||||
Binary file not shown.
@@ -4,10 +4,14 @@
|
||||
<stop offset="0.5" style="stop-color:#FFAB48"/>
|
||||
<stop offset="1" style="stop-color:#FF8B00"/>
|
||||
</linearGradient>
|
||||
<!-- 3 = half stroke width, 28 = 25 + 3, 25 = radius, 44 = 100 - 2*r - 2*3 -->
|
||||
<!-- <path fill="url(#orange)" stroke="#FFF" stroke-width="6" d="M3,28v44q0,25,25,25h44q25,0,25,-25v-44q0,-25,-25,-25h-44q-25,0,-25,25z"/> -->
|
||||
<g transform="translate(10 10) scale(.8 .8)">
|
||||
<path fill="url(#orange)" d="M0,25v50q0,25,25,25h50q25,0,25,-25v-50q0,-25,-25,-25h-50q-25,0,-25,25z"/>
|
||||
<g fill="#FFFFFF" transform="matrix(-0.75 0 0 0.75 87.5 12.5)">
|
||||
<circle cx="13" cy="13" r="13"/>
|
||||
<path d="M0,45v20Q65,65,65,0h-20Q45,45,0,45z"/>
|
||||
<path d="M0,80v20Q100,100,100,0h-20Q80,80,0,80z"/>
|
||||
<g fill="#FFF" transform="translate(12.5 12.5) scale(.75 .75)">
|
||||
<circle cx="87" cy="13" r="13"/>
|
||||
<path d="M35,0q0,65,65,65v-20q-45,0,-45,-45z"/>
|
||||
<path d="M0,0q0,100,100,100v-20q-80,0,-80,-80z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 663 B After Width: | Height: | Size: 941 B |
@@ -6,7 +6,6 @@
|
||||
// TODO: Add support for media player? image feed?
|
||||
// <enclosure url="https://url.mp3" length="63274022" type="audio/mpeg" />
|
||||
// TODO: Disable 'update all' menu item during update?
|
||||
// TODO: HTML to Feed Generator. https://github.com/RSS-Bridge/rss-bridge
|
||||
// TODO: SQlite instead of CoreData? https://www.objc.io/issues/4-core-data/SQLite-instead-of-core-data/
|
||||
|
||||
|
||||
@@ -35,6 +34,8 @@ static NSImageName const RSSImageMenuBarIconActive = @"RSSImageMenuBarIconActive
|
||||
static NSImageName const RSSImageMenuBarIconPaused = @"RSSImageMenuBarIconPaused";
|
||||
/// Menu item, unread state icon (blue dot)
|
||||
static NSImageName const RSSImageMenuItemUnread = @"RSSImageMenuItemUnread";
|
||||
/// Feed edit, regex editor icon @c "(.*)"
|
||||
static NSImageName const RSSImageRegexIcon = @"RSSImageRegexIcon";
|
||||
|
||||
|
||||
#pragma mark - NSNotificationName constants
|
||||
|
||||
@@ -10,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Generator methods / Feed update
|
||||
+ (instancetype)newFeedAndMetaInContext:(NSManagedObjectContext*)context;
|
||||
- (NSString*)notificationID;
|
||||
- (void)updateWithRSS:(RSParsedFeed*)obj postUnreadCountChange:(BOOL)flag;
|
||||
- (NSMenuItem*)newMenuItem;
|
||||
// Getter & Setter
|
||||
@@ -17,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
- (void)setNewIcon:(NSURL*)location;
|
||||
// Article properties
|
||||
- (nullable NSArray<FeedArticle*>*)sortedArticles;
|
||||
- (NSUInteger)countUnread;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#import "FeedGroup+Ext.h"
|
||||
#import "FeedArticle+Ext.h"
|
||||
#import "StoreCoordinator.h"
|
||||
#import "NotifyEndpoint.h"
|
||||
#import "NSURL+Ext.h"
|
||||
|
||||
@implementation Feed (Ext)
|
||||
@@ -17,6 +18,11 @@
|
||||
return feed;
|
||||
}
|
||||
|
||||
/// unique ID used for notifications. returns @c objectID.URIRepresentation.absoluteString
|
||||
- (NSString*)notificationID {
|
||||
return self.objectID.URIRepresentation.absoluteString;
|
||||
}
|
||||
|
||||
/// Call @c indexPathString on @c .group and update @c .indexPath if current value is different.
|
||||
- (void)calculateAndSetIndexPathString {
|
||||
NSString *pthStr = [self.group indexPathString];
|
||||
@@ -89,6 +95,8 @@
|
||||
[localSet removeObject:stored];
|
||||
if (stored.sortIndex != currentIndex)
|
||||
stored.sortIndex = currentIndex; // Ensures block of ascending indices
|
||||
// replace local values with remote changes (if any)
|
||||
[stored updateArticleIfChanged:article];
|
||||
} else {
|
||||
FeedArticle *newArticle = [FeedArticle newArticle:article inContext:self.managedObjectContext];
|
||||
newArticle.sortIndex = currentIndex;
|
||||
@@ -108,10 +116,12 @@
|
||||
- (NSUInteger)deleteArticles:(NSMutableSet<FeedArticle*>*)localSet withRemoteSet:(NSArray<RSParsedArticle*>*)remoteSet {
|
||||
NSUInteger c = 0;
|
||||
NSMutableSet<FeedArticle*> *deletingSet = [NSMutableSet setWithCapacity:localSet.count];
|
||||
NSMutableArray *dismissed = [NSMutableArray array];
|
||||
for (FeedArticle *fa in localSet) {
|
||||
if (![self findLocalArticle:fa inRemoteSet:remoteSet]) {
|
||||
if (fa.unread) ++c;
|
||||
// TODO: keep unread articles?
|
||||
[dismissed addObject:fa.notificationID];
|
||||
[self.managedObjectContext deleteObject:fa];
|
||||
[deletingSet addObject:fa];
|
||||
}
|
||||
@@ -119,6 +129,7 @@
|
||||
if (deletingSet.count > 0) {
|
||||
[localSet minusSet:deletingSet];
|
||||
[self removeArticles:deletingSet];
|
||||
[NotifyEndpoint dismiss:dismissed];
|
||||
}
|
||||
return c;
|
||||
}
|
||||
@@ -142,11 +153,13 @@
|
||||
- (FeedArticle*)findRemoteArticle:(RSParsedArticle*)remote inLocalSet:(NSSet<FeedArticle*>*)localSet {
|
||||
NSString *searchLink = remote.link;
|
||||
NSString *searchGuid = remote.guid;
|
||||
BOOL linkIsNil = (searchLink == nil);
|
||||
BOOL guidIsNil = (searchGuid == nil);
|
||||
for (FeedArticle *art in localSet) {
|
||||
if ((linkIsNil && art.link == nil) || (!linkIsNil && [art.link isEqualToString:searchLink])) {
|
||||
if ((guidIsNil && art.guid == nil) || (!guidIsNil && [art.guid isEqualToString:searchGuid]))
|
||||
// assuming if a guid is set, it will always be unique
|
||||
if (searchGuid != nil) {
|
||||
if ([art.guid isEqualToString:searchGuid])
|
||||
return art;
|
||||
} else if (searchLink != nil) {
|
||||
if ([art.link isEqualToString:searchLink])
|
||||
return art;
|
||||
}
|
||||
}
|
||||
@@ -159,17 +172,29 @@
|
||||
- (RSParsedArticle*)findLocalArticle:(FeedArticle*)local inRemoteSet:(NSArray<RSParsedArticle*>*)remoteSet {
|
||||
NSString *searchLink = local.link;
|
||||
NSString *searchGuid = local.guid;
|
||||
BOOL linkIsNil = (searchLink == nil);
|
||||
BOOL guidIsNil = (searchGuid == nil);
|
||||
for (RSParsedArticle *art in remoteSet) {
|
||||
if ((linkIsNil && art.link == nil) || (!linkIsNil && [art.link isEqualToString:searchLink])) {
|
||||
if ((guidIsNil && art.guid == nil) || (!guidIsNil && [art.guid isEqualToString:searchGuid]))
|
||||
// assuming if a guid is set, it will always be unique
|
||||
if (searchGuid != nil) {
|
||||
if ([art.guid isEqualToString:searchGuid])
|
||||
return art;
|
||||
} else if (searchLink != nil) {
|
||||
if ([art.link isEqualToString:searchLink])
|
||||
return art;
|
||||
}
|
||||
}
|
||||
return nil;
|
||||
}
|
||||
|
||||
/// Number of unread articles
|
||||
- (NSUInteger)countUnread {
|
||||
NSUInteger count = 0;
|
||||
for (FeedArticle *article in self.articles) {
|
||||
if (article.unread)
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Icon -
|
||||
|
||||
|
||||
@@ -6,6 +6,8 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface FeedArticle (Ext)
|
||||
+ (instancetype)newArticle:(RSParsedArticle*)entry inContext:(NSManagedObjectContext*)moc;
|
||||
- (NSString*)notificationID;
|
||||
- (void)updateArticleIfChanged:(RSParsedArticle*)entry;
|
||||
- (NSMenuItem*)newMenuItem;
|
||||
@end
|
||||
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
@import RSXML2.RSParsedArticle;
|
||||
#import "FeedArticle+Ext.h"
|
||||
#import "Feed+Ext.h"
|
||||
#import "Constants.h"
|
||||
#import "UserPrefs.h"
|
||||
#import "StoreCoordinator.h"
|
||||
#import "NotifyEndpoint.h"
|
||||
#import "NSString+Ext.h"
|
||||
|
||||
@implementation FeedArticle (Ext)
|
||||
@@ -25,6 +27,21 @@
|
||||
return fa;
|
||||
}
|
||||
|
||||
/// unique ID used for notifications. returns @c objectID.URIRepresentation.absoluteString
|
||||
- (NSString*)notificationID {
|
||||
return self.objectID.URIRepresentation.absoluteString;
|
||||
}
|
||||
|
||||
- (void)updateArticleIfChanged:(RSParsedArticle*)entry {
|
||||
[self setGuidIfChanged:entry.guid];
|
||||
[self setTitleIfChanged:entry.title];
|
||||
[self setAuthorIfChanged:entry.author];
|
||||
[self setAbstractIfChanged:(entry.abstract.length > 0) ? [entry.abstract htmlToPlainText] : nil];
|
||||
[self setBodyIfChanged:(entry.body.length > 0) ? [entry.body htmlToPlainText] : nil];
|
||||
[self setLinkIfChanged:(entry.link.length > 0) ? entry.link : entry.guid];
|
||||
[self setPublishedIfChanged:entry.datePublished ? entry.datePublished : entry.dateModified];
|
||||
}
|
||||
|
||||
/// @return Full or truncated article title, based on user preference in settings.
|
||||
- (NSString*)shortArticleName {
|
||||
NSString *title = self.title;
|
||||
@@ -67,8 +84,84 @@
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
NSNumber *num = (fa.unread ? @+1 : @-1);
|
||||
PostNotification(kNotificationTotalUnreadCountChanged, num);
|
||||
|
||||
[NotifyEndpoint dismiss:fa.feed.countUnread > 0 ? @[fa.notificationID] : @[fa.notificationID, fa.feed.notificationID]];
|
||||
}
|
||||
[moc reset];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Setter -
|
||||
|
||||
|
||||
/// Set @c guid attribute but only if value differs.
|
||||
- (void)setGuidIfChanged:(nullable NSString*)guid {
|
||||
if (guid.length == 0) {
|
||||
if (self.guid.length > 0)
|
||||
self.guid = nil; // nullify empty strings
|
||||
} else if (![self.guid isEqualToString: guid]) {
|
||||
self.guid = guid;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set @c link attribute but only if value differs.
|
||||
- (void)setLinkIfChanged:(nullable NSString*)link {
|
||||
if (link.length == 0) {
|
||||
if (self.link.length > 0)
|
||||
self.link = nil; // nullify empty strings
|
||||
} else if (![self.link isEqualToString: link]) {
|
||||
self.link = link;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set @c title attribute but only if value differs.
|
||||
- (void)setTitleIfChanged:(nullable NSString*)title {
|
||||
if (title.length == 0) {
|
||||
if (self.title.length > 0)
|
||||
self.title = nil; // nullify empty strings
|
||||
} else if (![self.title isEqualToString: title]) {
|
||||
self.title = title;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set @c abstract attribute but only if value differs.
|
||||
- (void)setAbstractIfChanged:(nullable NSString*)abstract {
|
||||
if (abstract.length == 0) {
|
||||
if (self.abstract.length > 0)
|
||||
self.abstract = nil; // nullify empty strings
|
||||
} else if (![self.abstract isEqualToString: abstract]) {
|
||||
self.abstract = abstract;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set @c body attribute but only if value differs.
|
||||
- (void)setBodyIfChanged:(nullable NSString*)body {
|
||||
if (body.length == 0) {
|
||||
if (self.body.length > 0)
|
||||
self.body = nil; // nullify empty strings
|
||||
} else if (![self.body isEqualToString: body]) {
|
||||
self.body = body;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set @c author attribute but only if value differs.
|
||||
- (void)setAuthorIfChanged:(nullable NSString*)author {
|
||||
if (author.length == 0) {
|
||||
if (self.author.length > 0)
|
||||
self.author = nil; // nullify empty strings
|
||||
} else if (![self.author isEqualToString: author]) {
|
||||
self.author = author;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set @c published attribute but only if value differs.
|
||||
- (void)setPublishedIfChanged:(nullable NSDate*)published {
|
||||
if (!published) {
|
||||
if (self.published)
|
||||
self.published = nil; // nullify empty date
|
||||
} else if (![self.published isEqualToDate: published]) {
|
||||
self.published = published;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
16
baRSS/Core Data/RegexConverter+Ext.h
Normal file
16
baRSS/Core Data/RegexConverter+Ext.h
Normal file
@@ -0,0 +1,16 @@
|
||||
@import Cocoa;
|
||||
#import "RegexConverter+CoreDataClass.h"
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RegexConverter (Ext)
|
||||
+ (instancetype)newInContext:(NSManagedObjectContext*)moc;
|
||||
- (void)setEntryIfChanged:(nullable NSString*)pattern;
|
||||
- (void)setHrefIfChanged:(nullable NSString*)pattern;
|
||||
- (void)setTitleIfChanged:(nullable NSString*)pattern;
|
||||
- (void)setDescIfChanged:(nullable NSString*)pattern;
|
||||
- (void)setDateIfChanged:(nullable NSString*)pattern;
|
||||
- (void)setDateFormatIfChanged:(nullable NSString*)pattern;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
70
baRSS/Core Data/RegexConverter+Ext.m
Normal file
70
baRSS/Core Data/RegexConverter+Ext.m
Normal file
@@ -0,0 +1,70 @@
|
||||
#import "RegexConverter+Ext.h"
|
||||
|
||||
@implementation RegexConverter (Ext)
|
||||
|
||||
/// Create new instance
|
||||
+ (instancetype)newInContext:(NSManagedObjectContext*)moc {
|
||||
return [[RegexConverter alloc] initWithEntity:[RegexConverter entity] insertIntoManagedObjectContext:moc];
|
||||
}
|
||||
|
||||
/// Set @c entry attribute but only if value differs.
|
||||
- (void)setEntryIfChanged:(nullable NSString*)pattern {
|
||||
if (pattern.length == 0) {
|
||||
if (self.entry.length > 0)
|
||||
self.entry = nil; // nullify empty strings
|
||||
} else if (![self.entry isEqualToString: pattern]) {
|
||||
self.entry = pattern;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set @c href attribute but only if value differs.
|
||||
- (void)setHrefIfChanged:(nullable NSString*)pattern {
|
||||
if (pattern.length == 0) {
|
||||
if (self.href.length > 0)
|
||||
self.href = nil; // nullify empty strings
|
||||
} else if (![self.href isEqualToString: pattern]) {
|
||||
self.href = pattern;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set @c title attribute but only if value differs.
|
||||
- (void)setTitleIfChanged:(nullable NSString*)pattern {
|
||||
if (pattern.length == 0) {
|
||||
if (self.title.length > 0)
|
||||
self.title = nil; // nullify empty strings
|
||||
} else if (![self.title isEqualToString: pattern]) {
|
||||
self.title = pattern;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set @c desc attribute but only if value differs.
|
||||
- (void)setDescIfChanged:(nullable NSString*)pattern {
|
||||
if (pattern.length == 0) {
|
||||
if (self.desc.length > 0)
|
||||
self.desc = nil; // nullify empty strings
|
||||
} else if (![self.desc isEqualToString: pattern]) {
|
||||
self.desc = pattern;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set @c date attribute but only if value differs.
|
||||
- (void)setDateIfChanged:(nullable NSString*)pattern {
|
||||
if (pattern.length == 0) {
|
||||
if (self.date.length > 0)
|
||||
self.date = nil; // nullify empty strings
|
||||
} else if (![self.date isEqualToString: pattern]) {
|
||||
self.date = pattern;
|
||||
}
|
||||
}
|
||||
|
||||
/// Set @c dateFormat attribute but only if value differs.
|
||||
- (void)setDateFormatIfChanged:(nullable NSString*)pattern {
|
||||
if (pattern.length == 0) {
|
||||
if (self.dateFormat.length > 0)
|
||||
self.dateFormat = nil; // nullify empty strings
|
||||
} else if (![self.dateFormat isEqualToString: pattern]) {
|
||||
self.dateFormat = pattern;
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -30,6 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
// Unread articles list & mark articled read
|
||||
+ (NSArray<FeedArticle*>*)articlesAtPath:(nullable NSString*)path isFeed:(BOOL)feedFlag sorted:(BOOL)sortFlag unread:(BOOL)readFlag inContext:(NSManagedObjectContext*)moc limit:(NSUInteger)limit;
|
||||
+ (nullable NSArray<NSString*>*)updateArticles:(NSArray<FeedArticle*>*)list markRead:(BOOL)markRead andOpen:(BOOL)openLinks inContext:(NSManagedObjectContext*)moc;
|
||||
|
||||
// Restore sound state
|
||||
+ (void)cleanupAndShowAlert:(BOOL)flag;
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
#import "AppHook.h"
|
||||
#import "Constants.h"
|
||||
#import "FaviconDownload.h"
|
||||
#import "UserPrefs.h"
|
||||
#import "Feed+Ext.h"
|
||||
#import "FeedArticle+Ext.h"
|
||||
#import "NSURL+Ext.h"
|
||||
#import "NSError+Ext.h"
|
||||
#import "NSFetchRequest+Ext.h"
|
||||
@@ -57,7 +59,9 @@
|
||||
opt = [[Options alloc] initWithEntity:Options.entity insertIntoManagedObjectContext:moc];
|
||||
opt.key = key;
|
||||
}
|
||||
if (opt.value != value) {
|
||||
opt.value = value;
|
||||
}
|
||||
[self saveContext:moc andParent:YES];
|
||||
[moc reset];
|
||||
}
|
||||
@@ -200,6 +204,50 @@
|
||||
return [fr fetchAllRows:moc];
|
||||
}
|
||||
|
||||
/**
|
||||
For provided articles, pen link, mark read, and save changes.
|
||||
@warning Will invalidate context.
|
||||
|
||||
@param list Should only contain @c FeedArticle
|
||||
@param markRead Whether the articles should be marked read or unread.
|
||||
@param openLinks Whether to open the link or mark read without opening
|
||||
|
||||
@return @c notificationID for all articles that were opened (empty if @c openLinks=NO or open failed).
|
||||
*/
|
||||
+ (nullable NSArray<NSString*>*)updateArticles:(NSArray<FeedArticle*>*)list markRead:(BOOL)markRead andOpen:(BOOL)openLinks inContext:(NSManagedObjectContext*)moc {
|
||||
if (openLinks) {
|
||||
NSMutableArray<NSURL*> *urls = [NSMutableArray arrayWithCapacity:list.count];
|
||||
for (FeedArticle *fa in list) {
|
||||
if (fa.link.length > 0)
|
||||
[urls addObject:[NSURL URLWithString:fa.link]];
|
||||
}
|
||||
if (urls.count > 0 && !UserPrefsOpenURLs(urls))
|
||||
return nil; // if success == NO, do not modify unread state & exit
|
||||
}
|
||||
|
||||
NSInteger countChange = 0;
|
||||
for (FeedArticle *fa in list) {
|
||||
if (fa.unread == markRead) { // only if differs
|
||||
fa.unread = !markRead;
|
||||
countChange += markRead ? -1 : +1;
|
||||
}
|
||||
}
|
||||
[self saveContext:moc andParent:YES];
|
||||
|
||||
// gather uri-ids for notification dismiss
|
||||
NSMutableArray<NSString*> *dbRefs = [NSMutableArray array];
|
||||
if (markRead) {
|
||||
for (FeedArticle *fa in list) {
|
||||
[dbRefs addObject:fa.notificationID];
|
||||
[dbRefs addObject:fa.feed.notificationID];
|
||||
}
|
||||
}
|
||||
|
||||
[moc reset];
|
||||
PostNotification(kNotificationTotalUnreadCountChanged, @(countChange));
|
||||
return dbRefs;
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Restore Sound State
|
||||
|
||||
|
||||
@@ -1,52 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="14460.32" systemVersion="17G8030" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="v1.0.0">
|
||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="19H2026" minimumToolsVersion="Automatic" sourceLanguage="Objective-C" userDefinedModelVersionIdentifier="v1.0.0">
|
||||
<entity name="Feed" representedClassName="Feed" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="indexPath" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="articles" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedArticle" inverseName="feed" inverseEntity="FeedArticle" syncable="YES"/>
|
||||
<relationship name="group" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedGroup" inverseName="feed" inverseEntity="FeedGroup" syncable="YES"/>
|
||||
<relationship name="meta" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="FeedMeta" inverseName="feed" inverseEntity="FeedMeta" syncable="YES"/>
|
||||
<attribute name="indexPath" optional="YES" attributeType="String"/>
|
||||
<attribute name="link" optional="YES" attributeType="String"/>
|
||||
<attribute name="subtitle" optional="YES" attributeType="String"/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
<relationship name="articles" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedArticle" inverseName="feed" inverseEntity="FeedArticle"/>
|
||||
<relationship name="group" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedGroup" inverseName="feed" inverseEntity="FeedGroup"/>
|
||||
<relationship name="meta" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="FeedMeta" inverseName="feed" inverseEntity="FeedMeta"/>
|
||||
<relationship name="regex" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="RegexConverter" inverseName="feed" inverseEntity="RegexConverter"/>
|
||||
</entity>
|
||||
<entity name="FeedArticle" representedClassName="FeedArticle" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="abstract" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="author" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="body" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="guid" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="link" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="published" optional="YES" attributeType="Date" usesScalarValueType="NO" customClassName="NSArray" syncable="YES"/>
|
||||
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="title" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="unread" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES" syncable="YES"/>
|
||||
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="articles" inverseEntity="Feed" syncable="YES"/>
|
||||
<attribute name="abstract" optional="YES" attributeType="String"/>
|
||||
<attribute name="author" optional="YES" attributeType="String"/>
|
||||
<attribute name="body" optional="YES" attributeType="String"/>
|
||||
<attribute name="guid" optional="YES" attributeType="String"/>
|
||||
<attribute name="link" optional="YES" attributeType="String"/>
|
||||
<attribute name="published" optional="YES" attributeType="Date" usesScalarValueType="NO" customClassName="NSArray"/>
|
||||
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
<attribute name="unread" optional="YES" attributeType="Boolean" defaultValueString="YES" usesScalarValueType="YES"/>
|
||||
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="articles" inverseEntity="Feed"/>
|
||||
</entity>
|
||||
<entity name="FeedGroup" representedClassName="FeedGroup" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="name" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="type" optional="YES" attributeType="Integer 16" defaultValueString="-1" usesScalarValueType="YES" syncable="YES"/>
|
||||
<relationship name="children" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedGroup" inverseName="parent" inverseEntity="FeedGroup" syncable="YES"/>
|
||||
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Feed" inverseName="group" inverseEntity="Feed" syncable="YES"/>
|
||||
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedGroup" inverseName="children" inverseEntity="FeedGroup" syncable="YES"/>
|
||||
<attribute name="name" optional="YES" attributeType="String"/>
|
||||
<attribute name="sortIndex" optional="YES" attributeType="Integer 32" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="type" optional="YES" attributeType="Integer 16" defaultValueString="-1" usesScalarValueType="YES"/>
|
||||
<relationship name="children" optional="YES" toMany="YES" deletionRule="Cascade" destinationEntity="FeedGroup" inverseName="parent" inverseEntity="FeedGroup"/>
|
||||
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Cascade" destinationEntity="Feed" inverseName="group" inverseEntity="Feed"/>
|
||||
<relationship name="parent" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="FeedGroup" inverseName="children" inverseEntity="FeedGroup"/>
|
||||
</entity>
|
||||
<entity name="FeedMeta" representedClassName="FeedMeta" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="errorCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="etag" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="modified" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="refresh" optional="YES" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES" syncable="YES"/>
|
||||
<attribute name="scheduled" optional="YES" attributeType="Date" usesScalarValueType="NO" syncable="YES"/>
|
||||
<attribute name="url" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="meta" inverseEntity="Feed" syncable="YES"/>
|
||||
<attribute name="errorCount" optional="YES" attributeType="Integer 16" defaultValueString="0" usesScalarValueType="YES"/>
|
||||
<attribute name="etag" optional="YES" attributeType="String"/>
|
||||
<attribute name="modified" optional="YES" attributeType="String"/>
|
||||
<attribute name="refresh" optional="YES" attributeType="Integer 32" defaultValueString="-1" usesScalarValueType="YES"/>
|
||||
<attribute name="scheduled" optional="YES" attributeType="Date" usesScalarValueType="NO"/>
|
||||
<attribute name="url" optional="YES" attributeType="String"/>
|
||||
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="meta" inverseEntity="Feed"/>
|
||||
</entity>
|
||||
<entity name="Options" representedClassName="Options" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="key" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="value" optional="YES" attributeType="String" syncable="YES"/>
|
||||
<attribute name="key" optional="YES" attributeType="String"/>
|
||||
<attribute name="value" optional="YES" attributeType="String"/>
|
||||
</entity>
|
||||
<entity name="RegexConverter" representedClassName="RegexConverter" syncable="YES" codeGenerationType="class">
|
||||
<attribute name="date" optional="YES" attributeType="String"/>
|
||||
<attribute name="dateFormat" optional="YES" attributeType="String"/>
|
||||
<attribute name="desc" optional="YES" attributeType="String"/>
|
||||
<attribute name="entry" optional="YES" attributeType="String"/>
|
||||
<attribute name="href" optional="YES" attributeType="String"/>
|
||||
<attribute name="title" optional="YES" attributeType="String"/>
|
||||
<relationship name="feed" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Feed" inverseName="regex" inverseEntity="Feed"/>
|
||||
</entity>
|
||||
<elements>
|
||||
<element name="Feed" positionX="-278.84765625" positionY="-112.953125" width="128" height="150"/>
|
||||
<element name="Feed" positionX="-278.84765625" positionY="-112.953125" width="128" height="163"/>
|
||||
<element name="FeedArticle" positionX="-96.77734375" positionY="-113.83984375" width="128" height="195"/>
|
||||
<element name="FeedGroup" positionX="-460.37890625" positionY="-111.62890625" width="130.52734375" height="135"/>
|
||||
<element name="FeedMeta" positionX="-348.02734375" positionY="136.89453125" width="128" height="150"/>
|
||||
<element name="Options" positionX="-279" positionY="36" width="128" height="75"/>
|
||||
<element name="FeedMeta" positionX="-456.265625" positionY="62.41015625" width="128" height="150"/>
|
||||
<element name="Options" positionX="-279.09375" positionY="91.4609375" width="128" height="75"/>
|
||||
<element name="RegexConverter" positionX="-115.984375" positionY="93.1796875" width="128" height="148"/>
|
||||
</elements>
|
||||
</model>
|
||||
@@ -1,5 +1,5 @@
|
||||
@import Cocoa;
|
||||
@class RSParsedFeed, RSHTMLMetadataFeedLink, Feed, FaviconDownload;
|
||||
@class RSParsedFeed, RSHTMLMetadataFeedLink, Feed, FaviconDownload, RegexConverter;
|
||||
@protocol FeedDownloadDelegate;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
@@ -14,6 +14,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property (readonly, nullable) RSParsedFeed *xmlfeed;
|
||||
@property (readonly, nullable) NSError *error;
|
||||
@property (readonly, nullable) NSString *faviconURL;
|
||||
@property (readonly, nullable) NSData *rawData;
|
||||
|
||||
typedef void (^FeedDownloadBlock)(FeedDownload *sender);
|
||||
|
||||
@@ -21,6 +22,7 @@ typedef void (^FeedDownloadBlock)(FeedDownload *sender);
|
||||
+ (instancetype)withURL:(NSString*)url;
|
||||
+ (instancetype)withFeed:(Feed*)feed forced:(BOOL)flag;
|
||||
// Actions
|
||||
- (instancetype)withRegex:(nullable RegexConverter *)converter enforce:(BOOL)flag;
|
||||
- (instancetype)startWithDelegate:(id<FeedDownloadDelegate>)delegate;
|
||||
- (instancetype)startWithBlock:(nonnull FeedDownloadBlock)block;
|
||||
- (void)cancel;
|
||||
|
||||
@@ -6,6 +6,9 @@
|
||||
#import "FeedMeta+Ext.h"
|
||||
#import "NSError+Ext.h"
|
||||
#import "NSURLRequest+Ext.h"
|
||||
#import "RegexFeed.h"
|
||||
#import "RegexConverter+Ext.h"
|
||||
|
||||
|
||||
@interface FeedDownload()
|
||||
@property (nonatomic, assign) BOOL respondToSelectFeed, respondToRedirect, respondToEnd;
|
||||
@@ -20,6 +23,9 @@
|
||||
@property (nonatomic, strong) RSParsedFeed *xmlfeed;
|
||||
@property (nonatomic, strong) NSError *error;
|
||||
@property (nonatomic, strong) NSString *faviconURL;
|
||||
@property (nonatomic, strong) NSData *rawData;
|
||||
@property (nonatomic, strong) RegexConverter *regexConverter;
|
||||
@property (nonatomic, assign) BOOL regexEnforce;
|
||||
@end
|
||||
|
||||
@implementation FeedDownload
|
||||
@@ -51,13 +57,20 @@
|
||||
FeedDownload *this = [FeedDownload new];
|
||||
this.assertIsFeedURL = YES;
|
||||
this.request = req;
|
||||
return this;
|
||||
return [this withRegex:feed.regex enforce:false];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// | MARK: - Getter & Setter
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// Set @c .regexConverter for html-processed feeds.
|
||||
- (instancetype)withRegex:(RegexConverter *)converter enforce:(BOOL)flag {
|
||||
self.regexConverter = converter;
|
||||
self.regexEnforce = flag;
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Set delegate and check what methods are implemented.
|
||||
- (void)setDelegate:(id<FeedDownloadDelegate>)observer {
|
||||
_delegate = observer;
|
||||
@@ -134,10 +147,16 @@
|
||||
self.currentDownload = [request dataTask:^(NSData * _Nullable data, NSError * _Nullable error, NSHTTPURLResponse *response) {
|
||||
self.error = error;
|
||||
self.response = response;
|
||||
self.rawData = data;
|
||||
if (!data) { // data = nil if (error || 304)
|
||||
[self performSelectorOnMainThread:@selector(finishAndNotify) withObject:nil waitUntilDone:NO];
|
||||
return;
|
||||
}
|
||||
// if regex is used, no further processing
|
||||
if (self.regexConverter || self.regexEnforce) {
|
||||
[self processWithRegexConverter:self.regexConverter data:data];
|
||||
return;
|
||||
}
|
||||
RSXMLData *xml = [[RSXMLData alloc] initWithData:data url:response.URL];
|
||||
if (!self.assertIsFeedURL && [xml.parserClass isHTMLParser])
|
||||
[self processXMLDataHTML:xml]; // HTML source handling
|
||||
@@ -146,6 +165,30 @@
|
||||
}];
|
||||
}
|
||||
|
||||
/// The downloaded source is HTML data and will be parsed with @c RegexConverter
|
||||
- (void)processWithRegexConverter:(RegexConverter *)converter data:(NSData *)rawData {
|
||||
NSError *err = nil;
|
||||
if (converter) {
|
||||
NSString *theData = [[NSString alloc] initWithData:rawData encoding:NSUTF8StringEncoding];
|
||||
NSArray<RegexFeedEntry*> *matches = [[RegexFeed from:converter] process:theData error:&err];
|
||||
|
||||
RSParsedFeed *feed = [[RSParsedFeed alloc] initWithURL:self.request.URL];
|
||||
feed.link = self.request.URL.absoluteString; // needed for group-menu-item-open
|
||||
for (RegexFeedEntry *rxEntry in matches) {
|
||||
RSParsedArticle *article = [feed appendNewArticle];
|
||||
article.link = rxEntry.href;
|
||||
article.title = rxEntry.title;
|
||||
article.body = rxEntry.desc;
|
||||
article.datePublished = rxEntry.date;
|
||||
}
|
||||
self.xmlfeed = feed;
|
||||
} else {
|
||||
self.xmlfeed = nil;
|
||||
}
|
||||
self.error = err;
|
||||
[self performSelectorOnMainThread:@selector(finishAndNotify) withObject:nil waitUntilDone:NO];
|
||||
}
|
||||
|
||||
/// The downloaded source seems to be HTML data, lets parse it with @c RSXML @c RSHTMLMetadataParser
|
||||
- (void)processXMLDataHTML:(RSXMLData*)xml {
|
||||
RSHTMLMetadataParser *parser = [RSHTMLMetadataParser parserWithXMLData:xml];
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#import "OpmlFile.h"
|
||||
#import "FeedMeta+Ext.h"
|
||||
#import "FeedGroup+Ext.h"
|
||||
#import "RegexConverter+Ext.h"
|
||||
#import "StoreCoordinator.h"
|
||||
#import "Constants.h"
|
||||
#import "NSDate+Ext.h"
|
||||
@@ -120,6 +121,24 @@ static NSInteger RadioGroupSelection(NSView *view) {
|
||||
|
||||
newFeed.feed.meta.url = [item attributeForKey:OPMLXMLURLKey];
|
||||
newFeed.feed.meta.refresh = interval;
|
||||
|
||||
// baRSS specific
|
||||
NSString *rxEntry = [item attributeForKey:@"rxEntry"];
|
||||
NSString *rxHref = [item attributeForKey:@"rxHref"];
|
||||
NSString *rxTitle = [item attributeForKey:@"rxTitle"];
|
||||
NSString *rxDesc = [item attributeForKey:@"rxDesc"];
|
||||
NSString *rxDate = [item attributeForKey:@"rxDate"];
|
||||
NSString *rxDateFormat = [item attributeForKey:@"rxDateFormat"];
|
||||
if (rxEntry || rxHref || rxTitle || rxDesc || rxDate || rxDateFormat) {
|
||||
RegexConverter *rx = [RegexConverter newInContext:moc];
|
||||
rx.entry = rxEntry;
|
||||
rx.href = rxHref;
|
||||
rx.title = rxTitle;
|
||||
rx.desc = rxDesc;
|
||||
rx.date = rxDate;
|
||||
rx.dateFormat = rxDateFormat;
|
||||
newFeed.feed.regex = rx;
|
||||
}
|
||||
} else { // GROUP
|
||||
for (NSUInteger i = 0; i < item.children.count; i++) {
|
||||
[self importFeed:item.children[i] parent:newFeed index:(int32_t)i inContext:moc];
|
||||
@@ -279,6 +298,21 @@ static NSInteger RadioGroupSelection(NSView *view) {
|
||||
[outline addAttribute:[NSXMLNode attributeWithName:OPMLTypeKey stringValue:@"rss"]];
|
||||
NSString *intervalStr = [NSString stringWithFormat:@"%d", item.feed.meta.refresh];
|
||||
[outline addAttribute:[NSXMLNode attributeWithName:@"refreshInterval" stringValue:intervalStr]]; // baRSS specific
|
||||
RegexConverter *rx = item.feed.regex;
|
||||
if (rx) { // baRSS specific
|
||||
if (rx.entry)
|
||||
[outline addAttribute:[NSXMLNode attributeWithName:@"rxEntry" stringValue:rx.entry]];
|
||||
if (rx.href)
|
||||
[outline addAttribute:[NSXMLNode attributeWithName:@"rxHref" stringValue:rx.href]];
|
||||
if (rx.title)
|
||||
[outline addAttribute:[NSXMLNode attributeWithName:@"rxTitle" stringValue:rx.title]];
|
||||
if (rx.desc)
|
||||
[outline addAttribute:[NSXMLNode attributeWithName:@"rxDesc" stringValue:rx.desc]];
|
||||
if (rx.date)
|
||||
[outline addAttribute:[NSXMLNode attributeWithName:@"rxDate" stringValue:rx.date]];
|
||||
if (rx.dateFormat)
|
||||
[outline addAttribute:[NSXMLNode attributeWithName:@"rxDateFormat" stringValue:rx.dateFormat]];
|
||||
}
|
||||
// TODO: option to export unread state?
|
||||
}
|
||||
parent = outline;
|
||||
|
||||
@@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
// Scheduling
|
||||
+ (void)scheduleNextFeed;
|
||||
+ (void)forceUpdateAllFeeds;
|
||||
+ (void)downloadList:(NSArray<Feed*>*)list userInitiated:(BOOL)flag finally:(nullable os_block_t)block;
|
||||
+ (void)downloadList:(NSArray<Feed*>*)list userInitiated:(BOOL)flag notifications:(BOOL)notify finally:(nullable os_block_t)block;
|
||||
+ (void)updateAllFavicons;
|
||||
// Auto Download & Parse Feed URL
|
||||
+ (void)autoDownloadAndParseURL:(NSString*)url;
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
#import "UpdateScheduler.h"
|
||||
#import "Constants.h"
|
||||
#import "StoreCoordinator.h"
|
||||
#import "NotifyEndpoint.h"
|
||||
#import "NSDate+Ext.h"
|
||||
|
||||
#import "FeedDownload.h"
|
||||
#import "FaviconDownload.h"
|
||||
#import "Feed+Ext.h"
|
||||
#import "FeedArticle+Ext.h"
|
||||
#import "FeedMeta+Ext.h"
|
||||
#import "FeedGroup+Ext.h"
|
||||
|
||||
@@ -129,7 +131,7 @@ static _Atomic(NSUInteger) _queueSize = 0;
|
||||
NSArray<Feed*> *list = [StoreCoordinator listOfFeedsThatNeedUpdate:updateAll inContext:moc];
|
||||
//NSAssert(list.count > 0, @"ERROR: Something went wrong, timer fired too early.");
|
||||
|
||||
[self downloadList:list userInitiated:updateAll finally:^{
|
||||
[self downloadList:list userInitiated:updateAll notifications:YES finally:^{
|
||||
[StoreCoordinator saveContext:moc andParent:YES]; // save parents too ...
|
||||
[moc reset];
|
||||
[self scheduleNextFeed]; // always reset the timer
|
||||
@@ -147,7 +149,7 @@ static _Atomic(NSUInteger) _queueSize = 0;
|
||||
}
|
||||
|
||||
/// Download list of feeds. Either silently in background or with alerts in foreground.
|
||||
+ (void)downloadList:(NSArray<Feed*>*)list userInitiated:(BOOL)flag finally:(nullable os_block_t)block {
|
||||
+ (void)downloadList:(NSArray<Feed*>*)list userInitiated:(BOOL)flag notifications:(BOOL)notify finally:(nullable os_block_t)block {
|
||||
if (![self allowNetworkConnection]) {
|
||||
if (block) block();
|
||||
return;
|
||||
@@ -158,7 +160,7 @@ static _Atomic(NSUInteger) _queueSize = 0;
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
for (Feed *f in list) {
|
||||
dispatch_group_enter(group);
|
||||
[self updateFeed:f alert:flag isForced:flag finally:^{
|
||||
[self updateFeed:f alert:flag isForced:flag notifications:notify finally:^{
|
||||
atomic_fetch_sub_explicit(&_queueSize, 1, memory_order_relaxed);
|
||||
PostNotification(kNotificationBackgroundUpdateInProgress, @(_queueSize));
|
||||
dispatch_group_leave(group);
|
||||
@@ -170,7 +172,7 @@ static _Atomic(NSUInteger) _queueSize = 0;
|
||||
/// Helper method to show modal error alert
|
||||
static inline void AlertDownloadError(NSError *err, NSString *url) {
|
||||
NSAlert *alertPopup = [NSAlert alertWithError:err];
|
||||
alertPopup.informativeText = [NSString stringWithFormat:@"Error loading source: %@", url];
|
||||
alertPopup.informativeText = [NSString stringWithFormat:NSLocalizedString(@"Error loading source: %@", nil), url];
|
||||
[alertPopup runModal];
|
||||
}
|
||||
|
||||
@@ -178,7 +180,7 @@ static inline void AlertDownloadError(NSError *err, NSString *url) {
|
||||
Start download request with existing @c Feed object. Reuses etag and modified headers (unless articles count is 0).
|
||||
@note Will post a @c kNotificationArticlesUpdated notification if download was successful and status code is @b not 304.
|
||||
*/
|
||||
+ (void)updateFeed:(Feed*)feed alert:(BOOL)alert isForced:(BOOL)forced finally:(nullable os_block_t)block {
|
||||
+ (void)updateFeed:(Feed*)feed alert:(BOOL)alert isForced:(BOOL)forced notifications:(BOOL)notify finally:(nullable os_block_t)block {
|
||||
NSManagedObjectContext *moc = feed.managedObjectContext;
|
||||
NSManagedObjectID *oid = feed.objectID;
|
||||
[[FeedDownload withFeed:feed forced:forced] startWithBlock:^(FeedDownload *mem) {
|
||||
@@ -188,7 +190,37 @@ static inline void AlertDownloadError(NSError *err, NSString *url) {
|
||||
BOOL recentlyAdded = (f.articles.count == 0); // before copy values
|
||||
BOOL downloadIcon = (!f.hasIcon && (recentlyAdded || forced));
|
||||
BOOL needsNotification = [mem copyValuesTo:f ignoreError:NO];
|
||||
|
||||
// need to gather object before save, because afterwards list will be empty
|
||||
NSArray *inserted = notify ? moc.insertedObjects.allObjects : nil;
|
||||
NSArray *deleted = moc.deletedObjects.allObjects;
|
||||
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
|
||||
// after save, update notifications
|
||||
// dismiss previously delivered notifications
|
||||
if (deleted) {
|
||||
NSMutableArray *ids = [NSMutableArray array];
|
||||
for (FeedArticle *article in deleted) { // will contain non-articles too
|
||||
if ([article isKindOfClass:[FeedArticle class]] || [article isKindOfClass:[Feed class]]) {
|
||||
[ids addObject:article.notificationID];
|
||||
}
|
||||
}
|
||||
[NotifyEndpoint dismiss:ids]; // no-op if empty
|
||||
}
|
||||
// post new notification (if needed)
|
||||
if (notify && inserted) {
|
||||
BOOL didAddAny = NO;
|
||||
for (FeedArticle *article in inserted) { // will contain non-articles too
|
||||
if ([article isKindOfClass:[FeedArticle class]]) {
|
||||
[NotifyEndpoint postArticle:article];
|
||||
didAddAny = YES;
|
||||
}
|
||||
}
|
||||
if (didAddAny)
|
||||
[NotifyEndpoint postFeed:f];
|
||||
}
|
||||
|
||||
if (needsNotification)
|
||||
PostNotification(kNotificationArticlesUpdated, oid);
|
||||
if (downloadIcon && !mem.error) {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#import "DrawImage.h"
|
||||
#import "Constants.h"
|
||||
#import "NSColor+Ext.h"
|
||||
#import "TinySVG.h"
|
||||
|
||||
|
||||
@implementation DrawSeparator
|
||||
@@ -125,7 +126,6 @@ static inline void AddGroupIconPath(CGContextRef c, CGFloat size, BOOL showBackg
|
||||
CGPathRelease(lower);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
Create @c CGPath for RSS icon; a circle in the lower left bottom and two radio waves going outwards.
|
||||
@param connection If @c NO, draw only one radio wave and a pause icon in the upper right
|
||||
@@ -146,22 +146,9 @@ static inline void AddRSSIconPath(CGContextRef c, CGFloat size, BOOL connection)
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Icon Background Generators
|
||||
#pragma mark - Icon Background
|
||||
|
||||
|
||||
/// Create @c CGPath with rounded corners (optional). @param roundness Value between @c 0.0 and @c 1.0
|
||||
static void AddRoundedBackgroundPath(CGContextRef c, CGRect r, CGFloat roundness) {
|
||||
const CGFloat corner = ShorterSide(r.size) * (roundness / 2.0);
|
||||
if (corner > 0) {
|
||||
CGMutablePathRef pth = CGPathCreateMutable();
|
||||
CGPathAddRoundedRect(pth, NULL, r, corner, corner);
|
||||
CGContextAddPath(c, pth);
|
||||
CGPathRelease(pth);
|
||||
} else {
|
||||
CGContextAddRect(c, r);
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert and draw linear gradient with @c color saturation @c ±0.3
|
||||
static void DrawGradient(CGContextRef c, CGFloat size, NSColor *color) {
|
||||
CGFloat h = 0, s = 1, b = 1, a = 1;
|
||||
@@ -190,6 +177,12 @@ static void DrawGradient(CGContextRef c, CGFloat size, NSColor *color) {
|
||||
#pragma mark - CGContext Drawing & Manipulation
|
||||
|
||||
|
||||
/// Flip coordinate system
|
||||
static void FlipCoordinateSystem(CGContextRef c, CGFloat height) {
|
||||
CGContextTranslateCTM(c, 0, height);
|
||||
CGContextScaleCTM(c, 1, -1);
|
||||
}
|
||||
|
||||
/// Scale and translate context to the center with respect to the new scale. If @c width @c != @c length align top left.
|
||||
static void SetContentScale(CGContextRef c, CGSize size, CGFloat scale) {
|
||||
const CGFloat s = ShorterSide(size);
|
||||
@@ -204,7 +197,7 @@ static void DrawRoundedFrame(CGContextRef c, CGRect r, CGColorRef color, BOOL ba
|
||||
CGContextSetStrokeColorWithColor(c, color);
|
||||
CGFloat contentScale = defaultScale;
|
||||
if (background) {
|
||||
AddRoundedBackgroundPath(c, r, corner);
|
||||
svgAddRect(c, 1, r, ShorterSide(r.size) * corner/2);
|
||||
if (scaling != 0.0)
|
||||
contentScale *= scaling;
|
||||
}
|
||||
@@ -277,6 +270,31 @@ static void DrawUnreadIcon(CGRect r, NSColor *color) {
|
||||
CGPathRelease(path);
|
||||
}
|
||||
|
||||
/// Draw "(.*)" as vector path
|
||||
static void DrawRegexIcon(CGRect r) {
|
||||
const CGFloat size = ShorterSide(r.size);
|
||||
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
|
||||
|
||||
svgAddRect(c, 1, r, .2 * size);
|
||||
CGContextSetFillColorWithColor(c, NSColor.redColor.CGColor);
|
||||
CGContextFillPath(c);
|
||||
|
||||
// SVG files use bottom-left corner coordinate system. Quartz uses top-left.
|
||||
FlipCoordinateSystem(c, r.size.height);
|
||||
SetContentScale(c, r.size, 0.8);
|
||||
// "("
|
||||
svgAddPath(c, size/1000, "m184 187c-140 205-134 432-1 622l-66 44c-159-221-151-499 0-708z");
|
||||
// "."
|
||||
svgAddCircle(c, size/1000, 315, 675, 70, NO);
|
||||
// "*"
|
||||
svgAddPath(c, size/1000, "m652 277 107-35 21 63-109 36 68 92-54 39-68-93-66 91-52-41 67-88-109-37 21-63 108 37v-113h66v112z");
|
||||
// ")"
|
||||
svgAddPath(c, size/1000, "m816 813c140-205 134-430 1-621l66-45c159 221 151 499 0 708z");
|
||||
|
||||
CGContextSetFillColorWithColor(c, NSColor.whiteColor.CGColor);
|
||||
CGContextFillPath(c);
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - NSImage Name Registration
|
||||
|
||||
@@ -297,4 +315,5 @@ void RegisterImageViewNames(void) {
|
||||
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; });
|
||||
Register(32, RSSImageRegexIcon, NSLocalizedString(@"Regex icon", nil), ^(NSRect r) { DrawRegexIcon(r); return YES; });
|
||||
}
|
||||
|
||||
5
baRSS/Helper/TinySVG.h
Normal file
5
baRSS/Helper/TinySVG.h
Normal file
@@ -0,0 +1,5 @@
|
||||
@import Cocoa;
|
||||
|
||||
void svgAddPath(CGContextRef context, CGFloat scale, const char * path);
|
||||
void svgAddCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise);
|
||||
void svgAddRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius);
|
||||
171
baRSS/Helper/TinySVG.m
Normal file
171
baRSS/Helper/TinySVG.m
Normal file
@@ -0,0 +1,171 @@
|
||||
#include "TinySVG.h"
|
||||
|
||||
|
||||
struct SVGState {
|
||||
CGFloat scale; // technically not part of parser but easier to pass along
|
||||
|
||||
char op;
|
||||
float x, y;
|
||||
bool prevDot;
|
||||
|
||||
float num[6];
|
||||
uint8 iNum;
|
||||
|
||||
char buf[15];
|
||||
uint8 iBuf;
|
||||
};
|
||||
|
||||
|
||||
# pragma mark - Helper
|
||||
|
||||
/// if number buffer contains anything, write it to num array and start new buffer
|
||||
static void finishNum(struct SVGState *state) {
|
||||
if (state->iBuf > 0) {
|
||||
state->buf[state->iBuf] = '\0';
|
||||
state->num[state->iNum++] = (float)atof(state->buf);
|
||||
state->iBuf = 0;
|
||||
state->prevDot = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// All numbers stored in num array, finalize SVG path operation and add path to @c CGContext
|
||||
static void finishOp(CGMutablePathRef path, struct SVGState *state) {
|
||||
char op = state->op;
|
||||
if (op >= 'a' && op <= 'z') {
|
||||
// convert relative to absolute coordinates
|
||||
for (uint8 t = 0; t < state->iNum; t++) {
|
||||
state->num[t] += (t % 2 || op == 'v') ? state->y : state->x;
|
||||
}
|
||||
// convert to upper-case
|
||||
op = op - 'a' + 'A';
|
||||
}
|
||||
|
||||
if (op == 'Z') {
|
||||
CGPathCloseSubpath(path);
|
||||
|
||||
} else if (op == 'V' && state->iNum == 1) {
|
||||
state->y = state->num[0];
|
||||
CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
|
||||
|
||||
} else if (op == 'H' && state->iNum == 1) {
|
||||
state->x = state->num[0];
|
||||
CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
|
||||
|
||||
} else if (op == 'M' && state->iNum == 2) {
|
||||
state->x = state->num[0];
|
||||
state->y = state->num[1];
|
||||
CGPathMoveToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
|
||||
// Edge-case: "M 1 2 3 4 5 6" is valid SVG after move 1,2 all remaining points are lines (3,4 and 5,6)
|
||||
// For this case we overwrite op here. It will be overwritten again if a new op starts. Else, assume line-op.
|
||||
state->op = (state->op == 'm') ? 'l' : 'L';
|
||||
|
||||
} else if (op == 'L' && state->iNum == 2) {
|
||||
state->x = state->num[0];
|
||||
state->y = state->num[1];
|
||||
CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
|
||||
|
||||
} else if (op == 'C' && state->iNum == 6) {
|
||||
state->x = state->num[4];
|
||||
state->y = state->num[5];
|
||||
CGPathAddCurveToPoint(path, NULL, state->num[0] * state->scale, state->num[1] * state->scale, state->num[2] * state->scale, state->num[3] * state->scale, state->x * state->scale, state->y * state->scale);
|
||||
} else {
|
||||
NSLog(@"Unsupported SVG operation %c %d", state->op, state->iNum);
|
||||
}
|
||||
state->iNum = 0;
|
||||
}
|
||||
|
||||
/// current number not finished yet. Append another char to internal buffer
|
||||
inline static void continueNum(char chr, struct SVGState *state) {
|
||||
state->buf[state->iBuf++] = chr;
|
||||
}
|
||||
|
||||
|
||||
# pragma mark - Parser
|
||||
|
||||
/// very basic svg path parser.
|
||||
static void tinySVG_parse(const char * code, CGFloat scale, CGMutablePathRef path) {
|
||||
struct SVGState state = {
|
||||
.scale = scale,
|
||||
.op = '_',
|
||||
.x = 0,
|
||||
.y = 0,
|
||||
.prevDot = false,
|
||||
|
||||
//.num = {0, 0, 0, 0, 0, 0},
|
||||
.iNum = 0,
|
||||
//.buf = " ",
|
||||
.iBuf = 0,
|
||||
};
|
||||
|
||||
unsigned long len = strlen(code);
|
||||
for (unsigned long i = 0; i < len; i++) {
|
||||
char chr = code[i];
|
||||
if ((chr >= 'a' && chr <= 'z') || (chr >= 'A' && chr <= 'Z')) {
|
||||
if (state.op != '_') {
|
||||
finishNum(&state);
|
||||
finishOp(path, &state);
|
||||
}
|
||||
state.op = chr;
|
||||
} else if (chr >= '0' && chr <= '9') {
|
||||
continueNum(chr, &state);
|
||||
} else if (chr == '-' && state.iBuf == 0) {
|
||||
continueNum(chr, &state);
|
||||
} else if (chr == '.' && !state.prevDot) {
|
||||
continueNum(chr, &state);
|
||||
state.prevDot = true;
|
||||
} else { // any number-separating character
|
||||
finishNum(&state);
|
||||
|
||||
// Edge-Case: SVG can reuse the previous operation without declaration
|
||||
// e.g. you can draw four lines with "L1 2 3 4 5 6 7 8"
|
||||
// or two curves with "c1 2 3 4 5 6 -1 -2 -3 -4 -5 -6"
|
||||
// Therefore we need to complete the operation if the number of arguments is reached
|
||||
if (state.iNum == 1 && strchr("HhVv", state.op) != NULL) {
|
||||
finishOp(path, &state);
|
||||
} else if (state.iNum == 2 && strchr("MmLl", state.op) != NULL) {
|
||||
finishOp(path, &state);
|
||||
} else if (state.iNum == 6 && strchr("Cc", state.op) != NULL) {
|
||||
finishOp(path, &state);
|
||||
}
|
||||
|
||||
if (chr == '-') {
|
||||
continueNum(chr, &state);
|
||||
} else if (chr == '.') {
|
||||
continueNum(chr, &state);
|
||||
state.prevDot = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# pragma mark - External API
|
||||
|
||||
/// calls @c tinySVG_path and handles @c CGPath creation and release.
|
||||
void svgAddPath(CGContextRef context, CGFloat scale, const char * code) {
|
||||
CGMutablePathRef path = CGPathCreateMutable();
|
||||
tinySVG_parse(code, scale, path);
|
||||
CGContextAddPath(context, path);
|
||||
CGPathRelease(path);
|
||||
}
|
||||
|
||||
/// calls @c CGPathAddArc with full circle
|
||||
void svgAddCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise) {
|
||||
CGMutablePathRef tmp = CGPathCreateMutable();
|
||||
CGPathAddArc(tmp, NULL, x * scale, y * scale, radius * scale, 0, M_PI * 2, clockwise);
|
||||
CGContextAddPath(context, tmp);
|
||||
CGPathRelease(tmp);
|
||||
}
|
||||
|
||||
/// Calls @c CGContextAddRect or @c CGPathAddRoundedRect (optional).
|
||||
/// @param cornerRadius Use @c <=0 for no corners. Use half of @c min(w,h) for a full circle.
|
||||
void svgAddRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius) {
|
||||
if (cornerRadius > 0) {
|
||||
CGMutablePathRef tmp = CGPathCreateMutable();
|
||||
CGPathAddRoundedRect(tmp, NULL, rect, cornerRadius * scale, cornerRadius * scale);
|
||||
CGContextAddPath(context, tmp);
|
||||
CGPathRelease(tmp);
|
||||
} else {
|
||||
CGContextAddRect(context, rect);
|
||||
}
|
||||
}
|
||||
@@ -13,13 +13,13 @@
|
||||
/** default: @c nil */ static NSString* const Pref_modalSheetWidth = @"modalSheetWidth";
|
||||
// ------ General settings ------ (Preferences > General Tab) ------
|
||||
/** default: @c nil */ static NSString* const Pref_defaultHttpApplication = @"defaultHttpApplication";
|
||||
/** default: @c nil */ static NSString* const Pref_notificationType = @"notificationType";
|
||||
// ------ Appearance matrix ------ (Preferences > Appearance Tab) ------
|
||||
/** default: @c YES */ static NSString* const Pref_globalTintMenuIcon = @"globalTintMenuBarIcon";
|
||||
/** default: @c YES */ static NSString* const Pref_globalUpdateAll = @"globalUpdateAll";
|
||||
/** default: @c YES */ static NSString* const Pref_globalOpenUnread = @"globalOpenUnread";
|
||||
/** default: @c YES */ static NSString* const Pref_globalMarkRead = @"globalMarkRead";
|
||||
/** default: @c YES */ static NSString* const Pref_globalMarkUnread = @"globalMarkUnread";
|
||||
/** default: @c NO */ static NSString* const Pref_globalUnreadOnly = @"globalUnreadOnly";
|
||||
/** default: @c YES */ static NSString* const Pref_globalUnreadCount = @"globalUnreadCount";
|
||||
/** default: @c YES */ static NSString* const Pref_groupOpenUnread = @"groupOpenUnread";
|
||||
/** default: @c YES */ static NSString* const Pref_groupMarkRead = @"groupMarkRead";
|
||||
@@ -49,6 +49,16 @@
|
||||
|
||||
void UserPrefsInit(void);
|
||||
NSColor* UserPrefsColor(NSString *key, NSColor *defaultColor); // Change with: defaults write de.relikd.baRSS {KEY} -string "#FBA33A"
|
||||
|
||||
typedef NS_ENUM(NSInteger, NotificationType) {
|
||||
NotificationTypeDisabled,
|
||||
NotificationTypePerArticle,
|
||||
NotificationTypePerFeed,
|
||||
NotificationTypeGlobal,
|
||||
};
|
||||
NotificationType UserPrefsNotificationType(void);
|
||||
NSString* NotificationTypeToString(NotificationType typ);
|
||||
|
||||
// ------ Getter ------
|
||||
/// Helper method calls @c (standardUserDefaults)boolForKey:
|
||||
static inline BOOL UserPrefsBool(NSString* const key) { return [[NSUserDefaults standardUserDefaults] boolForKey:key]; }
|
||||
@@ -71,7 +81,7 @@ static inline void UserPrefsSetBool(NSString* const key, BOOL value) { [[NSUserD
|
||||
// ---------------------------------------------------------------
|
||||
|
||||
/// Helper method calls @c (mainBundle)CFBundleShortVersionString
|
||||
static inline NSString* UserPrefsAppVersion() { return [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"]; }
|
||||
static inline NSString* UserPrefsAppVersion(void) { return [[NSBundle mainBundle] infoDictionary][@"CFBundleShortVersionString"]; }
|
||||
|
||||
// ---------------------------------------------------------------
|
||||
// | MARK: - Open URLs
|
||||
|
||||
@@ -20,7 +20,7 @@ void UserPrefsInit(void) {
|
||||
Pref_feedUnreadIndicator
|
||||
]);
|
||||
defaultsAppend(defs, @NO, @[
|
||||
Pref_globalUnreadOnly, Pref_groupUnreadOnly, Pref_feedUnreadOnly,
|
||||
Pref_groupUnreadOnly, Pref_feedUnreadOnly,
|
||||
Pref_groupUnreadIndicator,
|
||||
Pref_feedTruncateTitle,
|
||||
Pref_feedLimitArticles
|
||||
@@ -44,3 +44,22 @@ NSColor* UserPrefsColor(NSString *key, NSColor *defaultColor) {
|
||||
}
|
||||
return defaultColor;
|
||||
}
|
||||
|
||||
/// Convert stored notification type string into enum
|
||||
NotificationType UserPrefsNotificationType(void) {
|
||||
NSString *typ = UserPrefsString(Pref_notificationType);
|
||||
if ([typ isEqualToString:@"article"]) return NotificationTypePerArticle;
|
||||
if ([typ isEqualToString:@"feed"]) return NotificationTypePerFeed;
|
||||
if ([typ isEqualToString:@"global"]) return NotificationTypeGlobal;
|
||||
return NotificationTypeDisabled;
|
||||
}
|
||||
|
||||
/// Convert enum type to storable string
|
||||
NSString* NotificationTypeToString(NotificationType typ) {
|
||||
switch (typ) {
|
||||
case NotificationTypeDisabled: return nil;
|
||||
case NotificationTypePerArticle: return @"article";
|
||||
case NotificationTypePerFeed: return @"feed";
|
||||
case NotificationTypeGlobal: return @"global";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.2.2</string>
|
||||
<string>$(MARKETING_VERSION)</string>
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
<dict>
|
||||
@@ -70,7 +70,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>14866</string>
|
||||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.news</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
@@ -83,7 +83,7 @@
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2020 relikd.</string>
|
||||
<string>Copyright © 2025 relikd.</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
<string>AppHook</string>
|
||||
<key>UTImportedTypeDeclarations</key>
|
||||
|
||||
@@ -51,8 +51,8 @@
|
||||
if (last != '\n') {
|
||||
[result appendString:@"\n"];
|
||||
}
|
||||
if (order > 0) [result appendFormat:@" %d. ", order++];
|
||||
else [result appendString:@" • "];
|
||||
if (order > 0) [result appendFormat:@" %d. ", order++];
|
||||
else [result appendString:@" • "];
|
||||
}
|
||||
} else {
|
||||
// append text inbetween tags
|
||||
@@ -74,7 +74,10 @@
|
||||
// collapsing multiple horizontal whitespaces (\h) into one (the first one)
|
||||
[[NSRegularExpression regularExpressionWithPattern:@"(\\h)[\\h]+" options:0 error:nil]
|
||||
replaceMatchesInString:result options:0 range:NSMakeRange(0, result.length) withTemplate:@"$1"];
|
||||
return [result stringByTrimmingCharactersInSet:NSCharacterSet.whitespaceAndNewlineCharacterSet];
|
||||
|
||||
NSMutableCharacterSet *cs = NSMutableCharacterSet.whitespaceAndNewlineCharacterSet;
|
||||
[cs removeCharactersInString:@" "]; // used for "li"
|
||||
return [result stringByTrimmingCharactersInSet:cs];
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -69,6 +69,11 @@
|
||||
btn.bezelStyle = NSBezelStyleRounded;
|
||||
btn.bordered = NO;
|
||||
btn.image = [NSImage imageNamed:name];
|
||||
NSSize s = btn.image.size;
|
||||
if (s.width > s.height)
|
||||
[btn.image setSize:NSMakeSize(size, size * (s.height / s.width))];
|
||||
else
|
||||
[btn.image setSize:NSMakeSize(size * (s.width / s.height), size)];
|
||||
return btn;
|
||||
}
|
||||
|
||||
|
||||
18
baRSS/Notifications/NotifyEndpoint.h
Normal file
18
baRSS/Notifications/NotifyEndpoint.h
Normal file
@@ -0,0 +1,18 @@
|
||||
@import Cocoa;
|
||||
@import UserNotifications;
|
||||
|
||||
@class Feed, FeedArticle;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NotifyEndpoint : NSObject <UNUserNotificationCenterDelegate>
|
||||
+ (void)activate;
|
||||
|
||||
+ (void)setGlobalCount:(NSInteger)count previousCount:(NSInteger)count;
|
||||
+ (void)postFeed:(Feed*)feed;
|
||||
+ (void)postArticle:(FeedArticle*)article;
|
||||
|
||||
+ (void)dismiss:(nullable NSArray<NSString*>*)list;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
192
baRSS/Notifications/NotifyEndpoint.m
Normal file
192
baRSS/Notifications/NotifyEndpoint.m
Normal file
@@ -0,0 +1,192 @@
|
||||
#import "NotifyEndpoint.h"
|
||||
#import "UserPrefs.h"
|
||||
#import "StoreCoordinator.h"
|
||||
#import "Feed+Ext.h"
|
||||
#import "FeedGroup+Ext.h"
|
||||
#import "FeedArticle+Ext.h"
|
||||
|
||||
/**
|
||||
Sent for global unread count notification alert (Notification Center)
|
||||
*/
|
||||
static NSString* const kNotifyIdGlobal = @"global";
|
||||
|
||||
static NSString* const kCategoryDismissable = @"DISMISSIBLE";
|
||||
static NSString* const kActionOpenBackground = @"OPEN_IN_BACKGROUND";
|
||||
static NSString* const kActionMarkRead = @"MARK_READ_DONT_OPEN";
|
||||
static NSString* const kActionOpenOnly = @"OPEN_ONLY_DONT_MARK_READ";
|
||||
|
||||
|
||||
@implementation NotifyEndpoint
|
||||
|
||||
static NotifyEndpoint *singleton = nil;
|
||||
static NotificationType notifyType;
|
||||
|
||||
/// Ask user for permission to send notifications @b AND register delegate to respond to alert banner clicks.
|
||||
/// @note Called every time user changes notification settings
|
||||
+ (void)activate {
|
||||
UNUserNotificationCenter *center = UNUserNotificationCenter.currentNotificationCenter;
|
||||
notifyType = UserPrefsNotificationType();
|
||||
|
||||
// even if disabled, register delegate. This allows to open previously sent notifications
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
singleton = [NotifyEndpoint new];
|
||||
center.delegate = singleton;
|
||||
});
|
||||
|
||||
if (notifyType == NotificationTypeDisabled) {
|
||||
return;
|
||||
}
|
||||
// register action types (allow mark read without opening notification)
|
||||
UNNotificationAction *openBackgroundAction = [UNNotificationAction actionWithIdentifier:kActionOpenBackground title:NSLocalizedString(@"Open in background", nil) options:UNNotificationActionOptionNone];
|
||||
UNNotificationAction *dontOpenAction = [UNNotificationAction actionWithIdentifier:kActionMarkRead title:NSLocalizedString(@"Mark read & dismiss", nil) options:UNNotificationActionOptionNone];
|
||||
UNNotificationAction *dontReadAction = [UNNotificationAction actionWithIdentifier:kActionOpenOnly title:NSLocalizedString(@"Open but keep unread", nil) options:UNNotificationActionOptionNone];
|
||||
UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:kCategoryDismissable actions:@[openBackgroundAction, dontOpenAction, dontReadAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionNone];
|
||||
[center setNotificationCategories:[NSSet setWithObject:category]];
|
||||
|
||||
[center requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionSound completionHandler:^(BOOL granted, NSError * _Nullable error) {
|
||||
if (error) {
|
||||
dispatch_async(dispatch_get_main_queue(), ^{
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
alert.messageText = NSLocalizedString(@"Notifications Disabled", nil);
|
||||
alert.informativeText = NSLocalizedString(@"Either enable notifications in System Settings, or disable notifications in baRSS settings.", nil);
|
||||
alert.alertStyle = NSAlertStyleInformational;
|
||||
[alert runModal];
|
||||
});
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
/// Set (or update) global "X unread articles"
|
||||
+ (void)setGlobalCount:(NSInteger)newCount previousCount:(NSInteger)oldCount {
|
||||
if (newCount > 0) {
|
||||
if (notifyType != NotificationTypeGlobal) {
|
||||
return;
|
||||
}
|
||||
// TODO: how to handle global count updates?
|
||||
// ignore and keep old count until 0?
|
||||
// or update count and show a new notification banner?
|
||||
if (newCount > oldCount) { // only notify if new feeds (quirk: will also trigger for option-click menu to mark unread)
|
||||
[self send:kNotifyIdGlobal
|
||||
title:APP_NAME
|
||||
body:[NSString stringWithFormat:NSLocalizedString(@"%ld unread articles", nil), newCount]];
|
||||
}
|
||||
} else {
|
||||
[self dismiss:@[kNotifyIdGlobal]];
|
||||
}
|
||||
}
|
||||
|
||||
/// Triggers feed notifications (if enabled)
|
||||
+ (void)postFeed:(Feed*)feed {
|
||||
if (notifyType != NotificationTypePerFeed) {
|
||||
return;
|
||||
}
|
||||
NSUInteger count = feed.countUnread;
|
||||
if (count > 0) {
|
||||
[feed.managedObjectContext obtainPermanentIDsForObjects:@[feed] error:nil];
|
||||
[self send:feed.notificationID
|
||||
title:feed.group.anyName
|
||||
body:[NSString stringWithFormat:NSLocalizedString(@"%ld unread articles", nil), count]];
|
||||
}
|
||||
}
|
||||
|
||||
/// Triggers article notifications (if enabled)
|
||||
+ (void)postArticle:(FeedArticle*)article {
|
||||
if (notifyType != NotificationTypePerArticle) {
|
||||
return;
|
||||
}
|
||||
[article.managedObjectContext obtainPermanentIDsForObjects:@[article] error:nil];
|
||||
[self send:article.notificationID
|
||||
title:article.feed.group.anyName
|
||||
body:article.title];
|
||||
}
|
||||
|
||||
/// Close already posted notifications because they were opened via menu
|
||||
+ (void)dismiss:(nullable NSArray<NSString*>*)list {
|
||||
if (list.count > 0) {
|
||||
[UNUserNotificationCenter.currentNotificationCenter removeDeliveredNotificationsWithIdentifiers:list];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Helper methods
|
||||
|
||||
/// Post notification (immediatelly).
|
||||
/// @param identifier Used to identify a specific instance (and dismiss a previously shown notification).
|
||||
+ (void)send:(NSString *)identifier title:(nullable NSString *)title body:(nullable NSString *)body {
|
||||
UNMutableNotificationContent *msg = [UNMutableNotificationContent new];
|
||||
if (title != nil) msg.title = title;
|
||||
if (body != nil) msg.body = body;
|
||||
// common settings:
|
||||
msg.categoryIdentifier = kCategoryDismissable;
|
||||
// TODO: make sound configurable?
|
||||
msg.sound = [UNNotificationSound defaultSound];
|
||||
[self send:identifier content: msg];
|
||||
}
|
||||
|
||||
/// Internal method for queueing a new notification.
|
||||
+ (void)send:(NSString *)identifier content:(UNMutableNotificationContent*)msg {
|
||||
UNUserNotificationCenter *center = UNUserNotificationCenter.currentNotificationCenter;
|
||||
|
||||
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
|
||||
if (settings.authorizationStatus != UNAuthorizationStatusAuthorized) {
|
||||
return;
|
||||
}
|
||||
|
||||
UNNotificationRequest *req = [UNNotificationRequest requestWithIdentifier:identifier content:msg trigger:nil];
|
||||
[center addNotificationRequest:req withCompletionHandler:^(NSError * _Nullable error) {
|
||||
if (error) {
|
||||
NSLog(@"Could not send notification: %@", error);
|
||||
}
|
||||
}];
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Delegate
|
||||
|
||||
/// Must be implemented to show notifications while the app is in foreground
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
|
||||
// all the options
|
||||
UNNotificationPresentationOptions common = UNNotificationPresentationOptionSound | UNNotificationPresentationOptionBadge;
|
||||
if (@available(macOS 11.0, *)) {
|
||||
completionHandler(common | UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionList);
|
||||
} else {
|
||||
completionHandler(common | UNNotificationPresentationOptionAlert);
|
||||
}
|
||||
}
|
||||
|
||||
/// Callback method when user clicks on alert banner
|
||||
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
|
||||
NSArray<FeedArticle*> *articles;
|
||||
|
||||
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
|
||||
NSString *theId = response.notification.request.identifier;
|
||||
if ([theId isEqualToString:kNotifyIdGlobal]) {
|
||||
// global notification
|
||||
articles = [StoreCoordinator articlesAtPath:nil isFeed:NO sorted:YES unread:YES inContext:moc limit:0];
|
||||
} else {
|
||||
NSURL *uri = [NSURL URLWithString:theId];
|
||||
NSManagedObjectID *oid = [moc.persistentStoreCoordinator managedObjectIDForURIRepresentation:uri];
|
||||
NSManagedObject *obj = [moc objectWithID:oid];
|
||||
if ([obj isKindOfClass:[FeedArticle class]]) {
|
||||
// per-article notification
|
||||
articles = @[(FeedArticle*)obj];
|
||||
} else if ([obj isKindOfClass:[Feed class]]) {
|
||||
// per-feed notification
|
||||
articles = [[[(Feed*)obj articles]
|
||||
filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"unread = 1"]]
|
||||
sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:NO]]];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// open-in-background performs the same operation as a normal click
|
||||
// the "background" part is triggered by _NOT_ having the UNNotificationActionOptionForeground option
|
||||
BOOL dontOpen = [response.actionIdentifier isEqualToString:kActionMarkRead];
|
||||
BOOL dontMarkRead = [response.actionIdentifier isEqualToString:kActionOpenOnly];
|
||||
[StoreCoordinator updateArticles:articles markRead:!dontMarkRead andOpen:!dontOpen inContext:moc];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -28,9 +28,9 @@
|
||||
[self entry:NSLocalizedString(@"Open all unread", nil) c1:Pref_globalOpenUnread c2:Pref_groupOpenUnread c3:Pref_feedOpenUnread];
|
||||
[self entry:NSLocalizedString(@"Mark all read", nil) c1:Pref_globalMarkRead c2:Pref_groupMarkRead c3:Pref_feedMarkRead];
|
||||
[self entry:NSLocalizedString(@"Mark all unread", nil) c1:Pref_globalMarkUnread c2:Pref_groupMarkUnread c3:Pref_feedMarkUnread];
|
||||
[self entry:NSLocalizedString(@"Show only unread / hide read", nil) c1:Pref_globalUnreadOnly c2:Pref_groupUnreadOnly c3:Pref_feedUnreadOnly];
|
||||
[self entry:NSLocalizedString(@"Number of unread articles", nil) c1:Pref_globalUnreadCount c2:Pref_groupUnreadCount c3:Pref_feedUnreadCount];
|
||||
[self entry:NSLocalizedString(@"Indicator for unread articles", nil) c1:nil c2:Pref_groupUnreadIndicator c3:Pref_feedUnreadIndicator];
|
||||
[self entry:NSLocalizedString(@"Show only unread / hide read", nil) c1:nil c2:Pref_groupUnreadOnly c3:Pref_feedUnreadOnly];
|
||||
[[self entry:NSLocalizedString(@"Truncate article title", nil) c1:nil c2:nil c3:Pref_feedTruncateTitle]
|
||||
tooltip:NSLocalizedString(@"Truncate article title after 60 characters", nil)];
|
||||
[[self entry:NSLocalizedString(@"Limit number of articles", nil) c1:nil c2:nil c3:Pref_feedLimitArticles]
|
||||
|
||||
@@ -13,6 +13,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface ModalFeedEdit : ModalEditDialog <NSTextFieldDelegate>
|
||||
- (void)didClickWarningButton:(NSButton*)sender;
|
||||
- (void)openRegexConverter;
|
||||
@end
|
||||
|
||||
@interface ModalGroupEdit : ModalEditDialog
|
||||
|
||||
@@ -11,6 +11,9 @@
|
||||
#import "NSView+Ext.h"
|
||||
#import "NSDate+Ext.h"
|
||||
#import "NSURL+Ext.h"
|
||||
#import "RegexConverterController.h"
|
||||
#import "RegexConverterModal.h"
|
||||
#import "RegexConverter+Ext.h"
|
||||
|
||||
// ################################################################
|
||||
// #
|
||||
@@ -59,6 +62,9 @@
|
||||
@property (strong) FeedDownload *memFeed;
|
||||
@property (weak) FaviconDownload *memIcon;
|
||||
@property (strong) RefreshStatisticsView *statisticsView;
|
||||
@property (nonatomic, assign) BOOL skipIconDownload;
|
||||
@property (nonatomic, assign) BOOL openRegexAfterDownload;
|
||||
@property (weak) id eventMonitor;
|
||||
@end
|
||||
|
||||
@implementation ModalFeedEdit
|
||||
@@ -71,6 +77,13 @@
|
||||
self.view.refreshNum.intValue = 30;
|
||||
[NSDate populateUnitsMenu:self.view.refreshUnit selected:TimeUnitMinutes];
|
||||
[self populateTextFields:self.feedGroup];
|
||||
|
||||
// removed in windowShouldClose:
|
||||
self.eventMonitor = [NSEvent addLocalMonitorForEventsMatchingMask:NSEventMaskFlagsChanged handler:^(NSEvent *event) {
|
||||
BOOL optionKeyActive = ((event.modifierFlags & NSEventModifierFlagOption) != 0);
|
||||
self.view.regexConverterButton.hidden = !self.feedGroup.feed.regex && !optionKeyActive;
|
||||
return event;
|
||||
}];
|
||||
}
|
||||
|
||||
/// Pre-fill UI control field values with @c FeedGroup properties.
|
||||
@@ -81,6 +94,7 @@
|
||||
self.view.url.objectValue = fg.feed.meta.url;
|
||||
self.previousURL = self.view.url.stringValue;
|
||||
self.view.favicon.image = [fg.feed iconImage16];
|
||||
self.view.regexConverterButton.hidden = !fg.feed.regex;
|
||||
[NSDate setInterval:fg.feed.meta.refresh forPopup:self.view.refreshUnit andField:self.view.refreshNum animate:NO];
|
||||
[self statsForCoreDataObject];
|
||||
}
|
||||
@@ -102,7 +116,8 @@
|
||||
[f.meta setRefreshIfChanged:intv];
|
||||
if (self.memFeed) {
|
||||
[self.memFeed copyValuesTo:f ignoreError:YES];
|
||||
[f setNewIcon:self.faviconFile]; // only if downloaded anything (nil deletes icon!)
|
||||
if (self.faviconFile) // only if downloaded anything (nil deletes icon!)
|
||||
[f setNewIcon:self.faviconFile];
|
||||
self.faviconFile = nil;
|
||||
}
|
||||
}
|
||||
@@ -121,9 +136,11 @@
|
||||
- (void)downloadRSS {
|
||||
[self cancelDownloads];
|
||||
[self.modalSheet setDoneEnabled:NO]; // prevent user from closing the dialog during download
|
||||
[self.view.spinnerURL startAnimation:nil];
|
||||
[self.view.spinnerName startAnimation:nil];
|
||||
if (!self.skipIconDownload) {
|
||||
[self.view.spinnerURL startAnimation:nil];
|
||||
self.view.favicon.image = nil;
|
||||
}
|
||||
self.view.warningButton.hidden = YES;
|
||||
// User didn't change title since last fetch. Will be pre-filled with new title after download
|
||||
if ([self.view.name.stringValue isEqualToString:self.view.name.placeholderString]) {
|
||||
@@ -131,7 +148,9 @@
|
||||
self.view.name.placeholderString = NSLocalizedString(@"Loading …", nil);
|
||||
}
|
||||
self.previousURL = self.view.url.stringValue;
|
||||
self.memFeed = [[FeedDownload withURL:self.previousURL] startWithDelegate:self];
|
||||
self.memFeed = [[[FeedDownload withURL:self.previousURL]
|
||||
withRegex:self.feedGroup.feed.regex enforce:self.openRegexAfterDownload]
|
||||
startWithDelegate:self];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -182,7 +201,7 @@
|
||||
self.view.favicon.hidden = hasError;
|
||||
self.view.warningButton.hidden = !hasError;
|
||||
// Start favicon download
|
||||
if (hasError)
|
||||
if (hasError || self.skipIconDownload)
|
||||
[self downloadComplete];
|
||||
else
|
||||
self.memIcon = [[sender faviconDownload] startWithDelegate:self];
|
||||
@@ -210,7 +229,46 @@
|
||||
- (void)downloadComplete {
|
||||
[self.view.spinnerURL stopAnimation:nil];
|
||||
[self.modalSheet setDoneEnabled:YES];
|
||||
self.skipIconDownload = NO;
|
||||
|
||||
if (self.openRegexAfterDownload) {
|
||||
[self openRegexConverter];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Regex Converter
|
||||
|
||||
- (void)openRegexConverter {
|
||||
if (!self.openRegexAfterDownload) {
|
||||
self.openRegexAfterDownload = YES;
|
||||
self.skipIconDownload = self.feedGroup.feed.hasIcon;
|
||||
[self downloadRSS];
|
||||
return;
|
||||
}
|
||||
self.openRegexAfterDownload = NO;
|
||||
|
||||
// shrink FeedEdit modal size to effectively hide it behind new modal
|
||||
NSRect previous = self.modalSheet.frame;
|
||||
CGFloat minWidthDiff = previous.size.width - self.modalSheet.minSize.width;
|
||||
[self.modalSheet setFrame:NSInsetRect(previous, minWidthDiff / 2.0, 0) display:NO];
|
||||
|
||||
RegexConverterController *c = [RegexConverterController withData:self.memFeed.rawData andConverter:self.feedGroup.feed.regex];
|
||||
[self.modalSheet.sheetParent beginCriticalSheet:[c getModalSheet] completionHandler:^(NSModalResponse returnCode) {
|
||||
// reset previous size
|
||||
[self.modalSheet setFrame:previous display:NO];
|
||||
|
||||
if (returnCode == NSModalResponseOK) {
|
||||
[c applyChanges:self.feedGroup.feed];
|
||||
self.skipIconDownload = self.feedGroup.feed.hasIcon;
|
||||
self.view.regexConverterButton.hidden = !self.feedGroup.feed.regex;
|
||||
[self downloadRSS];
|
||||
} else {
|
||||
[self populateTextFields:self.feedGroup];
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Feed Statistics
|
||||
|
||||
@@ -264,6 +322,7 @@
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:NSControlTextDidEndEditingNotification object:self.view.url];
|
||||
return NO;
|
||||
}
|
||||
[NSEvent removeMonitor:self.eventMonitor];
|
||||
return YES;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN
|
||||
@property NSPopover *warningPopover;
|
||||
@property (strong) IBOutlet NSTextField *warningText;
|
||||
@property (strong) IBOutlet NSButton *warningReload;
|
||||
@property (strong) IBOutlet NSButton *regexConverterButton;
|
||||
|
||||
- (instancetype)initWithController:(ModalFeedEdit*)controller NS_DESIGNATED_INITIALIZER;
|
||||
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#import "ModalFeedEditView.h"
|
||||
#import "ModalFeedEdit.h"
|
||||
#import "NSView+Ext.h"
|
||||
#import "Constants.h"
|
||||
|
||||
@interface StrictUIntFormatter : NSFormatter
|
||||
@end
|
||||
@@ -25,7 +26,8 @@
|
||||
self.url = [[[NSView inputField:@"https://example.org/feed.rss" width:0] placeIn:self x:x yTop:0] sizeToRight:PAD_S + 18];
|
||||
self.spinnerURL = [[NSView activitySpinner] placeIn:self xRight:1 yTop:2.5];
|
||||
self.favicon = [[[NSView imageView:nil size:18] tooltip:NSLocalizedString(@"Favicon", nil)] placeIn:self xRight:0 yTop:1.5];
|
||||
self.warningButton = [[[[NSView buttonIcon:NSImageNameCaution size:18] action:@selector(didClickWarningButton:) target:nil] // up the responder chain
|
||||
self.warningButton = [[[[NSView buttonIcon:NSImageNameCaution size:18]
|
||||
action:@selector(didClickWarningButton:) target:nil] // up the responder chain
|
||||
tooltip:NSLocalizedString(@"Click here to show failure reason", nil)]
|
||||
placeIn:self xRight:0 yTop:1.5];
|
||||
// 2. row
|
||||
@@ -34,6 +36,10 @@
|
||||
// 3. row
|
||||
self.refreshNum = [[NSView inputField:@"30" width:85] placeIn:self x:x yTop:2*rowHeight];
|
||||
self.refreshUnit = [[NSView popupButton:120] placeIn:self x:NSMaxX(self.refreshNum.frame) + PAD_M yTop:2*rowHeight];
|
||||
self.regexConverterButton = [[[[NSView buttonIcon:RSSImageRegexIcon size:19]
|
||||
action:@selector(openRegexConverter) target:controller]
|
||||
tooltip:NSLocalizedString(@"Regex converter", nil)]
|
||||
placeIn:self xRight:0 yTop:2*rowHeight + 1];
|
||||
|
||||
// initial state
|
||||
self.url.accessibilityLabel = lbls[0];
|
||||
@@ -41,6 +47,7 @@
|
||||
self.refreshNum.accessibilityLabel = NSLocalizedString(@"Refresh interval", nil);
|
||||
self.url.delegate = controller;
|
||||
self.warningButton.hidden = YES;
|
||||
self.regexConverterButton.hidden = YES;
|
||||
self.refreshNum.formatter = [StrictUIntFormatter new]; // see below ...
|
||||
[self prepareWarningPopover];
|
||||
return self;
|
||||
|
||||
@@ -138,7 +138,7 @@ const NSPasteboardType dragReorder = @"de.relikd.baRSS.drag-reorder";
|
||||
if (selection.count > 0)
|
||||
[self.dataStore setSelectionIndexPaths:[selection sortedArrayUsingSelector:@selector(compare:)]];
|
||||
|
||||
[UpdateScheduler downloadList:feedsList userInitiated:YES finally:^{
|
||||
[UpdateScheduler downloadList:feedsList userInitiated:YES notifications:NO finally:^{
|
||||
[self endCoreDataChangeUndoEmpty:NO forceUndo:NO];
|
||||
for (Feed *f in feedsList)
|
||||
[moc refreshObject:f.group mergeChanges:NO]; // fixes blank icon if imported with no inet conn
|
||||
|
||||
@@ -3,8 +3,9 @@
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SettingsGeneral : NSViewController
|
||||
- (void)changeHttpApplication:(NSPopUpButton *)sender;
|
||||
- (void)clickHowToDefaults:(NSButton *)sender;
|
||||
- (void)changeHttpApplication:(NSPopUpButton *)sender;
|
||||
- (void)changeNotificationType:(NSPopUpButton *)sender;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#import "StoreCoordinator.h"
|
||||
#import "Constants.h"
|
||||
#import "SettingsGeneralView.h"
|
||||
#import "NotifyEndpoint.h"
|
||||
|
||||
@interface SettingsGeneral()
|
||||
@property (strong) IBOutlet SettingsGeneralView *view; // override
|
||||
@@ -13,19 +14,38 @@
|
||||
|
||||
- (void)loadView {
|
||||
self.view = [[SettingsGeneralView alloc] initWithController:self];
|
||||
// Default http application for opening the feed urls
|
||||
NSPopUpButton *pop = self.view.popupHttpApplication;
|
||||
[pop removeAllItems];
|
||||
[pop addItemWithTitle:NSLocalizedString(@"System Default", @"Default web browser application")];
|
||||
NSArray<NSString*> *browsers = CFBridgingRelease(LSCopyAllHandlersForURLScheme(CFSTR("https")));
|
||||
for (NSString *bundleID in browsers) {
|
||||
[pop addItemWithTitle: [self applicationNameForBundleId:bundleID]];
|
||||
pop.lastItem.representedObject = bundleID;
|
||||
}
|
||||
[pop selectItemAtIndex:[pop indexOfItemWithRepresentedObject:UserPrefsString(Pref_defaultHttpApplication)]];
|
||||
|
||||
// Default RSS Reader application
|
||||
NSString *feedBundleId = CFBridgingRelease(LSCopyDefaultHandlerForURLScheme(CFSTR("feed")));
|
||||
self.view.defaultReader.objectValue = [self applicationNameForBundleId:feedBundleId];
|
||||
|
||||
// Default http application for opening the feed urls
|
||||
NSPopUpButton *defaultApp = self.view.popupHttpApplication;
|
||||
[defaultApp removeAllItems];
|
||||
[defaultApp addItemWithTitle:NSLocalizedString(@"System Default", @"Default web browser application")];
|
||||
NSArray<NSString*> *browsers = CFBridgingRelease(LSCopyAllHandlersForURLScheme(CFSTR("https")));
|
||||
for (NSString *bundleID in browsers) {
|
||||
[defaultApp addItemWithTitle: [self applicationNameForBundleId:bundleID]];
|
||||
defaultApp.lastItem.representedObject = bundleID;
|
||||
}
|
||||
[defaultApp selectItemAtIndex:[defaultApp indexOfItemWithRepresentedObject:UserPrefsString(Pref_defaultHttpApplication)]];
|
||||
|
||||
// Notification settings (disabled, per article, per feed, total)
|
||||
NSPopUpButton *notify = self.view.popupNotificationType;
|
||||
[notify removeAllItems];
|
||||
[notify addItemsWithTitles:@[
|
||||
NSLocalizedString(@"Disabled", @"No notifications"),
|
||||
NSLocalizedString(@"Per Article", nil),
|
||||
NSLocalizedString(@"Per Feed", nil),
|
||||
NSLocalizedString(@"Global “X unread articles”", nil),
|
||||
]];
|
||||
notify.itemArray[0].representedObject = NotificationTypeToString(NotificationTypeDisabled);
|
||||
notify.itemArray[1].representedObject = NotificationTypeToString(NotificationTypePerArticle);
|
||||
notify.itemArray[2].representedObject = NotificationTypeToString(NotificationTypePerFeed);
|
||||
notify.itemArray[3].representedObject = NotificationTypeToString(NotificationTypeGlobal);
|
||||
NotificationType savedType = UserPrefsNotificationType();
|
||||
[notify selectItemAtIndex:[notify indexOfItemWithRepresentedObject:NotificationTypeToString(savedType)]];
|
||||
self.view.notificationHelp.stringValue = [self notificationHelpString:savedType];
|
||||
}
|
||||
|
||||
/// Get human readable application name such as 'Safari' or 'baRSS'
|
||||
@@ -41,11 +61,6 @@
|
||||
|
||||
#pragma mark - User interaction
|
||||
|
||||
// Callback method fired when user selects a different item from popup list
|
||||
- (void)changeHttpApplication:(NSPopUpButton *)sender {
|
||||
UserPrefsSet(Pref_defaultHttpApplication, sender.selectedItem.representedObject);
|
||||
}
|
||||
|
||||
// Callback method from round help button right of default feed reader text
|
||||
- (void)clickHowToDefaults:(NSButton *)sender {
|
||||
NSAlert *alert = [[NSAlert alloc] init];
|
||||
@@ -63,4 +78,29 @@
|
||||
|
||||
// x-apple.systempreferences:com.apple.preferences.users?startupItemsPref
|
||||
|
||||
// Callback method fired when user selects a different item from popup list
|
||||
- (void)changeHttpApplication:(NSPopUpButton *)sender {
|
||||
UserPrefsSet(Pref_defaultHttpApplication, sender.selectedItem.representedObject);
|
||||
}
|
||||
|
||||
- (void)changeNotificationType:(NSPopUpButton *)sender {
|
||||
UserPrefsSet(Pref_notificationType, sender.selectedItem.representedObject);
|
||||
self.view.notificationHelp.stringValue = [self notificationHelpString:UserPrefsNotificationType()];
|
||||
[NotifyEndpoint activate];
|
||||
}
|
||||
|
||||
/// Help string explaining the different notification settings (for the current configuration)
|
||||
- (NSString*)notificationHelpString:(NotificationType)typ {
|
||||
switch (typ) {
|
||||
case NotificationTypeDisabled:
|
||||
return NSLocalizedString(@"Notifications are disabled. You will not get any notifications even if you enable them in System Settings.", nil);
|
||||
case NotificationTypePerArticle:
|
||||
return NSLocalizedString(@"You will get a notification for each article (“Feed Title: Article Title”). A click on the notification banner opens the article link and marks the item as read.", nil);
|
||||
case NotificationTypePerFeed:
|
||||
return NSLocalizedString(@"You will get a notification for each feed whenever one or more new articles are published (“Feed Title: X unread articles”). A click on the notification banner will open all unread articles of that feed.", nil);
|
||||
case NotificationTypeGlobal:
|
||||
return NSLocalizedString(@"You will get a single notification for all feeds combined (“baRSS: X unread articles”). A click on the notification banner will open all unread articles of all feeds.", nil);
|
||||
}
|
||||
}
|
||||
|
||||
@end
|
||||
|
||||
@@ -4,8 +4,10 @@
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SettingsGeneralView : NSView
|
||||
@property (strong) IBOutlet NSPopUpButton* popupHttpApplication;
|
||||
@property (strong) IBOutlet NSTextField *defaultReader;
|
||||
@property (strong) IBOutlet NSPopUpButton* popupHttpApplication;
|
||||
@property (strong) IBOutlet NSPopUpButton* popupNotificationType;
|
||||
@property (strong) IBOutlet NSTextField* notificationHelp;
|
||||
|
||||
- (instancetype)initWithController:(SettingsGeneral*)controller NS_DESIGNATED_INITIALIZER;
|
||||
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
|
||||
|
||||
@@ -6,15 +6,29 @@
|
||||
|
||||
- (instancetype)initWithController:(SettingsGeneral*)controller {
|
||||
self = [super initWithFrame:NSMakeRect(0, 0, 320, 327)];
|
||||
|
||||
// Change default feed reader application
|
||||
NSTextField *l1 = [[NSView label:NSLocalizedString(@"Default feed reader:", nil)] placeIn:self x:PAD_WIN yTop:PAD_WIN + 3];
|
||||
NSButton *help = [[[NSView helpButton] action:@selector(clickHowToDefaults:) target:controller] placeIn:self xRight:PAD_WIN yTop:PAD_WIN];
|
||||
self.defaultReader = [[[[NSView label:@""] bold] placeIn:self x:NSMaxX(l1.frame) + PAD_S yTop:PAD_WIN + 3] sizeToRight:NSWidth(help.frame) + PAD_WIN];
|
||||
|
||||
// Popup button 'Open URLs with:'
|
||||
CGFloat y = YFromTop(help) + PAD_M;
|
||||
NSTextField *l2 = [[NSView label:NSLocalizedString(@"Open URLs with:", nil)] placeIn:self x:PAD_WIN yTop:y + 1];
|
||||
self.popupHttpApplication = [[[[NSView popupButton:0] placeIn:self x:NSMaxX(l2.frame) + PAD_S yTop:y] sizeToRight:PAD_WIN]
|
||||
action:@selector(changeHttpApplication:) target:controller];
|
||||
|
||||
// Notification type
|
||||
y = YFromTop(self.popupHttpApplication) + PAD_M;
|
||||
NSTextField *l3 = [[NSView label:NSLocalizedString(@"Notifications:", nil)] placeIn:self x:PAD_WIN yTop:y + 1];
|
||||
self.popupNotificationType = [[[[NSView popupButton:0] placeIn:self x:NSMaxX(l3.frame) + PAD_S yTop:y] sizeToRight:PAD_WIN]
|
||||
action:@selector(changeNotificationType:) target:controller];
|
||||
|
||||
// Notification help text
|
||||
y = YFromTop(self.popupNotificationType) + PAD_M;
|
||||
self.notificationHelp = [[[[[NSView label:@""] gray]
|
||||
multiline:NSMakeSize(320 - 2*PAD_WIN, HEIGHT_LABEL * 5)]
|
||||
placeIn:self x:PAD_WIN yTop:y] sizeToRight:PAD_WIN];
|
||||
return self;
|
||||
}
|
||||
|
||||
|
||||
12
baRSS/Regex Editor/RegexConverterController.h
Normal file
12
baRSS/Regex Editor/RegexConverterController.h
Normal file
@@ -0,0 +1,12 @@
|
||||
@import Cocoa;
|
||||
@class RegexConverter, RegexConverterModal, Feed;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RegexConverterController : NSViewController <NSTextFieldDelegate>
|
||||
+ (instancetype)withData:(NSData *)data andConverter:(nullable RegexConverter*)converter;
|
||||
- (RegexConverterModal*)getModalSheet;
|
||||
- (void)applyChanges:(Feed *)feed;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
125
baRSS/Regex Editor/RegexConverterController.m
Normal file
125
baRSS/Regex Editor/RegexConverterController.m
Normal file
@@ -0,0 +1,125 @@
|
||||
#import "RegexConverterController.h"
|
||||
#import "RegexConverterView.h"
|
||||
#import "RegexConverter+Ext.h"
|
||||
#import "RegexConverterModal.h"
|
||||
#import "RegexFeed.h"
|
||||
#import "Feed+Ext.h"
|
||||
#import "NSURLRequest+Ext.h"
|
||||
|
||||
|
||||
// ################################################################
|
||||
// #
|
||||
// # MARK: - RegexConverterController -
|
||||
// #
|
||||
// ################################################################
|
||||
|
||||
@interface RegexConverterController() <NSWindowDelegate>
|
||||
@property (strong) RegexConverter *converter;
|
||||
@property (strong) RegexConverterModal *modalSheet;
|
||||
@property (strong) IBOutlet RegexConverterView *view; // override
|
||||
|
||||
@property (strong) NSString *theData; // not "copy" because generated in initializer
|
||||
@end
|
||||
|
||||
@implementation RegexConverterController
|
||||
@dynamic view;
|
||||
|
||||
/// Dedicated initializer
|
||||
+ (instancetype)withData:(NSData *)data andConverter:(RegexConverter*)converter {
|
||||
RegexConverterController *diag = [self new];
|
||||
diag.theData = data ? [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]: @"";
|
||||
diag.converter = converter;
|
||||
return diag;
|
||||
}
|
||||
|
||||
- (RegexConverterModal *)getModalSheet {
|
||||
if (!self.modalSheet) {
|
||||
self.modalSheet = [[RegexConverterModal alloc] initWithView:self.view];
|
||||
self.modalSheet.delegate = self;
|
||||
}
|
||||
return self.modalSheet;
|
||||
}
|
||||
|
||||
- (void)loadView {
|
||||
self.view = [[RegexConverterView alloc] initWithController:self];
|
||||
[self populateTextFields:self.converter];
|
||||
[self updateOutput:self.theData];
|
||||
}
|
||||
|
||||
/// Pre-fill UI control field values with @c RegexConverter properties.
|
||||
- (void)populateTextFields:(RegexConverter*)converter {
|
||||
if (converter) {
|
||||
self.view.entry.objectValue = converter.entry;
|
||||
self.view.href.objectValue = converter.href;
|
||||
self.view.title.objectValue = converter.title;
|
||||
self.view.desc.objectValue = converter.desc;
|
||||
self.view.date.objectValue = converter.date;
|
||||
self.view.dateFormat.objectValue = converter.dateFormat;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma mark - Update CoreData
|
||||
|
||||
- (void)applyChanges:(Feed *)feed {
|
||||
BOOL shouldDelete = self.view.entry.stringValue.length == 0;
|
||||
|
||||
if (shouldDelete) {
|
||||
if (feed.regex) {
|
||||
[feed.managedObjectContext deleteObject:feed.regex];
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!feed.regex) {
|
||||
feed.regex = [RegexConverter newInContext:feed.managedObjectContext];
|
||||
}
|
||||
|
||||
[feed.regex setEntryIfChanged:self.view.entry.stringValue];
|
||||
[feed.regex setHrefIfChanged:self.view.href.stringValue];
|
||||
[feed.regex setTitleIfChanged:self.view.title.stringValue];
|
||||
[feed.regex setDescIfChanged:self.view.desc.stringValue];
|
||||
[feed.regex setDateIfChanged:self.view.date.stringValue];
|
||||
[feed.regex setDateFormatIfChanged:self.view.dateFormat.stringValue];
|
||||
}
|
||||
|
||||
#pragma mark - NSTextField Delegate
|
||||
|
||||
- (RegexFeed*)regexParser {
|
||||
RegexFeed *tmp = [RegexFeed new];
|
||||
tmp.rxEntry = self.view.entry.stringValue;
|
||||
tmp.rxHref = self.view.href.stringValue;
|
||||
tmp.rxTitle = self.view.title.stringValue;
|
||||
tmp.rxDesc = self.view.desc.stringValue;
|
||||
tmp.rxDate = self.view.date.stringValue;
|
||||
tmp.dateFormat = self.view.dateFormat.stringValue;
|
||||
return tmp;
|
||||
}
|
||||
|
||||
- (void)controlTextDidEndEditing:(NSNotification*)obj {
|
||||
if (self.view.entry.stringValue.length == 0) {
|
||||
[self updateOutput:self.theData];
|
||||
return;
|
||||
}
|
||||
|
||||
NSError *err = nil;
|
||||
NSArray<RegexFeedEntry*> *matches = [[self regexParser] process:self.theData error:&err];
|
||||
if (err) {
|
||||
[self updateOutput:[NSString stringWithFormat:@"%@\n––––\n%@",
|
||||
err.localizedDescription, err.localizedRecoverySuggestion]];
|
||||
return;
|
||||
}
|
||||
|
||||
NSMutableString *rv = [NSMutableString new];
|
||||
for (RegexFeedEntry *entry in matches) {
|
||||
[rv appendFormat:@"%@\n\n$_href: %@\n$_title: %@\n$_date: %@ -> %@\n$_description: %@\n\n----------\n\n",
|
||||
entry.rawMatch, entry.href, entry.title, entry.dateString, entry.date, entry.desc];
|
||||
}
|
||||
|
||||
[self updateOutput:rv];
|
||||
}
|
||||
|
||||
- (void)updateOutput:(NSString *)text {
|
||||
[self.view.output.textStorage setAttributedString:[[NSAttributedString alloc] initWithString:text]];
|
||||
}
|
||||
|
||||
@end
|
||||
12
baRSS/Regex Editor/RegexConverterModal.h
Normal file
12
baRSS/Regex Editor/RegexConverterModal.h
Normal file
@@ -0,0 +1,12 @@
|
||||
@import Cocoa;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RegexConverterModal : NSPanel
|
||||
@property (readonly) BOOL didTapCancel;
|
||||
|
||||
- (instancetype)initWithContentRect:(NSRect)contentRect styleMask:(NSWindowStyleMask)style backing:(NSBackingStoreType)backingStoreType defer:(BOOL)flag NS_UNAVAILABLE;
|
||||
- (instancetype)initWithView:(NSView*)content NS_DESIGNATED_INITIALIZER;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
60
baRSS/Regex Editor/RegexConverterModal.m
Normal file
60
baRSS/Regex Editor/RegexConverterModal.m
Normal file
@@ -0,0 +1,60 @@
|
||||
#import "RegexConverterModal.h"
|
||||
#import "UserPrefs.h"
|
||||
#import "NSView+Ext.h"
|
||||
|
||||
@interface RegexConverterModal()
|
||||
@property (assign) BOOL respondToShouldClose;
|
||||
@end
|
||||
|
||||
@implementation RegexConverterModal
|
||||
|
||||
/// Designated initializer. 'Done' and 'Cancel' buttons will be added automatically.
|
||||
- (instancetype)initWithView:(NSView*)content {
|
||||
static CGFloat const contentOffsetY = PAD_WIN + HEIGHT_BUTTON + PAD_L;
|
||||
|
||||
CGSize sz = content.frame.size;
|
||||
sz.width += 2 * (NSInteger)PAD_WIN;
|
||||
sz.height += PAD_WIN + contentOffsetY; // the second PAD_WIN is already in contentOffsetY
|
||||
|
||||
NSWindowStyleMask style = NSWindowStyleMaskTitled | NSWindowStyleMaskResizable | NSWindowStyleMaskFullSizeContentView;
|
||||
self = [super initWithContentRect:NSMakeRect(0, 0, sz.width, sz.height) styleMask:style backing:NSBackingStoreBuffered defer:NO];
|
||||
[content placeIn:self.contentView x:PAD_WIN y:contentOffsetY];
|
||||
|
||||
self.minSize = sz;
|
||||
|
||||
// Add default interaction buttons
|
||||
NSButton *btnDone = [self createButton:NSLocalizedString(@"Done", nil) atX:PAD_WIN];
|
||||
NSButton *btnCancel = [self createButton:NSLocalizedString(@"Cancel", nil) atX:sz.width - NSMinX(btnDone.frame) + PAD_M];
|
||||
btnDone.tag = 42; // mark 'Done' button
|
||||
//btnDone.keyEquivalent = @"\r"; // Enter / Return
|
||||
btnCancel.keyEquivalent = [NSString stringWithFormat:@"%c", 0x1b]; // ESC
|
||||
return self;
|
||||
}
|
||||
|
||||
/// Helper method to create bottom-right aligned button.
|
||||
- (NSButton*)createButton:(NSString*)text atX:(CGFloat)x {
|
||||
return [[[NSView button:text] action:@selector(didTapButton:) target:self] placeIn:self.contentView xRight:x y:PAD_WIN];
|
||||
}
|
||||
|
||||
/// Sets bool for future usage
|
||||
- (void)setDelegate:(id<NSWindowDelegate>)delegate {
|
||||
[super setDelegate:delegate];
|
||||
self.respondToShouldClose = [delegate respondsToSelector:@selector(windowShouldClose:)];
|
||||
}
|
||||
|
||||
/**
|
||||
Called after user has clicked the 'Done' (Return) or 'Cancel' (Esc) button.
|
||||
In the later case set @c .didTapCancel @c = @c YES
|
||||
*/
|
||||
- (void)didTapButton:(NSButton*)sender {
|
||||
BOOL successful = (sender.tag == 42); // 'Done' button
|
||||
_didTapCancel = !successful;
|
||||
if (self.respondToShouldClose && ![self.delegate windowShouldClose:self]) {
|
||||
return;
|
||||
}
|
||||
// Remove subviews to avoid _NSKeyboardFocusClipView issues
|
||||
[self.contentView.subviews makeObjectsPerformSelector:@selector(removeFromSuperview)];
|
||||
[self.sheetParent endSheet:self returnCode:(successful ? NSModalResponseOK : NSModalResponseCancel)];
|
||||
}
|
||||
|
||||
@end
|
||||
20
baRSS/Regex Editor/RegexConverterView.h
Normal file
20
baRSS/Regex Editor/RegexConverterView.h
Normal file
@@ -0,0 +1,20 @@
|
||||
@import Cocoa;
|
||||
@class RegexConverter, RegexConverterController;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RegexConverterView : NSView
|
||||
@property (strong) IBOutlet NSTextField *entry;
|
||||
@property (strong) IBOutlet NSTextField *href;
|
||||
@property (strong) IBOutlet NSTextField *title;
|
||||
@property (strong) IBOutlet NSTextField *date;
|
||||
@property (strong) IBOutlet NSTextField *dateFormat;
|
||||
@property (strong) IBOutlet NSTextField *desc;
|
||||
@property (strong) IBOutlet NSTextView *output;
|
||||
|
||||
- (instancetype)initWithController:(RegexConverterController*)controller NS_DESIGNATED_INITIALIZER;
|
||||
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;
|
||||
- (nullable instancetype)initWithCoder:(NSCoder *)decoder NS_UNAVAILABLE;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
141
baRSS/Regex Editor/RegexConverterView.m
Normal file
141
baRSS/Regex Editor/RegexConverterView.m
Normal file
@@ -0,0 +1,141 @@
|
||||
#import "RegexConverterView.h"
|
||||
#import "RegexConverterController.h"
|
||||
#import "RegexConverter+Ext.h"
|
||||
#import "NSDate+Ext.h"
|
||||
#import "NSView+Ext.h"
|
||||
|
||||
@interface RegexConverterView()
|
||||
@property NSPopover *infoPopover;
|
||||
@property (strong) IBOutlet NSTextField *popoverText;
|
||||
@property (strong) IBOutlet NSButton *infoButtonEntry;
|
||||
@end
|
||||
|
||||
|
||||
@implementation RegexConverterView
|
||||
|
||||
static CGFloat const heightHowTo = 2 * HEIGHT_LABEL_SMALL;
|
||||
static CGFloat const heightOutput = 150;
|
||||
static CGFloat const heightRow = PAD_S + HEIGHT_INPUTFIELD;
|
||||
|
||||
- (instancetype)initWithController:(RegexConverterController*)controller {
|
||||
NSArray *lbls = @[
|
||||
NSLocalizedString(@"Entries", nil),
|
||||
NSLocalizedString(@"Link", nil),
|
||||
NSLocalizedString(@"Title", nil),
|
||||
NSLocalizedString(@"Description", nil),
|
||||
NSLocalizedString(@"Date", nil),
|
||||
NSLocalizedString(@"Date Format", nil),
|
||||
];
|
||||
NSView *labels = [NSView labelColumn:lbls rowHeight:HEIGHT_INPUTFIELD padding:PAD_S];
|
||||
|
||||
self = [super initWithFrame:NSMakeRect(0, 0, 420, heightHowTo + PAD_L + NSHeight(labels.frame) + PAD_L + heightOutput)];
|
||||
self.autoresizingMask = NSViewWidthSizable | NSViewHeightSizable;
|
||||
|
||||
[self makeHowTo];
|
||||
|
||||
CGFloat x = NSWidth(labels.frame) + PAD_S;
|
||||
[labels placeIn:self x:0 yTop:heightHowTo + PAD_L];
|
||||
|
||||
self.entry = [self inputAndExamples:0 x:x delegate:controller];
|
||||
self.href = [self inputAndExamples:1 x:x delegate:controller];
|
||||
self.title = [self inputAndExamples:2 x:x delegate:controller];
|
||||
self.desc = [self inputAndExamples:3 x:x delegate:controller];
|
||||
self.date = [self inputAndExamples:4 x:x delegate:controller];
|
||||
self.dateFormat = [self inputAndExamples:5 x:x delegate:controller];
|
||||
|
||||
// output text field
|
||||
self.output = [self makeOutput];
|
||||
|
||||
// prepare info popover
|
||||
self.infoPopover = [NSView popover: NSMakeSize(400, 100)];
|
||||
NSView *content = self.infoPopover.contentViewController.view;
|
||||
self.popoverText = [[[[[NSView label:@""] selectable] sizableWidthAndHeight]
|
||||
multiline:NSMakeSize(384, 92)] placeIn:content x:8 y:4];
|
||||
return self;
|
||||
}
|
||||
|
||||
- (NSTextView *)makeHowTo {
|
||||
NSTextView *tv = [[NSTextView new] sizableWidthAndHeight];
|
||||
tv.editable = NO; // but selectable
|
||||
tv.drawsBackground = NO;
|
||||
tv.textContainer.textView.string = NSLocalizedString(@"DIY regex converter. Press enter to confirm. For help, refer to online tools (e.g., regex101 with options: global + single-line)", nil);
|
||||
NSScrollView *scroll = [self wrapContent:tv inScrollView:NSMakeRect(-1, NSHeight(self.frame) - heightHowTo, NSWidth(self.frame) + 2, heightHowTo)];
|
||||
scroll.drawsBackground = NO;
|
||||
scroll.borderType = NSNoBorder;
|
||||
scroll.verticalScrollElasticity = NSScrollElasticityNone;
|
||||
scroll.autoresizingMask = NSViewMinYMargin | NSViewWidthSizable;
|
||||
return tv;
|
||||
}
|
||||
|
||||
- (NSTextView *)makeOutput {
|
||||
NSTextView *tv = [[NSTextView new] sizableWidthAndHeight];
|
||||
tv.editable = NO; // but selectable
|
||||
tv.backgroundColor = NSColor.whiteColor;
|
||||
[self wrapContent:tv inScrollView:NSMakeRect(-1, 0, NSWidth(self.frame) + 2, heightOutput)];
|
||||
return tv;
|
||||
}
|
||||
|
||||
/// Helper method to create input field with help button showing regex examples
|
||||
- (NSTextField *)inputAndExamples:(NSInteger)row x:(CGFloat)x delegate:(id<NSTextFieldDelegate>)delegate {
|
||||
CGFloat yOffset = heightHowTo + PAD_L + row * heightRow;
|
||||
NSTextField *input = [[[NSView inputField:@"" width:0] placeIn:self x:x yTop:yOffset]
|
||||
sizeToRight:PAD_S + HEIGHT_BUTTON]; // width of the helpButton
|
||||
input.delegate = delegate;
|
||||
|
||||
NSInteger tag = 700 + row;
|
||||
NSArray<NSString *> *examples = [self examplesFor:tag];
|
||||
if (examples.count > 0) {
|
||||
[[[[NSView helpButton] action:@selector(didClickExamplesButton:) target:self]
|
||||
tooltip:NSLocalizedString(@"Click here to show examples", nil)]
|
||||
placeIn:self xRight:0 yTop:yOffset].tag = tag;
|
||||
|
||||
input.placeholderString = [examples firstObject];
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
/// Example to be displayed in help button
|
||||
- (NSArray<NSString *> *)examplesFor:(NSInteger)tag {
|
||||
switch (tag) {
|
||||
case 700: return @[ // entries
|
||||
@"<dt[ >].*?<\\/dd>",
|
||||
];
|
||||
case 701: return @[ // link
|
||||
@"href=\"([^\"]*)\"",
|
||||
];
|
||||
case 702: return @[ // title
|
||||
@"title=\"([^\"]*)\"",
|
||||
@">([^\\s<]*?)<\\/span>"
|
||||
];
|
||||
case 703: return @[ // description
|
||||
@"<dd[^>]*>(.*?)<\\/dd>",
|
||||
];
|
||||
case 704: return @[ // date matcher
|
||||
@"(\\d{2}.\\d{2}.\\d{4})",
|
||||
];
|
||||
case 705: return @[ // date format
|
||||
@"dd.MM.yyyy",
|
||||
@"dd. MMM yyyy",
|
||||
@"yyyy-MM-dd'T'HH:mm:ssZZZZZ",
|
||||
];
|
||||
default: break;
|
||||
}
|
||||
return @[];
|
||||
}
|
||||
|
||||
- (void)didClickExamplesButton:(NSButton*)sender {
|
||||
NSString *examples = [[self examplesFor:sender.tag] componentsJoinedByString:@"\n"];
|
||||
|
||||
// TODO: clickable entries
|
||||
self.popoverText.stringValue = [NSString stringWithFormat:@"%@", examples];
|
||||
|
||||
NSSize newSize = self.popoverText.fittingSize; // width is limited by the textfield's preferred width
|
||||
newSize.width += 2 * self.popoverText.frame.origin.x; // the padding
|
||||
newSize.height += 2 * self.popoverText.frame.origin.y;
|
||||
|
||||
// apply fitting size and display
|
||||
self.infoPopover.contentSize = newSize;
|
||||
[self.infoPopover showRelativeToRect:NSZeroRect ofView:sender preferredEdge:NSRectEdgeMinY];
|
||||
}
|
||||
|
||||
@end
|
||||
30
baRSS/Regex Editor/RegexFeed.h
Normal file
30
baRSS/Regex Editor/RegexFeed.h
Normal file
@@ -0,0 +1,30 @@
|
||||
@import Cocoa;
|
||||
@class RegexConverter;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface RegexFeedEntry : NSObject
|
||||
@property (nullable, readonly) NSString *href;
|
||||
@property (nullable, readonly) NSString *title;
|
||||
@property (nullable, readonly) NSString *desc;
|
||||
@property (nullable, readonly) NSString *dateString;
|
||||
@property (nullable, readonly) NSDate *date;
|
||||
|
||||
@property (nullable, readonly) NSString *rawMatch;
|
||||
@end
|
||||
|
||||
|
||||
@interface RegexFeed : NSObject
|
||||
@property (nullable, copy) NSString *rxEntry;
|
||||
@property (nullable, copy) NSString *rxHref;
|
||||
@property (nullable, copy) NSString *rxTitle;
|
||||
@property (nullable, copy) NSString *rxDesc;
|
||||
@property (nullable, copy) NSString *rxDate;
|
||||
@property (nullable, copy) NSString *dateFormat;
|
||||
|
||||
+ (RegexFeed *)from:(RegexConverter*)regex;
|
||||
|
||||
- (NSArray<RegexFeedEntry*>*)process:(NSString*)rawData error:(NSError * __autoreleasing *)err;
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
86
baRSS/Regex Editor/RegexFeed.m
Normal file
86
baRSS/Regex Editor/RegexFeed.m
Normal file
@@ -0,0 +1,86 @@
|
||||
#import "RegexFeed.h"
|
||||
#import "RegexConverter+Ext.h"
|
||||
|
||||
@interface RegexFeedEntry()
|
||||
@property (nullable, copy) NSString *href;
|
||||
@property (nullable, copy) NSString *title;
|
||||
@property (nullable, copy) NSString *desc;
|
||||
@property (nullable, copy) NSString *dateString;
|
||||
@property (nullable, retain) NSDate *date;
|
||||
|
||||
@property (nullable, copy) NSString *rawMatch;
|
||||
@end
|
||||
|
||||
@implementation RegexFeedEntry
|
||||
@end
|
||||
|
||||
|
||||
@implementation RegexFeed
|
||||
|
||||
+ (RegexFeed *)from:(RegexConverter*)regex {
|
||||
RegexFeed *x = [RegexFeed new];
|
||||
x.rxEntry = regex.entry;
|
||||
x.rxHref = regex.href;
|
||||
x.rxTitle = regex.title;
|
||||
x.rxDesc = regex.desc;
|
||||
x.rxDate = regex.date;
|
||||
x.dateFormat = regex.dateFormat;
|
||||
return x;
|
||||
}
|
||||
|
||||
- (NSArray<RegexFeedEntry*>*)process:(NSString*)rawData error:(NSError * __autoreleasing *)err {
|
||||
NSRegularExpression *re_entries = [self regex:_rxEntry error:err];
|
||||
if (!re_entries) {
|
||||
return @[];
|
||||
}
|
||||
NSDateFormatter *dateFormatter = [NSDateFormatter new];
|
||||
[dateFormatter setDateFormat:_dateFormat];
|
||||
[dateFormatter setTimeZone:[NSTimeZone timeZoneWithName:@"UTC"]];
|
||||
// TODO: we probably need to handle locale. Especially for "d. MMM" like "3. Dec"
|
||||
|
||||
NSMutableArray<RegexFeedEntry*> *rv = [NSMutableArray new];
|
||||
NSRegularExpression *re4 = [self regex:_rxDate error:err];
|
||||
NSRegularExpression *re3 = [self regex:_rxDesc error:err];
|
||||
NSRegularExpression *re2 = [self regex:_rxTitle error:err];
|
||||
NSRegularExpression *re1 = [self regex:_rxHref error:err];
|
||||
NSArray<NSTextCheckingResult*> *matches = [re_entries matchesInString:rawData options:0 range:NSMakeRange(0, rawData.length)];
|
||||
|
||||
for (NSTextCheckingResult *match in matches) {
|
||||
NSString *subdata = [rawData substringWithRange:match.range];
|
||||
RegexFeedEntry *entry = [[RegexFeedEntry alloc] init];
|
||||
entry.rawMatch = subdata;
|
||||
entry.href = [self firstMatch:subdata re:re1];
|
||||
entry.title = [self firstMatch:subdata re:re2];
|
||||
entry.desc = [self firstMatch:subdata re:re3];
|
||||
entry.dateString = [self firstMatch:subdata re:re4];
|
||||
entry.date = (_dateFormat.length && entry.dateString.length) ? [dateFormatter dateFromString:entry.dateString] : nil;
|
||||
[rv addObject:entry];
|
||||
};
|
||||
return rv;
|
||||
}
|
||||
|
||||
- (nullable NSRegularExpression*)regex:(NSString*)pattern error:(NSError * __autoreleasing *)err {
|
||||
if (pattern.length == 0) {
|
||||
return nil;
|
||||
}
|
||||
NSRegularExpression *re = [[NSRegularExpression alloc] initWithPattern:pattern options:NSRegularExpressionDotMatchesLineSeparators error:err];
|
||||
if (*err) {
|
||||
return nil;
|
||||
}
|
||||
return re;
|
||||
}
|
||||
|
||||
- (nonnull NSString*)firstMatch:(NSString*)str re:(NSRegularExpression*)re {
|
||||
NSTextCheckingResult *match = [[re matchesInString:str options:0 range:NSMakeRange(0, str.length)] firstObject];
|
||||
if (match) {
|
||||
if (match.numberOfRanges < 2) {
|
||||
return NSLocalizedString(@"Regex error: Missing match-group? ('outer(.*?)text')", nil);
|
||||
}else if (match.numberOfRanges > 2) {
|
||||
return NSLocalizedString(@"Regex error: Multiple match-groups found", nil);
|
||||
}
|
||||
return [str substringWithRange:[match rangeAtIndex:1]];
|
||||
}
|
||||
return @"";
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -66,8 +66,7 @@
|
||||
for (FeedGroup *fg in sortedList) {
|
||||
[menu insertFeedGroupItem:fg withUnread:self.unreadMap].submenu.delegate = self;
|
||||
}
|
||||
UnreadTotal *uct = self.unreadMap[menu.titleIndexPath];
|
||||
[menu setHeaderHasUnread:(uct.unread > 0) hasRead:(uct.unread < uct.total)];
|
||||
[menu setHeaderHasUnread:self.unreadMap[menu.titleIndexPath]];
|
||||
}
|
||||
|
||||
/// Generate items for @c FeedArticles menu.
|
||||
@@ -85,8 +84,7 @@
|
||||
break;
|
||||
[menu addItem:[fa newMenuItem]];
|
||||
}
|
||||
UnreadTotal *uct = self.unreadMap[menu.titleIndexPath];
|
||||
[menu setHeaderHasUnread:(uct.unread > 0) hasRead:(uct.unread < uct.total)];
|
||||
[menu setHeaderHasUnread:self.unreadMap[menu.titleIndexPath]];
|
||||
}
|
||||
|
||||
|
||||
@@ -131,11 +129,15 @@
|
||||
// 3. set unread count & enabled header for all parents
|
||||
NSArray<UnreadTotal*> *itms = [self.unreadMap itemsForPath:item.submenu.titleIndexPath create:NO];
|
||||
for (UnreadTotal *uct in itms.reverseObjectEnumerator) {
|
||||
[item.submenu setHeaderHasUnread:(uct.unread > 0) hasRead:(uct.unread < uct.total)];
|
||||
if (item) { // nil on last loop (aka main menu, see below)
|
||||
[item.submenu setHeaderHasUnread:uct];
|
||||
[item setTitleCount:uct.unread];
|
||||
item.hidden = NO;
|
||||
item = item.parentItem;
|
||||
}
|
||||
// TODO: need to re-create groups if user chose to hide already read articles
|
||||
}
|
||||
// call on main menu
|
||||
[self.statusItem.mainMenu setHeaderHasUnread:itms.firstObject];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface BarStatusItem : NSObject
|
||||
@interface BarStatusItem : NSObject <NSMenuDelegate>
|
||||
@property (weak, readonly) NSMenu *mainMenu;
|
||||
|
||||
- (void)setUnreadCountAbsolute:(NSUInteger)count;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#import "UserPrefs.h"
|
||||
#import "BarMenu.h"
|
||||
#import "AppHook.h"
|
||||
#import "NotifyEndpoint.h"
|
||||
#import "NSView+Ext.h"
|
||||
#import "NSColor+Ext.h"
|
||||
|
||||
@@ -17,8 +18,6 @@
|
||||
|
||||
@implementation BarStatusItem
|
||||
|
||||
- (NSMenu *)mainMenu { return _statusItem.menu; }
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
// Show icon & prefetch unread count
|
||||
@@ -28,8 +27,7 @@
|
||||
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];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(mainMenuDidClose) name:NSMenuDidEndTrackingNotification object:self.statusItem.menu];
|
||||
self.statusItem.menu.delegate = self;
|
||||
// Some icon unread count notification callback methods
|
||||
RegisterNotification(kNotificationNetworkStatusChanged, @selector(networkChanged:), self);
|
||||
RegisterNotification(kNotificationTotalUnreadCountChanged, @selector(unreadCountChanged:), self);
|
||||
@@ -72,14 +70,21 @@
|
||||
|
||||
/// Assign total unread count value directly.
|
||||
- (void)setUnreadCountAbsolute:(NSUInteger)count {
|
||||
_unreadCountTotal = (NSInteger)count;
|
||||
NSInteger oldCount = _unreadCountTotal;
|
||||
_unreadCountTotal = count > 0 ? (NSInteger)count : 0;
|
||||
[self updateBarIcon];
|
||||
[NotifyEndpoint setGlobalCount:_unreadCountTotal previousCount:oldCount];
|
||||
}
|
||||
|
||||
/// Assign new value by adding @c count to total unread count (may be negative).
|
||||
- (void)setUnreadCountRelative:(NSInteger)count {
|
||||
NSInteger oldCount = _unreadCountTotal;
|
||||
_unreadCountTotal += count;
|
||||
if (_unreadCountTotal < 0) {
|
||||
_unreadCountTotal = 0;
|
||||
}
|
||||
[self updateBarIcon];
|
||||
[NotifyEndpoint setGlobalCount:_unreadCountTotal previousCount:oldCount];
|
||||
}
|
||||
|
||||
/// Fetch new total unread count from core data and assign it as new value (dispatch async on main thread).
|
||||
@@ -144,19 +149,22 @@
|
||||
|
||||
#pragma mark - Main Menu Handling
|
||||
|
||||
- (void)mainMenuWillOpen {
|
||||
-(void)menuWillOpen:(NSMenu *)menu {
|
||||
_mainMenu = menu; // autoreleased once closed
|
||||
self.barMenu = [[BarMenu alloc] initWithStatusItem:self];
|
||||
[self insertMainMenuHeader:self.statusItem.menu];
|
||||
[self.barMenu menuNeedsUpdate:self.statusItem.menu];
|
||||
|
||||
[self insertMainMenuHeader:menu];
|
||||
[self.barMenu menuNeedsUpdate:menu];
|
||||
// Add main menu items 'Preferences' and 'Quit'.
|
||||
[self.statusItem.menu addItem:[NSMenuItem separatorItem]];
|
||||
[self.statusItem.menu addItemWithTitle:NSLocalizedString(@"Preferences", nil) action:@selector(openPreferences) keyEquivalent:@","];
|
||||
[self.statusItem.menu addItemWithTitle:NSLocalizedString(@"Quit", nil) action:@selector(terminate:) keyEquivalent:@"q"];
|
||||
[menu addItem:[NSMenuItem separatorItem]];
|
||||
[menu addItemWithTitle:NSLocalizedString(@"Preferences", nil) action:@selector(openPreferences) keyEquivalent:@","];
|
||||
[menu addItemWithTitle:NSLocalizedString(@"Quit", nil) action:@selector(terminate:) keyEquivalent:@"q"];
|
||||
}
|
||||
|
||||
- (void)mainMenuDidClose {
|
||||
[self.statusItem.menu removeAllItems];
|
||||
-(void)menuDidClose:(NSMenu *)menu {
|
||||
self.barMenu = nil;
|
||||
self.statusItem.menu = [[NSMenu alloc] initWithTitle:@"M"];
|
||||
self.statusItem.menu.delegate = self;
|
||||
}
|
||||
|
||||
- (void)insertMainMenuHeader:(NSMenu*)menu {
|
||||
|
||||
@@ -14,15 +14,12 @@
|
||||
- (instancetype)initWithCoreData:(NSArray<NSDictionary*>*)data {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
UnreadTotal *sum = [UnreadTotal new];
|
||||
_map = [NSMutableDictionary dictionaryWithCapacity:data.count];
|
||||
_map[@""] = sum;
|
||||
_map = [NSMutableDictionary dictionaryWithCapacity:data.count + 1];
|
||||
_map[@""] = [UnreadTotal new];
|
||||
|
||||
for (NSDictionary *d in data) {
|
||||
NSUInteger u = [d[@"unread"] unsignedIntegerValue];
|
||||
NSUInteger t = [d[@"total"] unsignedIntegerValue];
|
||||
sum.unread += u;
|
||||
sum.total += t;
|
||||
|
||||
for (UnreadTotal *uct in [self itemsForPath:d[@"indexPath"] create:YES]) {
|
||||
uct.unread += u;
|
||||
@@ -37,6 +34,7 @@
|
||||
- (NSArray<UnreadTotal*>*)itemsForPath:(NSString*)path create:(BOOL)flag {
|
||||
NSMutableArray<UnreadTotal*> *arr = [NSMutableArray array];
|
||||
NSMutableString *key = [NSMutableString string];
|
||||
[arr addObject:_map[@""]];
|
||||
for (NSString *idx in [path componentsSeparatedByString:@"."]) {
|
||||
if (key.length > 0)
|
||||
[key appendString:@"."];
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
@import Cocoa;
|
||||
@class FeedGroup, MapUnreadTotal;
|
||||
@class FeedGroup, MapUnreadTotal, UnreadTotal;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSMenu (Ext)
|
||||
@property (nonnull, copy, readonly) NSString *titleIndexPath;
|
||||
@property (nullable, readonly) NSMenuItem* parentItem;
|
||||
@property (readonly) BOOL isMainMenu;
|
||||
@property (readonly) BOOL isFeedMenu;
|
||||
|
||||
// Generator
|
||||
- (nullable NSMenuItem*)insertFeedGroupItem:(FeedGroup*)fg withUnread:(MapUnreadTotal*)unreadMap;
|
||||
- (void)insertDefaultHeader;
|
||||
// Update menu
|
||||
- (void)setHeaderHasUnread:(BOOL)hasUnread hasRead:(BOOL)hasRead;
|
||||
- (void)setHeaderHasUnread:(UnreadTotal*)count;
|
||||
- (nullable NSMenuItem*)deepestItemWithPath:(nonnull NSString*)path;
|
||||
@end
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#import "FeedGroup+Ext.h"
|
||||
#import "Constants.h"
|
||||
#import "MapUnreadTotal.h"
|
||||
#import "NotifyEndpoint.h"
|
||||
|
||||
typedef NS_ENUM(NSInteger, MenuItemTag) {
|
||||
/// Used in @c allowDisplayOfHeaderItem: to identify and enable items
|
||||
@@ -36,9 +37,6 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
|
||||
return [self.supermenu itemAtIndex:[self.supermenu indexOfItemWithSubmenu:self]];
|
||||
}
|
||||
|
||||
/// @return @c YES if menu is status bar menu.
|
||||
- (BOOL)isMainMenu { return (self.supermenu == nil); }
|
||||
|
||||
/// @return @c YES if menu contains feed articles only.
|
||||
- (BOOL)isFeedMenu { return ([self.title characterAtIndex:0] == 'F'); }
|
||||
|
||||
@@ -59,10 +57,10 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
|
||||
NSUInteger unread = unreadMap[[t substringFromIndex:2]].unread;
|
||||
|
||||
// Check user preferences to show only unread entries
|
||||
if (unread == 0 &&
|
||||
((fg.type == FEED && UserPrefsBool(Pref_groupUnreadOnly)) ||
|
||||
(fg.type == GROUP && UserPrefsBool(Pref_globalUnreadOnly)))) {
|
||||
return nil;
|
||||
if (unread == 0
|
||||
&& (fg.type == FEED || fg.type == GROUP)
|
||||
&& UserPrefsBool(Pref_groupUnreadOnly)) {
|
||||
item.hidden = YES;
|
||||
}
|
||||
|
||||
item.submenu = [[NSMenu alloc] initWithTitle:t];
|
||||
@@ -95,7 +93,9 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
|
||||
|
||||
|
||||
/// Loop over default header and enable 'OpenAllUnread' and 'TagMarkAllRead' based on unread count.
|
||||
- (void)setHeaderHasUnread:(BOOL)hasUnread hasRead:(BOOL)hasRead {
|
||||
- (void)setHeaderHasUnread:(UnreadTotal*)count {
|
||||
BOOL hasUnread = count.unread > 0;
|
||||
BOOL hasRead = count.unread < count.total;
|
||||
NSInteger i = [self indexOfItemWithTag:TagHeaderDelimiter] - 1;
|
||||
for (; i >= 0; i--) {
|
||||
NSMenuItem *item = [self itemAtIndex:i];
|
||||
@@ -138,7 +138,7 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
|
||||
static NSString* const mr[] = {Pref_globalMarkRead, Pref_groupMarkRead, Pref_feedMarkRead};
|
||||
static NSString* const mu[] = {Pref_globalMarkUnread, Pref_groupMarkUnread, Pref_feedMarkUnread};
|
||||
static NSString* const ou[] = {Pref_globalOpenUnread, Pref_groupOpenUnread, Pref_feedOpenUnread};
|
||||
int i = (self.isMainMenu ? 0 : (self.isFeedMenu ? 2 : 1));
|
||||
int i = (self.supermenu == nil ? 0 : (self.isFeedMenu ? 2 : 1));
|
||||
switch (tag) {
|
||||
case TagMarkAllRead: return UserPrefsBool(mr[i]);
|
||||
case TagMarkAllUnread: return UserPrefsBool(mu[i]);
|
||||
@@ -182,27 +182,8 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
|
||||
}
|
||||
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
|
||||
NSArray<FeedArticle*> *list = [StoreCoordinator articlesAtPath:path isFeed:isFeedMenu sorted:openLinks unread:markRead inContext:moc limit:limit];
|
||||
|
||||
BOOL success = NO;
|
||||
if (openLinks) {
|
||||
NSMutableArray<NSURL*> *urls = [NSMutableArray arrayWithCapacity:list.count];
|
||||
for (FeedArticle *fa in list) {
|
||||
if (fa.link.length > 0)
|
||||
[urls addObject:[NSURL URLWithString:fa.link]];
|
||||
}
|
||||
if (urls.count > 0)
|
||||
success = UserPrefsOpenURLs(urls);
|
||||
}
|
||||
// if success == NO, do not modify unread state
|
||||
if (!openLinks || success) {
|
||||
for (FeedArticle *fa in list) {
|
||||
fa.unread = !markRead;
|
||||
}
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
[moc reset];
|
||||
NSNumber *num = [NSNumber numberWithInteger: (markRead ? -1 : +1) * (NSInteger)list.count ];
|
||||
PostNotification(kNotificationTotalUnreadCountChanged, num);
|
||||
}
|
||||
[NotifyEndpoint dismiss:
|
||||
[StoreCoordinator updateArticles:list markRead:markRead andOpen:openLinks inContext:moc]];
|
||||
}
|
||||
|
||||
@end
|
||||
@@ -218,10 +199,7 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
|
||||
NSMenuItem *alt = [self copy];
|
||||
alt.title = title;
|
||||
alt.keyEquivalentModifierMask = NSEventModifierFlagOption;
|
||||
if (!alt.hidden) { // hidden will be ignored if alternate is YES
|
||||
alt.hidden = YES; // force hidden to hide if menu is already open (background update)
|
||||
alt.alternate = YES;
|
||||
}
|
||||
return alt;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user