61 Commits

Author SHA1 Message Date
relikd
4864208754 chore: update changelog 2025-12-13 00:24:09 +01:00
relikd
76c7263548 doc: remove reference to obsolete QLOPML 2025-12-13 00:02:15 +01:00
relikd
7f40bb259c fix: appearance background color + tabbar transparency 2025-12-13 00:01:48 +01:00
relikd
68aa4ef94b fix: tooltip strings 2025-12-12 14:56:12 +01:00
relikd
217a91b23c feat: option to configure "Open a few unread" 2025-12-12 14:45:52 +01:00
relikd
f0299d8246 fix: spacing + alignment (Appearance settings) 2025-12-12 14:07:43 +01:00
relikd
ae0d5967c7 fix: allow 0 for input field (Appearance settings) 2025-12-12 14:06:44 +01:00
relikd
d45d4864b0 fix: mouseDown on appearance view, not FlippedView 2025-12-12 14:02:49 +01:00
relikd
ef2c588f4c fix: better handling of open-a-few limit 2025-12-12 14:01:53 +01:00
relikd
03aecdfa4a feat: version migration for new option names 2025-12-11 18:34:13 +01:00
relikd
3b65bca88f ref: appearance settings 2025-12-11 18:33:53 +01:00
relikd
bd03059247 ref: rename pref options 2025-12-11 18:33:35 +01:00
relikd
d03840757a feat: Appearance settings v2 2025-12-11 15:51:10 +01:00
relikd
2e77f67102 feat: introduce new Pref_article options 2025-12-11 15:46:40 +01:00
relikd
5d339b8125 ref: rename pref key "articleTooltipLimit" 2025-12-11 15:25:50 +01:00
relikd
65cac6b19a feat: expose more NSView methods 2025-12-11 15:21:14 +01:00
relikd
2ec1743dd9 ref: rename menu item "Show hidden feeds" 2025-12-11 15:20:46 +01:00
relikd
ca2b3cb887 fix: accessibility strings 2025-12-11 15:20:23 +01:00
relikd
b1ca30f914 ref: wrapInScrollView 2025-12-11 15:16:39 +01:00
relikd
5427cb58ee feat: uint formatter with units 2025-12-11 15:10:32 +01:00
relikd
b94dd030b4 ref: cleaner "menu bar icon" 2025-12-11 15:07:56 +01:00
relikd
469d7bcdd4 feat: separator with direction flip 2025-12-09 15:08:20 +01:00
relikd
0806003fc3 fix: draw separator in bounds 2025-12-09 15:07:35 +01:00
relikd
385bcf99f3 ref: uint-formatter on NSView+Ext 2025-12-09 15:06:43 +01:00
relikd
b194a1427d feat: add svg artwork 2025-12-09 00:30:25 +01:00
relikd
ff34781fea ref: simplify regex icon 2025-12-09 00:28:29 +01:00
relikd
4edd4448ae ref: pixel-perfect rss icon alignment 2025-12-09 00:10:54 +01:00
relikd
33f907228b ref: simplify rss icon path 2025-12-08 23:31:09 +01:00
relikd
673e0d3d48 fix: quadratic curve 2025-12-08 23:30:50 +01:00
relikd
b3fdadb9f4 feat: feed group icon 2025-12-08 22:40:11 +01:00
relikd
9fc513254f fix: pixel-perfect group icon 2025-12-08 22:31:49 +01:00
relikd
881b9db02c ref: flip coordinate system 2025-12-08 21:43:24 +01:00
relikd
3a14c90f37 ref: split svgRect and svgRoundedRect 2025-12-08 21:36:49 +01:00
relikd
96884474ac ref: unread dot icon 2025-12-08 21:21:29 +01:00
relikd
82ae18c8a5 ref: pixel-perfect main menu icon (+feed icon) 2025-12-08 21:10:20 +01:00
relikd
6eddb57651 ref: svg rss icon 2025-12-08 21:09:38 +01:00
relikd
67d17599b5 ref: default rss icon 2025-12-08 19:05:27 +01:00
relikd
3507fd8e27 feat: appearance settings article icon 2025-12-08 19:04:53 +01:00
relikd
ca417f35b6 ref: rename drawing methods 2025-12-08 17:36:48 +01:00
relikd
6e5326f913 feat: new menubar icon for Appearance settings 2025-12-08 16:32:39 +01:00
relikd
1589b23aa9 fix: TinySVG rect scaling 2025-12-08 16:31:53 +01:00
relikd
e0cd04b882 feat: new group icon (svg) 2025-12-08 14:59:48 +01:00
relikd
6b4c38ec21 feat: TinySVG support for quadratic curves 2025-12-08 14:49:40 +01:00
relikd
e7208ae2ab fix: variable name 2025-12-08 14:13:09 +01:00
relikd
508377a823 fix: limit tooltip to 2000 characters 2025-12-05 22:24:29 +01:00
relikd
2185eb76fb fix: uniform menu titles 2025-12-05 14:11:48 +01:00
relikd
8de163859b chore: bump version 2025-12-03 15:34:26 +01:00
relikd
f739b64ceb feat: add setting to show "toggle hidden" button 2025-12-03 15:06:56 +01:00
relikd
c2fda881b1 feat: add menu option to toggle hidden articles 2025-12-03 14:48:39 +01:00
relikd
a0a5b5b82d ref: tooltips on options 2025-12-03 14:15:21 +01:00
relikd
43e32b2286 ref: remove tooltip on column icon 2025-12-03 13:43:42 +01:00
relikd
205b544acd fix: undo settings migration
for whatever reason but it breaks Sandbox flag on release build
2025-12-02 20:25:59 +01:00
relikd
56f6ec1356 chore: bump version 2025-12-02 18:52:18 +01:00
relikd
ab71c51380 feat: show hidden articles by holding down option key 2025-12-02 18:48:46 +01:00
relikd
7a805ccdc4 feat: show tooltip for all appearance settings 2025-12-02 18:42:39 +01:00
relikd
f4f4bc9271 fix: annoying negative frame warning 2025-12-01 20:10:44 +01:00
relikd
64637243b5 chore: update recommended settings + xcconfig 2025-12-01 19:41:25 +01:00
relikd
5894b12c1d fix: user provided feed title for notifications (fixes #22) 2025-10-29 18:27:32 +01:00
relikd
0700eebb13 chore: update min OS in readme 2025-10-29 15:19:50 +01:00
relikd
4c4a133fe2 chore: bump version 2025-10-29 15:12:43 +01:00
relikd
ccca329630 feat: notification open options 2025-10-29 15:10:06 +01:00
39 changed files with 889 additions and 438 deletions

View File

@@ -5,6 +5,47 @@ 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).
## [1.6.0] 2025-12-13
### Added
- *UI:* Limit content length for article tooltips. (fixes #25)
- *Settings, Appearance:* Revamped appearance options v2. (thanks @Shnub)
- *Settings, Appearance:* New GUI options for previously CLI-only options. Modify display limits directly in settings.
### Fixed
- *Status Bar Menu:* Uniform capitalization for all menu items.
- *Status Bar Menu:* Setting the "Open a few"-limit to zero, hides the button altogether.
- *Settings, Appearance:* Pixel-perfect alignment of all drawable icons.
- *UI:* Accessibility hints for appearance options generate better VoiceOver output.
### Changed
- *UI:* "Show Hidden Article" renamed to "Show hidden feeds".
## [1.5.5] 2025-12-03
### Added
- *Settings, Appearance:* Improved tooltips on individual options
- *Status Bar Menu:* Toggle button to show hidden articles without holding down option-key.
## [1.5.4] 2025-12-02
### Added
- *Settings, Appearance:* Tooltip explanation for all options
- *Status Bar Menu:* Hold down option key before opening the menu bar icon to show hidden articles (if option "Show only unread" is active)
### Fixed
- *UI:* Table cells were rendered slightly off bounds.
## [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"
@@ -220,6 +261,11 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
Initial release
[1.6.0]: https://github.com/relikd/baRSS/compare/v1.5.5...v1.6.0
[1.5.5]: https://github.com/relikd/baRSS/compare/v1.5.4...v1.5.5
[1.5.4]: https://github.com/relikd/baRSS/compare/v1.5.3...v1.5.4
[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

6
Config-debug.xcconfig Normal file
View File

@@ -0,0 +1,6 @@
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
#include "Config.xcconfig"
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS.beta

12
Config.xcconfig Normal file
View File

@@ -0,0 +1,12 @@
// Configuration settings file format documentation can be found at:
// https://help.apple.com/xcode/#/dev745c5c974
CODE_SIGN_STYLE = Manual
CODE_SIGN_IDENTITY = Apple Development
ENABLE_HARDENED_RUNTIME = YES
MACOSX_DEPLOYMENT_TARGET = 10.14
MARKETING_VERSION = 1.6.0
PRODUCT_NAME = baRSS
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS
CURRENT_PROJECT_VERSION = 17752

View File

@@ -1,4 +1,4 @@
[![macOS 10.13+](https://img.shields.io/badge/macOS-10.13+-888)](#download--install)
[![macOS 10.14+](https://img.shields.io/badge/macOS-10.14+-888)](#download--install)
[![Current release](https://img.shields.io/github/release/relikd/baRSS)](https://github.com/relikd/baRSS/releases)
[![All downloads](https://img.shields.io/github/downloads/relikd/baRSS/total)](https://github.com/relikd/baRSS/releases)
[![GitHub license](https://img.shields.io/github/license/relikd/baRSS)](LICENSE)
@@ -35,25 +35,20 @@ Further, tuning the update frequently will decrease the traffic even more.
Download & Install
------------------
Requires macOS High Sierra (10.13) 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.
Searching for the App Store release? Read this [notice](#app-store-notice).
### Build from source
You'll need Xcode, [RSXML2] \(required), and [QLOPML] \(optional).
You'll need Xcode and [RSXML2].
```sh
git clone https://github.com/relikd/baRSS
git clone https://github.com/relikd/RSXML2
git clone https://github.com/relikd/QLOPML
```
Alternatively, you can simply delete the `QLOPML` project reference without much harm.
`QLOPML` is a Quick Look plugin for `.opml` files.
It will display the file contents whenever you hit spacebar.
That's it.
Open `baRSS/baRSS.xcodeproj` and build the project.
Note, there are some compiler flags that append 'beta' to the development release.
@@ -69,19 +64,24 @@ Hidden options
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 > User > Login Items` (macOS 10.x-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.
I am still searching for a way to keep the menu open after click (if you know of a way, let me know!).
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.
3. The option “Show only unread” will hide all items which have been read.
You can hold down option key before opening the menu bar icon to show hidden articles regardless.
This is a nice way to quickly lookup a hidden article without going into settings and twiddling with the checkbox.
### CLI options
@@ -90,33 +90,14 @@ 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. 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
```
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
```
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
```
4. You can change the appearance of colors throughout the application.
1. 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"
```
5. To backup your list of subscribed feeds, here is a one-liner:
2. 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"
```
@@ -183,6 +164,7 @@ Sadly, this was before Swift 5 and ABI stability.
Had I only started the project a year later…
But on the other hand, now it is macOS 10.12 compatible.
### 3rd Party Libraries
This project uses a modified version of Brent Simmons' [RSXML] for feed parsing.
@@ -192,10 +174,9 @@ This project uses a modified version of Brent Simmons' [RSXML] for feed parsing.
##### Trivia
- Start of project: __July 19, 2018__
- Estimated development time: __2053h+__
- Estimated development time: __2121h+__
- First prototype used __feedparser python__ library
[QLOPML]: https://github.com/relikd/QLOPML
[RSXML2]: https://github.com/relikd/RSXML2
[RSXML]: https://github.com/brentsimmons/RSXML

View File

@@ -22,6 +22,7 @@
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 */; };
545EB5DA2EE8622200FABBE0 /* StrictUIntFormatter.m in Sources */ = {isa = PBXBuildFile; fileRef = 545EB5D92EE8622200FABBE0 /* StrictUIntFormatter.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 */; };
@@ -46,7 +47,7 @@
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, ); }; };
54AD90F72E30C48400160925 /* QLOPML.appex in Embed Foundation 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 */; };
@@ -105,15 +106,15 @@
name = "Embed Frameworks";
runOnlyForDeploymentPostprocessing = 0;
};
54AD90F62E30C48400160925 /* Embed App Extensions */ = {
54AD90F62E30C48400160925 /* Embed Foundation Extensions */ = {
isa = PBXCopyFilesBuildPhase;
buildActionMask = 2147483647;
dstPath = "";
dstSubfolderSpec = 13;
files = (
54AD90F72E30C48400160925 /* QLOPML.appex in Embed App Extensions */,
54AD90F72E30C48400160925 /* QLOPML.appex in Embed Foundation Extensions */,
);
name = "Embed App Extensions";
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXCopyFilesBuildPhase section */
@@ -150,12 +151,16 @@
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>"; };
545EB5D62EE8620300FABBE0 /* StrictUIntFormatter.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = StrictUIntFormatter.h; sourceTree = "<group>"; };
545EB5D92EE8622200FABBE0 /* StrictUIntFormatter.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = StrictUIntFormatter.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>"; };
546A6A2E22C585580034E806 /* SettingsAboutView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsAboutView.h; sourceTree = "<group>"; };
546BD1882EDE156000943942 /* Config.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Config.xcconfig; sourceTree = "<group>"; };
546BD1892EDE156000943942 /* Config-debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Config-debug.xcconfig"; sourceTree = "<group>"; };
546FC43B21188AD5007CC3A3 /* SettingsFeeds.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsFeeds.h; sourceTree = "<group>"; };
546FC43C21188AD5007CC3A3 /* SettingsFeeds.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = SettingsFeeds.m; sourceTree = "<group>"; };
546FC44021189975007CC3A3 /* SettingsGeneral.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = SettingsGeneral.h; sourceTree = "<group>"; };
@@ -365,6 +370,8 @@
54ACC27321061B3B0020715F = {
isa = PBXGroup;
children = (
546BD1882EDE156000943942 /* Config.xcconfig */,
546BD1892EDE156000943942 /* Config-debug.xcconfig */,
540CD14821C094A2004AB594 /* README.md */,
54892F1D2235285700271CBA /* CHANGELOG.md */,
54ACC27E21061B3B0020715F /* baRSS */,
@@ -511,6 +518,8 @@
54910066233A4D4000858AE2 /* URLScheme.m */,
54229F532E02491A0019ACB0 /* TinySVG.h */,
54229F542E02491A0019ACB0 /* TinySVG.m */,
545EB5D62EE8620300FABBE0 /* StrictUIntFormatter.h */,
545EB5D92EE8622200FABBE0 /* StrictUIntFormatter.m */,
);
path = Helper;
sourceTree = "<group>";
@@ -536,7 +545,7 @@
54ACC27A21061B3B0020715F /* Resources */,
544DCCBB212A2B4D002DBC46 /* Embed Frameworks */,
54FB05D12305BFAB00A088AD /* dynamic app name in db migration */,
54AD90F62E30C48400160925 /* Embed App Extensions */,
54AD90F62E30C48400160925 /* Embed Foundation Extensions */,
);
buildRules = (
);
@@ -572,7 +581,7 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1640;
LastUpgradeCheck = 2600;
ORGANIZATIONNAME = relikd;
TargetAttributes = {
54ACC27B21061B3B0020715F = {
@@ -728,6 +737,7 @@
546FC43D21188AD5007CC3A3 /* SettingsFeeds.m in Sources */,
54253C952C49BFE400742695 /* RegexConverterView.m in Sources */,
548C6D0A230C33DE003A1AAF /* NSURL+Ext.m in Sources */,
545EB5DA2EE8622200FABBE0 /* StrictUIntFormatter.m in Sources */,
54E88320211B509D00064188 /* ModalFeedEdit.m in Sources */,
54195883218A061100581B79 /* Feed+Ext.m in Sources */,
5469E13C2EA90C6C00D46CE7 /* NotifyEndpoint.m in Sources */,
@@ -772,6 +782,7 @@
/* Begin XCBuildConfiguration section */
54ACC28E21061B3C0020715F /* Debug */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 546BD1892EDE156000943942 /* Config-debug.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
@@ -805,7 +816,6 @@
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 16720;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = UY657LKNHJ;
@@ -822,8 +832,6 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 1.5.1;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx;
@@ -832,6 +840,7 @@
};
54ACC28F21061B3C0020715F /* Release */ = {
isa = XCBuildConfiguration;
baseConfigurationReference = 546BD1882EDE156000943942 /* Config.xcconfig */;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_LOCALIZABILITY_NONLOCALIZED = YES;
@@ -866,7 +875,6 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 16720;
DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = UY657LKNHJ;
@@ -880,8 +888,6 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 1.5.1;
MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx;
};
@@ -931,8 +937,7 @@
"@executable_path/../Frameworks",
"$(FRAMEWORK_SEARCH_PATHS)",
);
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS.beta;
PRODUCT_NAME = "$(TARGET_NAME) Beta";
PRODUCT_NAME = "$(inherited) Beta";
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
};
name = Debug;
@@ -981,8 +986,6 @@
"@executable_path/../Frameworks",
"$(FRAMEWORK_SEARCH_PATHS)",
);
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS;
PRODUCT_NAME = "$(TARGET_NAME)";
};
name = Release;
};
@@ -1003,7 +1006,7 @@
);
MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE;
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS.beta.QLOPML;
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).QLOPML";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};
@@ -1021,7 +1024,7 @@
"@executable_path/../../../../Frameworks",
);
MTL_FAST_MATH = YES;
PRODUCT_BUNDLE_IDENTIFIER = de.relikd.baRSS.QLOPML;
PRODUCT_BUNDLE_IDENTIFIER = "$(inherited).QLOPML";
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
};

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1640"
LastUpgradeVersion = "2600"
version = "1.8">
<BuildAction
parallelizeBuildables = "YES"

View File

@@ -59,7 +59,44 @@
/// Called during application start. Perform any version migration updates here.
- (void)migrateVersionUpdate {
// Currently unused, but you'll be thankful in the future for a previously saved version number
[StoreCoordinator setOption:@"app-version" value: UserPrefsAppVersion()];
// thank you, past-self! but it would have been nice to have easier "<=" comparison
NSString *prevVersion = [StoreCoordinator optionForKey:@"app-version"];
NSString *curVersion = UserPrefsAppVersion();
// migrate if not run for the first time
if (prevVersion != nil) {
if ([prevVersion isEqualToString:curVersion]) {
return; // migration already performed
}
// else: migrate
NSInteger ver = 0;
for (NSString *part in [prevVersion componentsSeparatedByString:@"."]) {
ver = ver * 100 + [part integerValue];
}
if (ver <= 10505) { // v1.5.5
[self migrate_v1_6_0];
}
}
[StoreCoordinator setOption:@"app-version" value:curVersion];
}
- (void)migrate_v1_6_0 {
NSLog(@"Migrating to v1.6.0");
// rename options
BOOL shouldLimitCount = UserPrefsBool(@"feedLimitArticles"); // default: NO
if (shouldLimitCount) {
NSInteger prev = UserPrefsInt(@"articlesInMenuLimit"); // default: 40
UserPrefsSetInt(Pref_articleCountLimit, prev == 0 ? 40 : prev);
}
BOOL shouldLimitTitle = UserPrefsBool(@"feedTruncateTitle"); // default: NO
if (shouldLimitTitle) {
NSInteger prev = UserPrefsInt(@"shortArticleNamesLimit"); // default: 60
UserPrefsSetInt(Pref_articleTitleLimit, prev == 0 ? 60 : prev);
}
// delete old keys
UserPrefsSet(@"feedLimitArticles", nil);
UserPrefsSet(@"feedTruncateTitle", nil);
UserPrefsSet(@"articlesInMenuLimit", nil);
UserPrefsSet(@"shortArticleNamesLimit", nil);
}

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<rect y="14" width="16" height="1"/>
<rect y="10" width="16" height="1"/>
<rect x="9" y="6" width="7" height="1"/>
<rect x="9" y="2" width="7" height="1"/>
<rect x="1" y="1" width="7" height="7"/>
</svg>

After

Width:  |  Height:  |  Size: 313 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<g fill="none" stroke="#000">
<path d="M3,13.5c-1.5,0-2.5-1-2.5-2.5V3.5c0-1.5.5-2,2-2h1.5c1.5,0,1.5,1,3,1h6c1.5,0,2.5,1,2.5,2.5v6c0,1.5-1,2.5-2.5,2.5H3Z"/>
<path d="M1.5,5h13Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 294 B

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
<!-- menu -->
<rect x="0" y="0" width="16" height="3"/>
<rect x="5" y="4" width="9" height="12"/>
<rect x="6" y="3" width="7" height="12" fill="#aaa"/>
<!-- entries -->
<rect x="6" y="12" width="6" height="1"/>
<rect x="6" y="9" width="6" height="1"/>
<rect x="6" y="6" width="6" height="1"/>
</svg>

After

Width:  |  Height:  |  Size: 415 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M18,19c-14,21-13,43,0,62l-7,4C-4,63-4,35,12,14l6,5Z"/>
<circle cx="31" cy="67" r="7"/>
<path d="M65,28l11-4,2,6-11,4,7,9-5,4-7-9-7,9-5-4,7-9-11-4,2-6,11,4v-11h6v11Z"/>
<path d="M82,81c14-21,13-43,0-62l7-5c16,22,15,50,0,71l-7-4Z"/>
</svg>

After

Width:  |  Height:  |  Size: 356 B

View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="13" cy="87" r="13"/>
<path d="M0,35q65,0,65,65h-20q0,-45,-45,-45z"/>
<rect x="60" y="0" width="15" height="50"/>
<rect x="85" y="0" width="15" height="50"/>
</svg>

After

Width:  |  Height:  |  Size: 281 B

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<circle cx="13" cy="87" r="13"/>
<path d="M0,35q65,0,65,65h-20q0,-45,-45,-45z"/>
<path d="M0,0q100,0,100,100h-20q0,-80,-80,-80z"/>
</svg>

After

Width:  |  Height:  |  Size: 242 B

View File

@@ -21,21 +21,25 @@ static NSString* const auxiliaryAppURL = @"https://github.com/relikd/URL-Scheme-
/// Default RSS icon (with border, with gradient, orange)
static NSImageName const RSSImageDefaultRSSIcon = @"RSSImageDefaultRSSIcon";
/// Settings, global icon (menu bar, black)
static NSImageName const RSSImageSettingsGlobal = @"RSSImageSettingsGlobal";
static NSImageName const RSSImageDefaultRSSIcon = @"RSSImageDefaultRSSIcon";
/// Settings, global statusbar icon (rss icon with neighbor icons)
static NSImageName const RSSImageSettingsGlobalIcon = @"RSSImageSettingsGlobalIcon";
/// Settings, global menu icon (menu bar, black)
static NSImageName const RSSImageSettingsGlobalMenu = @"RSSImageSettingsGlobalMenu";
/// Settings, group icon (folder, black)
static NSImageName const RSSImageSettingsGroup = @"RSSImageSettingsGroup";
static NSImageName const RSSImageSettingsGroup = @"RSSImageSettingsGroup";
/// Settings, feed icon (RSS, no border, no gradient, black)
static NSImageName const RSSImageSettingsFeed = @"RSSImageSettingsFeed";
static NSImageName const RSSImageSettingsFeed = @"RSSImageSettingsFeed";
/// Settings, article icon (RSS surrounded by text lines)
static NSImageName const RSSImageSettingsArticle = @"RSSImageSettingsArticle";
/// Menu bar, bar icon (RSS, with border, no gradient, orange)
static NSImageName const RSSImageMenuBarIconActive = @"RSSImageMenuBarIconActive";
static NSImageName const RSSImageMenuBarIconActive = @"RSSImageMenuBarIconActive";
/// Menu bar, bar icon (RSS, with border, no gradient, paused, orange)
static NSImageName const RSSImageMenuBarIconPaused = @"RSSImageMenuBarIconPaused";
static NSImageName const RSSImageMenuBarIconPaused = @"RSSImageMenuBarIconPaused";
/// Menu item, unread state icon (blue dot)
static NSImageName const RSSImageMenuItemUnread = @"RSSImageMenuItemUnread";
static NSImageName const RSSImageMenuItemUnread = @"RSSImageMenuItemUnread";
/// Feed edit, regex editor icon @c "(.*)"
static NSImageName const RSSImageRegexIcon = @"RSSImageRegexIcon";
static NSImageName const RSSImageRegexIcon = @"RSSImageRegexIcon";
#pragma mark - NSNotificationName constants

View File

@@ -47,10 +47,9 @@
NSString *title = self.title;
if (!title) return @"";
// TODO: It should be enough to get user prefs once per menu build
if (UserPrefsBool(Pref_feedTruncateTitle)) {
NSUInteger limit = UserPrefsUInt(Pref_shortArticleNamesLimit);
if (title.length > limit)
title = [[title substringToIndex:limit] stringByAppendingString:@"…"];
NSUInteger limit = UserPrefsUInt(Pref_articleTitleLimit); // -1 will become MAX_INT
if (limit > 0 && title.length > limit) {
title = [[title substringToIndex:limit] stringByAppendingString:@"…"];
}
return title;
}
@@ -60,10 +59,17 @@
NSMenuItem *item = [NSMenuItem new];
item.title = [self shortArticleName];
item.enabled = (self.link.length > 0);
item.state = (self.unread && UserPrefsBool(Pref_feedUnreadIndicator) ? NSControlStateValueOn : NSControlStateValueOff);
item.state = (self.unread && UserPrefsBool(Pref_articleUnreadIndicator) ? NSControlStateValueOn : NSControlStateValueOff);
item.onStateImage = [NSImage imageNamed:RSSImageMenuItemUnread];
item.accessibilityLabel = (self.unread ? NSLocalizedString(@"article: unread", @"accessibility label, feed menu item") : NSLocalizedString(@"article: read", @"accessibility label, feed menu item"));
item.toolTip = (self.abstract ? self.abstract : self.body); // fall back to body (html)
// truncate tooltip
NSUInteger limit = UserPrefsUInt(Pref_articleTooltipLimit); // -1 will become MAX_INT
if (limit > 0) {
NSString *tooltip = (self.abstract ? self.abstract : self.body); // fall back to body (html)
if (tooltip.length > limit)
tooltip = [[tooltip substringToIndex:limit] stringByAppendingString:@"…\n[…]"];
item.toolTip = tooltip;
}
item.representedObject = self.objectID;
item.target = [self class];
item.action = @selector(didClickOnMenuItem:);

View File

@@ -225,9 +225,11 @@
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];
@@ -242,8 +244,7 @@
}
[moc reset];
NSNumber *num = [NSNumber numberWithInteger: (markRead ? -1 : +1) * (NSInteger)list.count ];
PostNotification(kNotificationTotalUnreadCountChanged, num);
PostNotification(kNotificationTotalUnreadCountChanged, @(countChange));
return dbRefs;
}

View File

@@ -3,6 +3,8 @@
/// Draw separator line in @c NSOutlineView
IB_DESIGNABLE
@interface DrawSeparator : NSView
@property (assign) BOOL invert;
+ (instancetype)withSize:(NSSize)size;
@end

View File

@@ -5,10 +5,15 @@
@implementation DrawSeparator
+ (instancetype)withSize:(NSSize)size {
return [[super alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)];
}
- (void)drawRect:(NSRect)r {
NSColor *color = [NSColor darkGrayColor];
NSGradient *grdnt = [[NSGradient alloc] initWithStartingColor:color endingColor:[color colorWithAlphaComponent:0.0]];
NSRect separatorRect = NSMakeRect(1, NSMidY(self.frame) - 1, NSWidth(self.frame) - 2, 2);
NSColor *transparent = [color colorWithAlphaComponent:0.0];
NSGradient *grdnt = [[NSGradient alloc] initWithStartingColor:self.invert ? transparent : color endingColor:self.invert ? color : transparent];
NSRect separatorRect = NSMakeRect(1, NSMidY(self.bounds) - 1, NSWidth(self.bounds) - 2, 2);
NSBezierPath *rounded = [NSBezierPath bezierPathWithRoundedRect:separatorRect xRadius:1 yRadius:1];
[grdnt drawInBezierPath:rounded angle:0];
}
@@ -23,126 +28,18 @@ static inline const CGFloat ShorterSide(NSSize s) {
return (s.width < s.height ? s.width : s.height);
}
/// Perform @c CGAffineTransform with custom rotation point
// CGAffineTransform RotateAroundPoint(CGAffineTransform at, CGFloat angle, CGFloat x, CGFloat y) {
// at = CGAffineTransformTranslate(at, x, y);
// at = CGAffineTransformRotate(at, angle);
// return CGAffineTransformTranslate(at, -x, -y);
/// Flip coordinate system
//static void FlipCoordinateSystem(CGContextRef c, CGFloat height) {
// CGContextTranslateCTM(c, 0, height);
// CGContextScaleCTM(c, 1, -1);
//}
#pragma mark - CGPath Component Generators
/// Add circle with @c radius
static inline void PathAddCircle(CGMutablePathRef path, CGFloat radius) {
CGPathAddArc(path, NULL, radius, radius, radius, 0, M_PI * 2, YES);
}
/// Add ring with @c radius and @c innerRadius
static inline void PathAddRing(CGMutablePathRef path, CGFloat radius, CGFloat innerRadius) {
CGPathAddArc(path, NULL, radius, radius, radius, 0, M_PI * 2, YES);
CGPathAddArc(path, NULL, radius, radius, innerRadius, 0, M_PI * -2, YES);
}
/// Add a single RSS icon radio wave
static inline void PathAddRSSArc(CGMutablePathRef path, CGFloat radius, CGFloat thickness) {
CGPathMoveToPoint(path, NULL, 0, radius + thickness);
CGPathAddArc(path, NULL, 0, 0, radius + thickness, M_PI_2, 0, YES);
CGPathAddLineToPoint(path, NULL, radius, 0);
CGPathAddArc(path, NULL, 0, 0, radius, 0, M_PI_2, NO);
CGPathCloseSubpath(path);
}
/// Add two vertical bars representing a pause icon
static inline void PathAddPauseIcon(CGMutablePathRef path, CGAffineTransform at, CGFloat size, CGFloat thickness) {
const CGFloat off = (size - 2 * thickness) / 4;
CGPathAddRect(path, &at, CGRectMake(off, 0, thickness, size));
CGPathAddRect(path, &at, CGRectMake(size/2 + off, 0, thickness, size));
}
/// Add X icon by applying a rotational affine transform and drawing a plus sign
// void PathAddXIcon(CGMutablePathRef path, CGAffineTransform at, CGFloat size, CGFloat thickness) {
// at = RotateAroundPoint(at, M_PI_4, size/2, size/2);
// const CGFloat p = size * 0.5 - thickness / 2;
// CGPathAddRect(path, &at, CGRectMake(0, p, size, thickness));
// CGPathAddRect(path, &at, CGRectMake(p, 0, thickness, p));
// CGPathAddRect(path, &at, CGRectMake(p, p + thickness, thickness, p));
//}
#pragma mark - Full Icon Path Generators
/// Create @c CGPath for global icon; a menu bar and an open menu below
static inline void AddGlobalIconPath(CGContextRef c, CGFloat size) {
CGMutablePathRef menu = CGPathCreateMutable();
CGPathAddRect(menu, NULL, CGRectMake(0, 0.8 * size, size, 0.2 * size));
CGPathAddRect(menu, NULL, CGRectMake(0.3 * size, 0, 0.55 * size, 0.75 * size));
CGPathAddRect(menu, NULL, CGRectMake(0.35 * size, 0.05 * size, 0.45 * size, 0.75 * size));
CGFloat entryHeight = 0.1 * size; // 0.075
for (int i = 0; i < 3; i++) { // 4
//CGPathAddRect(menu, NULL, CGRectMake(0.37 * size, (2 * i + 1) * entryHeight, 0.42 * size, entryHeight)); // uncomment path above
CGPathAddRect(menu, NULL, CGRectMake(0.35 * size, (2 * i + 1.5) * entryHeight, 0.4 * size, entryHeight * 0.8));
}
CGContextAddPath(c, menu);
CGPathRelease(menu);
}
/// Create @c CGPath for group icon; a folder symbol
static inline void AddGroupIconPath(CGContextRef c, CGFloat size, BOOL showBackground) {
const CGFloat r1 = size * 0.05; // corners
const CGFloat r2 = size * 0.08; // upper part, name tag
const CGFloat r3 = size * 0.15; // lower part, corners inside
const CGFloat posTop = 0.85 * size;
const CGFloat posMiddle = 0.6 * size - r3;
const CGFloat posBottom = 0.15 * size + r1;
const CGFloat posNameTag = 0.3 * size;
CGMutablePathRef upper = CGPathCreateMutable();
CGPathMoveToPoint(upper, NULL, 0, 0.5 * size);
CGPathAddLineToPoint(upper, NULL, 0, posTop - r1);
CGPathAddArc(upper, NULL, r1, posTop - r1, r1, M_PI, M_PI_2, YES);
CGPathAddArc(upper, NULL, posNameTag, posTop - r2, r2, M_PI_2, M_PI_4, YES);
CGPathAddArc(upper, NULL, posNameTag + 1.85 * r2, posTop, r2, M_PI + M_PI_4, -M_PI_2, NO);
CGPathAddArc(upper, NULL, size - r1, posTop - r1 - r2, r1, M_PI_2, 0, YES);
CGPathAddArc(upper, NULL, size - r1, posBottom, r1, 0, -M_PI_2, YES);
CGPathAddArc(upper, NULL, r1, posBottom, r1, -M_PI_2, M_PI, YES);
CGPathCloseSubpath(upper);
CGMutablePathRef lower = CGPathCreateMutable();
CGPathAddArc(lower, NULL, r3, posMiddle, r3, M_PI, M_PI_2, YES);
CGPathAddArc(lower, NULL, size - r3, posMiddle, r3, M_PI_2, 0, YES);
CGPathAddArc(lower, NULL, size - r1, posBottom, r1, 0, -M_PI_2, YES);
CGPathAddArc(lower, NULL, r1, posBottom, r1, -M_PI_2, M_PI, YES);
CGPathCloseSubpath(lower);
CGContextAddPath(c, upper);
if (showBackground)
CGContextEOFillPath(c);
CGContextAddPath(c, lower);
CGPathRelease(upper);
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
*/
static inline void AddRSSIconPath(CGContextRef c, CGFloat size, BOOL connection) {
CGMutablePathRef bars = CGPathCreateMutable(); // the rss bars
PathAddCircle(bars, size * 0.125);
PathAddRSSArc(bars, size * 0.45, size * 0.2);
if (connection) {
PathAddRSSArc(bars, size * 0.8, size * 0.2);
} else {
CGAffineTransform at = CGAffineTransformMake(0.5, 0, 0, 0.5, size/2, size/2);
PathAddPauseIcon(bars, at, size, size * 0.3);
//PathAddXIcon(bars, at, size, size * 0.3);
}
CGContextAddPath(c, bars);
CGPathRelease(bars);
/// 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);
CGFloat offset = s * (1 - scale) / 2;
CGContextTranslateCTM(c, offset, size.height - s + offset); // top left alignment
CGContextScaleCTM(c, scale, scale);
}
@@ -168,130 +65,214 @@ static void DrawGradient(CGContextRef c, CGFloat size, NSColor *color) {
CFArrayRef colors = CFArrayCreate(NULL, cgColors, 3, NULL);
CGGradientRef gradient = CGGradientCreateWithColors(NULL, colors, NULL);
CGContextDrawLinearGradient(c, gradient, CGPointMake(0, size), CGPointMake(size, 0), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
CGContextDrawLinearGradient(c, gradient, CGPointMake(0, 0), CGPointMake(size, size), kCGGradientDrawsBeforeStartLocation | kCGGradientDrawsAfterEndLocation);
CGGradientRelease(gradient);
CFRelease(colors);
}
#pragma mark - CGContext Drawing & Manipulation
#pragma mark - RSS Icon (rounded corners)
/// 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);
CGFloat offset = s * (1 - scale) / 2;
CGContextTranslateCTM(c, offset, size.height - s + offset); // top left alignment
CGContextScaleCTM(c, scale, scale);
}
/// Helper method; set drawing color, add rounded background and prepare content scale
static void DrawRoundedFrame(CGContextRef c, CGRect r, CGColorRef color, BOOL background, CGFloat corner, CGFloat defaultScale, CGFloat scaling) {
CGContextSetFillColorWithColor(c, color);
CGContextSetStrokeColorWithColor(c, color);
CGFloat contentScale = defaultScale;
if (background) {
svgAddRect(c, 1, r, ShorterSide(r.size) * corner/2);
if (scaling != 0.0)
contentScale *= scaling;
/**
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
*/
static inline void AddRSSIconPath(CGContextRef c, CGFloat size, BOOL connection) {
svgCircle(c, size/100, 13, 87, 13, NO);
svgPath(c, size/100, "M0,35q65,0,65,65h-20q0,-45,-45,-45z");
if (connection) {
svgPath(c, size/100, "M0,0q100,0,100,100h-20q0,-80,-80,-80z");
} else {
// pause icon
svgRect(c, size/100, CGRectMake(60, 0, 15, 50));
svgRect(c, size/100, CGRectMake(85, 0, 15, 50));
}
SetContentScale(c, r.size, contentScale);
}
#pragma mark - Easy Icon Drawing Methods
/// Draw global icon (menu bar)
static void DrawGlobalIcon(CGRect r, CGColorRef color, BOOL background) {
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
DrawRoundedFrame(c, r, color, background, 0.4, 1.0, 0.7);
AddGlobalIconPath(c, ShorterSide(r.size));
CGContextEOFillPath(c);
}
/// Draw group icon (folder)
static void DrawGroupIcon(CGRect r, CGColorRef color, BOOL background) {
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
const CGFloat s = ShorterSide(r.size);
const CGFloat l = s * 0.08; // line width
DrawRoundedFrame(c, r, color, background, 0.4, 1.0 - (l / s), 0.85);
CGContextSetLineWidth(c, l * (background ? 0.5 : 1.0));
AddGroupIconPath(c, s, background);
CGContextStrokePath(c);
}
/// Draw RSS icon (flat without gradient)
static void DrawRSSIcon(CGRect r, CGColorRef color, BOOL background, BOOL connection) {
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
DrawRoundedFrame(c, r, color, background, 0.4, 1.0, 0.7);
AddRSSIconPath(c, ShorterSide(r.size), connection);
CGContextEOFillPath(c);
}
/// Draw RSS icon (with orange gradient, corner @c 0.4, white radio waves)
static void DrawRSSGradientIcon(CGRect r, NSColor *color) {
/// Draw monochrome RSS icon with rounded corners
static void RoundedRSS_Monochrome(CGRect r, BOOL connection) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
DrawRoundedFrame(c, r, NSColor.whiteColor.CGColor, YES, 0.4, 1.0, 0.7);
CGContextSetFillColorWithColor(c, [NSColor menuBarIconColor].CGColor);
// background rounded rect
svgRoundedRect(c, 1, r, size * 0.4/2);
// RSS icon
SetContentScale(c, r.size, 11/16.0);
AddRSSIconPath(c, size, connection);
CGContextEOFillPath(c);
}
/// Draw RSS icon with orange gradient background
static void RoundedRSS_Gradient(CGRect r, NSColor *color) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
CGContextSetFillColorWithColor(c, NSColor.whiteColor.CGColor);
// background rounded rect
svgRoundedRect(c, 1, r, size * 0.4/2);
// Gradient
CGContextSaveGState(c);
CGContextClip(c);
DrawGradient(c, size, color);
CGContextRestoreGState(c);
// Bars
// RSS icon
SetContentScale(c, r.size, 11/16.0);
AddRSSIconPath(c, size, YES);
CGContextEOFillPath(c);
}
#pragma mark - Appearance Settings
/// Draw icon representing global `status bar icon` (rounded RSS icon with neighbor items)
static void Appearance_MenuBarIcon(CGRect r) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
// menu bar
CGContextSetFillColorWithColor(c, [NSColor controlTextColor].CGColor);
svgRect(c, 1, CGRectInset(r, 0, size * 2/16));
CGContextFillPath(c);
// neighbors
const CGFloat offset = round(size*.75);
const CGFloat iconInset = round(size*.2);
const CGFloat iconCorner = size*.12;
CGContextSetAlpha(c, .66);
CGContextSetFillColorWithColor(c, [NSColor controlBackgroundColor].CGColor);
// left neighbor
CGContextTranslateCTM(c, -offset, 0);
svgRoundedRect(c, 1, CGRectInset(r, iconInset, iconInset), iconCorner);
CGContextFillPath(c);
// right neighbor
CGContextTranslateCTM(c, +2*offset, 0);
svgRoundedRect(c, 1, CGRectInset(r, iconInset, iconInset), iconCorner);
CGContextFillPath(c);
// main icon
CGContextSetAlpha(c, 1);
CGContextTranslateCTM(c, -offset, 0);
svgRoundedRect(c, 1, CGRectInset(r, iconInset, iconInset), iconCorner);
SetContentScale(c, r.size, 7/16.0);
AddRSSIconPath(c, size, YES);
CGContextEOFillPath(c);
}
/// Draw icon representing `Main Menu` (menu bar)
static void Appearance_MainMenu(CGRect r) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
CGContextSetFillColorWithColor(c, [NSColor controlTextColor].CGColor);
// menu
svgRect(c, size/16, CGRectMake(0, 0, 16, 3));
svgRect(c, size/16, CGRectMake(5, 4, 9, 12));
svgRect(c, size/16, CGRectMake(6, 3, 7, 12));
// entries
svgRect(c, size/16, CGRectMake(6, 12, 6, 1));
svgRect(c, size/16, CGRectMake(6, 9, 6, 1));
svgRect(c, size/16, CGRectMake(6, 6, 6, 1));
CGContextEOFillPath(c);
}
/// Draw icon representing `FeedGroup` (folder)
static void Appearance_Group(CGRect r, BOOL withLine) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
// folder path
svgPath(c, size/16, "M3,13.5c-1.5,0-2.5-1-2.5-2.5V3.5c0-1.5.5-2,2-2h1.5c1.5,0,1.5,1,3,1h6c1.5,0,2.5,1,2.5,2.5v6c0,1.5-1,2.5-2.5,2.5H3Z");
// line
if (withLine) {
svgPath(c, size/16, "M1.5,5h13Z");
}
CGContextSetLineWidth(c, size * 1/16);
CGContextSetStrokeColorWithColor(c, [NSColor controlTextColor].CGColor);
CGContextStrokePath(c);
}
/// Draw icon representing `Feed` (group + RSS)
static void Appearance_Feed(CGRect r) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
CGContextSetFillColorWithColor(c, [NSColor controlTextColor].CGColor);
// folder
Appearance_Group(r, NO);
// rss icon
SetContentScale(c, r.size, 7/16.0);
AddRSSIconPath(c, size, YES);
CGContextFillPath(c);
}
/// Draw icon representing `Article` (RSS inside text document)
static void Appearance_Article(CGRect r) {
const CGFloat size = ShorterSide(r.size);
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
CGContextSetFillColorWithColor(c, [NSColor controlTextColor].CGColor);
// text lines
svgRect(c, size/16, CGRectMake(0, 14, 16, 1));
svgRect(c, size/16, CGRectMake(0, 10, 16, 1));
svgRect(c, size/16, CGRectMake(9, 6, 7, 1));
svgRect(c, size/16, CGRectMake(9, 2, 7, 1));
// picture
//svgRect(c, size/16, CGRectMake(1, 1, 7, 7));
// RSS icon
CGContextTranslateCTM(c, size/16 * 1, size/16 * 1); // same offset as picture
CGContextScaleCTM(c, 7/16.0, 7/16.0); // same size as picture
AddRSSIconPath(c, size, YES);
CGContextEOFillPath(c);
}
#pragma mark - Other Icons
/// Draw unread icon (blue dot for unread menu item)
static void DrawUnreadIcon(CGRect r, NSColor *color) {
CGFloat size = ShorterSide(r.size) / 2.0;
const CGFloat radius = ShorterSide(r.size) / 2.0;
CGContextRef c = NSGraphicsContext.currentContext.CGContext;
CGMutablePathRef path = CGPathCreateMutable();
SetContentScale(c, r.size, 0.7);
CGContextTranslateCTM(c, 0, size * -0.15); // align with baseline of menu item text
CGContextTranslateCTM(c, 0, radius * -0.15); // align with baseline of menu item text
// outer ring (opaque)
CGContextSetFillColorWithColor(c, color.CGColor);
PathAddRing(path, size, size * 0.7);
CGPathAddArc(path, NULL, radius, radius, radius, 0, M_PI * 2, YES);
CGPathAddArc(path, NULL, radius, radius, radius*.7, 0, M_PI * -2, YES);
CGContextAddPath(c, path);
CGContextEOFillPath(c);
// inner circle (translucent)
CGContextSetFillColorWithColor(c, [color colorWithAlphaComponent:0.5].CGColor);
PathAddCircle(path, size);
CGPathAddArc(path, NULL, radius, radius, radius, 0, M_PI * 2, YES);
CGContextAddPath(c, path);
CGContextFillPath(c);
CGPathRelease(path);
}
/// Draw "(.*)" as vector 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);
// background
CGContextSetFillColorWithColor(c, NSColor.redColor.CGColor);
svgRoundedRect(c, 1, r, size * 0.4/2);
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");
// foreground
CGContextSetFillColorWithColor(c, NSColor.whiteColor.CGColor);
SetContentScale(c, r.size, 25/32.0);
// "("
svgPath(c, size/100, "M18,19c-14,21-13,43,0,62l-7,4C-4,63-4,35,12,14l6,5Z");
// "."
svgCircle(c, size/100, 31, 67, 7, NO);
// "*"
svgPath(c, size/100, "M65,28l11-4,2,6-11,4,7,9-5,4-7-9-7,9-5-4,7-9-11-4,2-6,11,4v-11h6v11Z");
// ")"
svgPath(c, size/100, "M82,81c14-21,13-43,0-62l7-5c16,22,15,50,0,71l-7-4Z");
CGContextFillPath(c);
}
@@ -301,19 +282,25 @@ static void DrawRegexIcon(CGRect r) {
/// Add single image to @c ImageNamed cache and set accessibility description
static void Register(CGFloat size, NSImageName name, NSString *description, BOOL (^draw)(NSRect r)) {
NSImage *img = [NSImage imageWithSize: NSMakeSize(size, size) flipped:NO drawingHandler:draw];
NSImage *img = [NSImage imageWithSize: NSMakeSize(size, size) flipped:YES drawingHandler:draw];
img.accessibilityDescription = description;
img.name = name;
}
/// Register all icons that require custom drawing in @c ImageNamed cache
void RegisterImageViewNames(void) {
Register(16, RSSImageDefaultRSSIcon, NSLocalizedString(@"RSS icon", nil), ^(NSRect r) { DrawRSSGradientIcon(r, [NSColor rssOrange]); return YES; });
Register(16, RSSImageSettingsGlobal, NSLocalizedString(@"Global settings", nil), ^(NSRect r) { DrawGlobalIcon(r, [NSColor controlTextColor].CGColor, NO); return YES; });
Register(16, RSSImageSettingsGroup, NSLocalizedString(@"Group settings", nil), ^(NSRect r) { DrawGroupIcon(r, [NSColor controlTextColor].CGColor, NO); return YES; });
Register(16, RSSImageSettingsFeed, NSLocalizedString(@"Feed settings", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor controlTextColor].CGColor, NO, YES); return YES; });
Register(16, RSSImageMenuBarIconActive, NSLocalizedString(@"RSS menu bar icon", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor menuBarIconColor].CGColor, YES, YES); return YES; });
Register(16, RSSImageMenuBarIconPaused, NSLocalizedString(@"RSS menu bar icon, paused", nil), ^(NSRect r) { DrawRSSIcon(r, [NSColor menuBarIconColor].CGColor, YES, NO); return YES; });
Register(14, RSSImageMenuItemUnread, NSLocalizedString(@"Unread icon", nil), ^(NSRect r) { DrawUnreadIcon(r, [NSColor unreadIndicatorColor]); return YES; });
// Default feed icon (fallback icon if no favicon found)
Register(16, RSSImageDefaultRSSIcon, NSLocalizedString(@"Default feed icon", nil), ^(NSRect r) { RoundedRSS_Gradient(r, [NSColor rssOrange]); return YES; });
// Menu bar icon
Register(16, RSSImageMenuBarIconActive, NSLocalizedString(@"Menu bar icon", nil), ^(NSRect r) { RoundedRSS_Monochrome(r, YES); return YES; });
Register(16, RSSImageMenuBarIconPaused, NSLocalizedString(@"Menu bar icon, paused", nil), ^(NSRect r) { RoundedRSS_Monochrome(r, NO); return YES; });
// Appearance settings
Register(16, RSSImageSettingsGlobalIcon, NSLocalizedString(@"Global settings, menu bar icon", nil), ^(NSRect r) { Appearance_MenuBarIcon(r); return YES; });
Register(16, RSSImageSettingsGlobalMenu, NSLocalizedString(@"Global settings, main menu", nil), ^(NSRect r) { Appearance_MainMenu(r); return YES; });
Register(16, RSSImageSettingsGroup, NSLocalizedString(@"Group settings", nil), ^(NSRect r) { Appearance_Group(r, YES); return YES; });
Register(16, RSSImageSettingsFeed, NSLocalizedString(@"Feed settings", nil), ^(NSRect r) { Appearance_Feed(r); return YES; });
Register(16, RSSImageSettingsArticle, NSLocalizedString(@"Article settings", nil), ^(NSRect r) { Appearance_Article(r); return YES; });
// Other settings
Register(14, RSSImageMenuItemUnread, NSLocalizedString(@"Unread indicator", nil), ^(NSRect r) { DrawUnreadIcon(r, [NSColor unreadIndicatorColor]); return YES; });
Register(32, RSSImageRegexIcon, NSLocalizedString(@"Regex icon", nil), ^(NSRect r) { DrawRegexIcon(r); return YES; });
}

View File

@@ -0,0 +1,6 @@
@import Cocoa;
@interface StrictUIntFormatter : NSFormatter
/// Note: must contain `%ld` and is used as formatter string.
@property (nullable, copy) NSString *unit;
@end

View File

@@ -0,0 +1,40 @@
#import "StrictUIntFormatter.h"
@implementation StrictUIntFormatter
/// Display object as integer formatted string.
- (NSString *)stringForObjectValue:(id)obj {
NSString *str = [NSString stringWithFormat:@"%@", obj];
if (str.length == 0)
return @"";
if (self.unit)
return [NSString stringWithFormat:self.unit, [str integerValue]];
return [NSString stringWithFormat:@"%ld", [str integerValue]];
}
- (NSString *)editingStringForObjectValue:(id)obj {
NSString *str = [NSString stringWithFormat:@"%@", obj];
if (str.length == 0)
return @"";
return [NSString stringWithFormat:@"%ld", [str integerValue]];
}
/// Parse any pasted input as integer.
- (BOOL)getObjectValue:(out id _Nullable __autoreleasing *)obj forString:(NSString *)string errorDescription:(out NSString *__autoreleasing _Nullable *)error {
if (string.length == 0) {
*obj = @"";
} else {
*obj = [[NSNumber numberWithInt:[string intValue]] stringValue];
}
return YES;
}
/// Only digits, no other character allowed
- (BOOL)isPartialStringValid:(NSString *__autoreleasing _Nonnull *)partialStringPtr proposedSelectedRange:(NSRangePointer)proposedSelRangePtr originalString:(NSString *)origString originalSelectedRange:(NSRange)origSelRange errorDescription:(NSString *__autoreleasing _Nullable *)error {
for (NSUInteger i = 0; i < [*partialStringPtr length]; i++) {
unichar c = [*partialStringPtr characterAtIndex:i];
if (c < '0' || c > '9')
return NO;
}
return YES;
}
@end

View File

@@ -1,5 +1,6 @@
@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);
void svgPath(CGContextRef context, CGFloat scale, const char * path);
void svgCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise);
void svgRoundedRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius);
void svgRect(CGContextRef context, CGFloat scale, CGRect rect);

View File

@@ -64,10 +64,16 @@ static void finishOp(CGMutablePathRef path, struct SVGState *state) {
state->y = state->num[1];
CGPathAddLineToPoint(path, NULL, state->x * state->scale, state->y * state->scale);
} else if (op == 'Q' && state->iNum == 4) {
state->x = state->num[2];
state->y = state->num[3];
CGPathAddQuadCurveToPoint(path, NULL, state->num[0] * state->scale, state->num[1] * state->scale, 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);
}
@@ -124,6 +130,8 @@ static void tinySVG_parse(const char * code, CGFloat scale, CGMutablePathRef pat
finishOp(path, &state);
} else if (state.iNum == 2 && strchr("MmLl", state.op) != NULL) {
finishOp(path, &state);
} else if (state.iNum == 4 && strchr("Qq", state.op) != NULL) {
finishOp(path, &state);
} else if (state.iNum == 6 && strchr("Cc", state.op) != NULL) {
finishOp(path, &state);
}
@@ -138,11 +146,17 @@ static void tinySVG_parse(const char * code, CGFloat scale, CGMutablePathRef pat
}
}
/// Helper method to scale `rect` according to svg size.
static inline CGRect scaledRect(CGRect rect, CGFloat scale) {
if (scale == 1.0) { return rect; }
return CGRectMake(rect.origin.x * scale, rect.origin.y * scale, rect.size.width * scale, rect.size.height * scale);
}
# pragma mark - External API
/// calls @c tinySVG_path and handles @c CGPath creation and release.
void svgAddPath(CGContextRef context, CGFloat scale, const char * code) {
void svgPath(CGContextRef context, CGFloat scale, const char * code) {
CGMutablePathRef path = CGPathCreateMutable();
tinySVG_parse(code, scale, path);
CGContextAddPath(context, path);
@@ -150,22 +164,24 @@ void svgAddPath(CGContextRef context, CGFloat scale, const char * code) {
}
/// calls @c CGPathAddArc with full circle
void svgAddCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise) {
void svgCircle(CGContextRef context, CGFloat scale, CGFloat x, CGFloat y, CGFloat radius, bool clockwise) {
// No `CGContextAddArc` because that doesnt work well with overlapping counter-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);
}
/// Calls @c CGPathAddRoundedRect
/// @param cornerRadius Use half of @c min(w,h) for a full circle.
void svgRoundedRect(CGContextRef context, CGFloat scale, CGRect rect, CGFloat cornerRadius) {
CGMutablePathRef tmp = CGPathCreateMutable();
CGPathAddRoundedRect(tmp, NULL, scaledRect(rect, scale), cornerRadius * scale, cornerRadius * scale);
CGContextAddPath(context, tmp);
CGPathRelease(tmp);
}
/// Calls @c CGContextAddRect
void svgRect(CGContextRef context, CGFloat scale, CGRect rect) {
CGContextAddRect(context, scaledRect(rect, scale));
}

View File

@@ -15,30 +15,35 @@
/** 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 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";
/** default: @c YES */ static NSString* const Pref_groupMarkUnread = @"groupMarkUnread";
/** default: @c NO */ static NSString* const Pref_groupUnreadOnly = @"groupUnreadOnly";
/** default: @c YES */ static NSString* const Pref_groupUnreadCount = @"groupUnreadCount";
/** default: @c NO */ static NSString* const Pref_groupUnreadIndicator = @"groupUnreadIndicator";
/** default: @c YES */ static NSString* const Pref_feedOpenUnread = @"feedOpenUnread";
/** default: @c YES */ static NSString* const Pref_feedMarkRead = @"feedMarkRead";
/** default: @c YES */ static NSString* const Pref_feedMarkUnread = @"feedMarkUnread";
/** default: @c NO */ static NSString* const Pref_feedUnreadOnly = @"feedUnreadOnly";
/** default: @c YES */ static NSString* const Pref_feedUnreadCount = @"feedUnreadCount";
/** default: @c YES */ static NSString* const Pref_feedUnreadIndicator = @"feedUnreadIndicator";
/** default: @c NO */ static NSString* const Pref_feedTruncateTitle = @"feedTruncateTitle";
/** default: @c NO */ static NSString* const Pref_feedLimitArticles = @"feedLimitArticles";
// menu buttons
/** default: @c NO */ static NSString* const Pref_globalToggleHidden = @"globalToggleHidden";
/** 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_groupOpenUnread = @"groupOpenUnread";
/** default: @c YES */ static NSString* const Pref_feedOpenUnread = @"feedOpenUnread";
/** default: @c YES */ static NSString* const Pref_globalMarkRead = @"globalMarkRead";
/** default: @c YES */ static NSString* const Pref_groupMarkRead = @"groupMarkRead";
/** default: @c YES */ static NSString* const Pref_feedMarkRead = @"feedMarkRead";
/** default: @c YES */ static NSString* const Pref_globalMarkUnread = @"globalMarkUnread";
/** default: @c YES */ static NSString* const Pref_groupMarkUnread = @"groupMarkUnread";
/** default: @c YES */ static NSString* const Pref_feedMarkUnread = @"feedMarkUnread";
// display options
/** default: @c YES */ static NSString* const Pref_globalUnreadCount = @"globalUnreadCount";
/** default: @c YES */ static NSString* const Pref_groupUnreadCount = @"groupUnreadCount";
/** default: @c YES */ static NSString* const Pref_feedUnreadCount = @"feedUnreadCount";
/** default: @c YES */ static NSString* const Pref_globalTintMenuIcon = @"globalTintMenuBarIcon";
/** default: @c NO */ static NSString* const Pref_groupUnreadIndicator = @"groupUnreadIndicator";
/** default: @c NO */ static NSString* const Pref_feedUnreadIndicator = @"feedUnreadIndicator";
/** default: @c YES */ static NSString* const Pref_articleUnreadIndicator = @"articleUnreadIndicator";
/** default: @c NO */ static NSString* const Pref_groupUnreadOnly = @"groupUnreadOnly";
/** default: @c NO */ static NSString* const Pref_feedUnreadOnly = @"feedUnreadOnly";
/** default: @c NO */ static NSString* const Pref_articleUnreadOnly = @"articleUnreadOnly";
// article display
/** default: @c -1 */ static NSString* const Pref_articleCountLimit = @"articleCountLimit";
/** default: @c -1 */ static NSString* const Pref_articleTitleLimit = @"articleTitleLimit";
/** default: @c 2k */ static NSString* const Pref_articleTooltipLimit = @"articleTooltipLimit";
// ------ Hidden preferences ------ only modifiable via `defaults write de.relikd.baRSS {KEY}` ------
/** default: @c 10 */ static NSString* const Pref_openFewLinksLimit = @"openFewLinksLimit";
/** default: @c 60 */ static NSString* const Pref_shortArticleNamesLimit = @"shortArticleNamesLimit";
/** default: @c 40 */ static NSString* const Pref_articlesInMenuLimit = @"articlesInMenuLimit";
/** default: @c nil */ static NSString* const Pref_colorStatusIconTint = @"colorStatusIconTint";
/** default: @c nil */ static NSString* const Pref_colorUnreadIndicator = @"colorUnreadIndicator";

View File

@@ -17,18 +17,18 @@ void UserPrefsInit(void) {
Pref_globalMarkRead, Pref_groupMarkRead, Pref_feedMarkRead,
Pref_globalMarkUnread, Pref_groupMarkUnread, Pref_feedMarkUnread,
Pref_globalUnreadCount, Pref_groupUnreadCount, Pref_feedUnreadCount,
Pref_feedUnreadIndicator
Pref_articleUnreadIndicator
]);
defaultsAppend(defs, @NO, @[
Pref_groupUnreadOnly, Pref_feedUnreadOnly,
Pref_groupUnreadIndicator,
Pref_feedTruncateTitle,
Pref_feedLimitArticles
Pref_globalToggleHidden,
Pref_groupUnreadOnly, Pref_feedUnreadOnly, Pref_articleUnreadOnly,
Pref_groupUnreadIndicator, Pref_feedUnreadIndicator,
]);
// Display limits & truncation ( defaults write de.relikd.baRSS {KEY} -int 10 )
[defs setObject:[NSNumber numberWithUnsignedInteger:10] forKey:Pref_openFewLinksLimit];
[defs setObject:[NSNumber numberWithUnsignedInteger:60] forKey:Pref_shortArticleNamesLimit];
[defs setObject:[NSNumber numberWithUnsignedInteger:40] forKey:Pref_articlesInMenuLimit];
[defs setObject:[NSNumber numberWithInteger:-1] forKey:Pref_articleCountLimit];
[defs setObject:[NSNumber numberWithInteger:-1] forKey:Pref_articleTitleLimit];
[defs setObject:[NSNumber numberWithInteger:2000] forKey:Pref_articleTooltipLimit];
[defs setObject:[NSNumber numberWithUnsignedInteger:1] forKey:Pref_prefSelectedTab]; // feed tab
[[NSUserDefaults standardUserDefaults] registerDefaults:defs];
}

View File

@@ -36,6 +36,7 @@ static inline CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.fr
// UI: TextFields
+ (NSTextField*)label:(NSString*)text;
+ (NSTextField*)inputField:(NSString*)placeholder width:(CGFloat)w;
+ (NSTextField*)integerField:(NSString*)placeholder unit:(nullable NSString*)unit width:(CGFloat)w;
+ (NSView*)labelColumn:(NSArray<NSString*>*)labels rowHeight:(CGFloat)h padding:(CGFloat)pad;
// UI: Buttons
+ (NSButton*)button:(NSString*)text;
@@ -52,7 +53,7 @@ static inline CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.fr
+ (nullable NSView*)radioGroup:(NSArray<NSString*>*)entries;
// UI: Enclosing Container
+ (NSPopover*)popover:(NSSize)size;
- (NSScrollView*)wrapContent:(NSView*)content inScrollView:(NSRect)rect;
- (NSScrollView*)wrapInScrollView:(NSSize)size;
+ (NSView*)wrapView:(NSView*)other withLabel:(NSString*)str padding:(CGFloat)pad;
// Insert UI elements in parent view
- (instancetype)placeIn:(NSView*)parent x:(CGFloat)x y:(CGFloat)y;
@@ -60,7 +61,10 @@ static inline CGFloat NSMaxWidth(NSView *a, NSView *b) { return Max(NSWidth(a.fr
- (instancetype)placeIn:(NSView*)parent xRight:(CGFloat)x y:(CGFloat)y;
- (instancetype)placeIn:(NSView*)parent xRight:(CGFloat)x yTop:(CGFloat)y;
// Modify existing UI elements
- (instancetype)alignTop;
- (instancetype)alignRight;
- (instancetype)sizableWidthAndHeight;
- (instancetype)sizableWidth;
- (instancetype)sizeToRight:(CGFloat)rightPadding;
- (instancetype)sizeWidthToFit;
- (instancetype)tooltip:(NSString*)tt;

View File

@@ -1,4 +1,5 @@
#import "NSView+Ext.h"
#import "StrictUIntFormatter.h"
@implementation NSView (Ext)
@@ -27,6 +28,15 @@
return input;
}
/// Create input text field which only accepts integer values. (calls `inputField`) `21px` height.
/// `field.formatter` is of type `StrictUIntFormatter`.
+ (NSTextField*)integerField:(NSString*)placeholder unit:(nullable NSString*)unit width:(CGFloat)w {
NSTextField *input = [self inputField:placeholder width:w];
input.formatter = [StrictUIntFormatter new];
((StrictUIntFormatter*)input.formatter).unit = unit;
return input;
}
/// Create view with @c NSTextField subviews with right-aligned and row-centered text from @c labels.
+ (NSView*)labelColumn:(NSArray<NSString*>*)labels rowHeight:(CGFloat)h padding:(CGFloat)pad {
CGFloat w = 0, y = 0;
@@ -170,17 +180,19 @@
return pop;
}
/// Insert @c scrollView, remove @c self from current view and set as @c documentView for the newly created scroll view.
- (NSScrollView*)wrapContent:(NSView*)content inScrollView:(NSRect)rect {
NSScrollView *scroll = [[[NSScrollView alloc] initWithFrame:rect] sizableWidthAndHeight];
/// Removes `self` from current view (if already added) and sets `documentView` content for the newly created scroll view.
/// You are responsible for adding this scroll view to the view hierarchy.
- (NSScrollView*)wrapInScrollView:(NSSize)size {
NSScrollView *scroll = [[[NSScrollView alloc] initWithFrame:CGRectMake(0, 0, size.width, size.height)] sizableWidthAndHeight];
scroll.borderType = NSBezelBorder;
scroll.hasVerticalScroller = YES;
scroll.horizontalScrollElasticity = NSScrollElasticityNone;
[self addSubview:scroll];
if (content.superview) [content removeFromSuperview]; // remove if added already (e.g., helper methods above)
content.frame = NSMakeRect(0, 0, scroll.contentSize.width, scroll.contentSize.height);
scroll.documentView = content;
if (self.superview) [self removeFromSuperview]; // remove if added already (e.g., helper methods above)
if (self.frame.size.width == 0 && self.frame.size.height == 0) {
self.frame = NSMakeRect(0, 0, scroll.contentSize.width, scroll.contentSize.height);
}
scroll.documentView = self;
return scroll;
}
@@ -257,6 +269,9 @@
/// Modify @c .autoresizingMask; Add @c NSViewWidthSizable @c | @c NSViewHeightSizable flags
- (instancetype)sizableWidthAndHeight { self.autoresizingMask |= NSViewWidthSizable | NSViewHeightSizable; return self; }
/// Modify @c .autoresizingMask; Add @c NSViewWidthSizable flags
- (instancetype)sizableWidth { self.autoresizingMask |= NSViewWidthSizable; return self; }
/// Extend frame in its @c superview and stick to right with padding. Adds @c NSViewWidthSizable to @c autoresizingMask
- (instancetype)sizeToRight:(CGFloat)rightPadding {
SetFrameWidth(self, NSWidth(self.superview.frame) - NSMinX(self.frame) - rightPadding + self.alignmentRectInsets.right);
@@ -273,10 +288,12 @@
/// Set @c tooltip and @c accessibilityTitle of view and return self
- (instancetype)tooltip:(NSString*)tt {
self.toolTip = tt;
if (self.accessibilityLabel.length == 0)
self.accessibilityLabel = tt;
else
if ([self isKindOfClass:[NSTextField class]] && ((NSTextField*)self).editable == NO) {
// a label already shows text, so the tooltip will probably be extended information.
self.accessibilityHelp = tt;
} else {
self.accessibilityValueDescription = tt;
}
return self;
}

View File

@@ -2,6 +2,7 @@
#import "UserPrefs.h"
#import "StoreCoordinator.h"
#import "Feed+Ext.h"
#import "FeedGroup+Ext.h"
#import "FeedArticle+Ext.h"
/**
@@ -9,6 +10,11 @@
*/
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
@@ -18,20 +24,27 @@ 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];
UNUserNotificationCenter.currentNotificationCenter.delegate = singleton;
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]];
[UNUserNotificationCenter.currentNotificationCenter requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionSound completionHandler:^(BOOL granted, NSError * _Nullable error) {
[center requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionSound completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
NSAlert *alert = [[NSAlert alloc] init];
@@ -72,7 +85,7 @@ static NotificationType notifyType;
if (count > 0) {
[feed.managedObjectContext obtainPermanentIDsForObjects:@[feed] error:nil];
[self send:feed.notificationID
title:feed.title
title:feed.group.anyName
body:[NSString stringWithFormat:NSLocalizedString(@"%ld unread articles", nil), count]];
}
}
@@ -84,7 +97,7 @@ static NotificationType notifyType;
}
[article.managedObjectContext obtainPermanentIDsForObjects:@[article] error:nil];
[self send:article.notificationID
title:article.feed.title
title:article.feed.group.anyName
body:article.title];
}
@@ -105,6 +118,7 @@ static NotificationType notifyType;
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];
@@ -167,7 +181,12 @@ static NotificationType notifyType;
return;
}
}
[StoreCoordinator updateArticles:articles markRead:YES andOpen:YES inContext:moc];
// 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

View File

@@ -24,7 +24,7 @@
tv.alignment = NSTextAlignmentCenter;
tv.editable = NO; // but selectable
[tv.textStorage setAttributedString:[self rtfDocument]];
[self wrapContent:tv inScrollView:NSMakeRect(-1, 20, NSWidth(self.frame) + 2, NSMinY(lblV.frame) - PAD_M - 20)];
[[tv wrapInScrollView:NSMakeSize(NSWidth(self.frame) + 2, NSMinY(lblV.frame) - PAD_M - 20)] placeIn:self x:-1 y:20];
return self;
}

View File

@@ -8,10 +8,12 @@
- (void)loadView {
self.view = [SettingsAppearanceView new];
for (NSButton *button in self.view.subviews) {
if ([button isKindOfClass:[NSButton class]]) { // for all checkboxes
[button setAction:@selector(didSelectCheckbox:)];
[button setTarget:self];
NSScrollView *scroll = self.view.subviews[0];
NSView *contentView = scroll.documentView.subviews[0];
for (NSControl *control in contentView.subviews) {
if ([control isKindOfClass:[NSButton class]]) { // for all checkboxes
[control setAction:@selector(didSelectCheckbox:)];
[control setTarget:self];
}
}
}

View File

@@ -1,6 +1,6 @@
@import Cocoa;
@class SettingsAppearance;
@interface SettingsAppearanceView : NSView
@interface SettingsAppearanceView : NSView <NSTextFieldDelegate>
@end

View File

@@ -2,63 +2,264 @@
#import "NSView+Ext.h"
#import "Constants.h" // column icons
#import "UserPrefs.h" // preference constants & UserPrefsBool()
#import "DrawImage.h" // DrawSeparator
@interface FlippedView : NSView @end
@implementation FlippedView
- (BOOL)isFlipped { return YES; }
@end
@interface SettingsAppearanceView()
@property (assign) CGFloat y;
@property (assign) NSView *content;
@property (strong) NSMutableArray<NSString*> *columns;
@end
/***/ static CGFloat const IconSize = 18;
/***/ static CGFloat const colWidth = (IconSize + PAD_M); // checkbox column width
/***/ static CGFloat const X__ = PAD_WIN + 0 * colWidth;
/***/ static CGFloat const _X_ = PAD_WIN + 1 * colWidth;
/***/ static CGFloat const __X = PAD_WIN + 2 * colWidth;
/***/ static CGFloat const X___ = PAD_WIN + 0 * colWidth;
/***/ static CGFloat const _X__ = PAD_WIN + 1 * colWidth;
/***/ static CGFloat const __X_ = PAD_WIN + 2 * colWidth;
/***/ static CGFloat const ___X = PAD_WIN + 3 * colWidth;
/***/ static CGFloat const lbl_start = PAD_WIN + 4 * colWidth;
@implementation SettingsAppearanceView
- (instancetype)init {
self = [super initWithFrame:NSMakeRect(0, 0, 320, 327)];
// Insert matrix header (icons above checkbox matrix)
ColumnIcon(self, X__, RSSImageSettingsGlobal, NSLocalizedString(@"Show in menu bar", nil));
ColumnIcon(self, _X_, RSSImageSettingsGroup, NSLocalizedString(@"Show in group menu", nil));
ColumnIcon(self, __X, RSSImageSettingsFeed, NSLocalizedString(@"Show in feed menu", nil));
// Generate checkbox matrix
self.y = PAD_WIN + IconSize + PAD_S;
[self entry:NSLocalizedString(@"Tint menu bar icon on unread", nil) c1:Pref_globalTintMenuIcon c2:nil c3:nil];
[self entry:NSLocalizedString(@"Update all feeds", nil) c1:Pref_globalUpdateAll c2:nil c3:nil];
[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(@"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]
tooltip:NSLocalizedString(@"Display at most 40 articles in feed menu", nil)];
self.y = PAD_WIN;
// stupidly complex, nested UI just because you cant top-align `.documentView`
// View is 0.5px shorter than self.frame because it will otherwise add a transparency to the TabBar
NSScrollView *scroll = [[[FlippedView new] wrapInScrollView:NSMakeSize(320, 326.5)] placeIn:self x:0 y:0];
self.content = [[[NSView alloc] initWithFrame:scroll.documentView.frame] placeIn:scroll.documentView x:0 y:0];
scroll.borderType = NSNoBorder;
// fix default window background color instead of pure black/white
scroll.drawsBackground = NO;
[self note:NSLocalizedString(@"Hover over the options for additional explanations and usage tips.", nil)];
// Menu Buttons
[self section:NSLocalizedString(@"Menu buttons", nil)];
[self columns:@[
RSSImageSettingsGlobalMenu, NSLocalizedString(@"Main menu", nil),
RSSImageSettingsGroup, NSLocalizedString(@"Group menu", nil),
RSSImageSettingsFeed, NSLocalizedString(@"Feed menu", nil),
]];
[self entry:NSLocalizedString(@"“Show hidden feeds”", nil)
help:NSLocalizedString(@"Show button to quickly toggle whether hidden entries should be shown. See option “Show only unread”.", nil)
tip:NSLocalizedString(@"You can hold down option-key before opening the main menu to temporarily show all hidden entries.", nil)
c1:Pref_globalToggleHidden c2:nil c3:nil c4:nil];
[self entry:NSLocalizedString(@"“Update all feeds”", nil)
help:NSLocalizedString(@"Show button to reload all feeds. This will force fetch new online content regardless of next-update timer.", nil)
tip:nil
c1:Pref_globalUpdateAll c2:nil c3:nil c4:nil];
[self entry:NSLocalizedString(@"“Open all unread”", nil)
help:NSLocalizedString(@"Show button to open unread articles.", nil)
tip:nil
c1:Pref_globalOpenUnread c2:Pref_groupOpenUnread c3:Pref_feedOpenUnread c4:nil];
[self entry:NSLocalizedString(@"“Mark all read”", nil)
help:NSLocalizedString(@"Show button to mark articles read.", nil)
tip:nil
c1:Pref_globalMarkRead c2:Pref_groupMarkRead c3:Pref_feedMarkRead c4:nil];
[self entry:NSLocalizedString(@"“Mark all unread”", nil)
help:NSLocalizedString(@"Show button to mark articles unread.", nil)
tip:NSLocalizedString(@"Alternatively, you can hold down option-key and click on an article to toggle that item (un-)read.", nil)
c1:Pref_globalMarkUnread c2:Pref_groupMarkUnread c3:Pref_feedMarkUnread c4:nil];
// self.y += PAD_M;
[self intInput:Pref_openFewLinksLimit
unit:NSLocalizedString(@"%ld unread", nil)
label:NSLocalizedString(@"“Open a few unread” ⌥", nil)
help:NSLocalizedString(@"If you hold down option-key, the “Open all unread” button becomes an “Open a few unread” button.", nil)];
// self.y += PAD_M;
// [self note:NSLocalizedString(@"Hold down option-key and click on an article to toggle that item (un-)read.", nil)];
// Display options
[self section:NSLocalizedString(@"Display options", nil)];
[self columns:@[
RSSImageSettingsGlobalIcon, NSLocalizedString(@"Menu bar icon", nil),
RSSImageSettingsGroup, NSLocalizedString(@"Group menu item", nil),
RSSImageSettingsFeed, NSLocalizedString(@"Feed menu item", nil),
RSSImageSettingsArticle, NSLocalizedString(@"Article menu item", nil),
]];
[self entry:NSLocalizedString(@"Number of unread articles", nil)
help:NSLocalizedString(@"Show count of unread articles in parenthesis.", nil)
tip:nil
c1:Pref_globalUnreadCount c2:Pref_groupUnreadCount c3:Pref_feedUnreadCount c4:nil];
[self entry:NSLocalizedString(@"Color for unread articles", nil)
help:NSLocalizedString(@"Show color marker on menu items with unread articles.", nil)
tip:nil
c1:Pref_globalTintMenuIcon c2:Pref_groupUnreadIndicator c3:Pref_feedUnreadIndicator c4:Pref_articleUnreadIndicator];
[self entry:NSLocalizedString(@"Show only unread", nil)
help:NSLocalizedString(@"Hide articles which have been read.", nil)
tip:nil
c1:nil c2:Pref_groupUnreadOnly c3:Pref_feedUnreadOnly c4:Pref_articleUnreadOnly];
// self.y += PAD_M;
// [self note:NSLocalizedString(@"Hold down option-key before opening the main menu to temporarily show hidden feeds.", nil)];
// Other UI elements
[self section:NSLocalizedString(@"Article display", nil)];
[self intInput:Pref_articleCountLimit
unit:NSLocalizedString(@"%ld entries", nil)
label:NSLocalizedString(@"Limit number of articles", nil)
help:NSLocalizedString(@"Display at most X articles in feed menu. Remaining articles will be hidden from view but are still there. Unread count may be confusing because hidden articles are counted too.", nil)];
[self intInput:Pref_articleTitleLimit
unit:NSLocalizedString(@"%ld chars", nil)
label:NSLocalizedString(@"Truncate article title", nil)
help:NSLocalizedString(@"Truncate article title after X characters. If a title is longer than that, show an ellipsis character “…”.", nil)];
[self intInput:Pref_articleTooltipLimit
unit:NSLocalizedString(@"%ld chars", nil)
label:NSLocalizedString(@"Truncate article tooltip", nil)
help:NSLocalizedString(@"Truncate article tooltip after X characters. This tooltip shows the whole article content (if provided by the server).", nil)];
self.y += PAD_WIN;
// sest final view size
[[self.content sizableWidth] setFrameSize:NSMakeSize(NSWidth(self.content.frame), self.y)];
[[scroll.documentView sizableWidth] setFrame:self.content.frame];
return self;
}
/// Helper method for matrix table header icons
static inline void ColumnIcon(id this, CGFloat x, const NSImageName img, NSString *ttip) {
[[[NSView imageView:img size:IconSize] placeIn:this x:x yTop:PAD_WIN] tooltip:ttip];
// MARK: - Section Header
- (void)section:(NSString*)title {
self.y += PAD_L;
NSTextField *label = [[[NSView label:title] placeIn:self.content x:PAD_WIN yTop:self.y] large];
[[[DrawSeparator withSize:NSMakeSize(100, NSHeight(label.frame))] placeIn:self.content x:NSMaxX(label.frame) + PAD_S yTop:self.y] sizeToRight:0];
self.y += NSHeight(label.frame) + PAD_M;
}
// MARK: - Column Icons
/// Helper method for matrix table header icons
- (void)columns:(NSArray<NSString*>*)columns {
self.columns = [NSMutableArray arrayWithCapacity:4];
for (NSUInteger i = 0; i < columns.count / 2; i++) {
NSString *img = columns[i*2];
NSString *ttip = columns[i*2 + 1];
[[[NSView imageView:img size:IconSize] tooltip:ttip]
placeIn:self.content x:PAD_WIN + i * colWidth yTop:self.y]
.accessibilityLabel = NSLocalizedString(@"Column header:", nil);
[self.columns addObject:ttip ? ttip : @""];
}
self.y += HEIGHT_INPUTFIELD + PAD_S;
}
// MARK: - Notes
- (void)note:(NSString*)text {
NSTextField *lbl = [[[NSView label:text] multiline:NSMakeSize(320 - 2*PAD_WIN, 7 * HEIGHT_LABEL)] gray];
NSSize bestSize = [lbl sizeThatFits:lbl.frame.size];
[lbl setFrameSize:bestSize];
[[lbl placeIn:self.content x:PAD_WIN yTop:self.y] sizeToRight:PAD_WIN];
self.y += NSHeight(lbl.frame);
}
// MARK: - Checkboxes
/// Helper method for generating a checkbox
static inline NSButton* Checkbox(id this, CGFloat x, CGFloat y, NSString *key) {
NSButton *check = [[NSView checkbox: UserPrefsBool(key)] placeIn:this x:x yTop:y];
static inline NSButton* Checkbox(SettingsAppearanceView *self, CGFloat x, NSString *key) {
NSButton *check = [[NSView checkbox:UserPrefsBool(key)] placeIn:self.content x:x+2 yTop:self.y+2];
check.identifier = key;
return check;
}
/// Create new entry with 1-3 checkboxes and a descriptive label
- (NSTextField*)entry:(NSString*)label c1:(NSString*)pref1 c2:(NSString*)pref2 c3:(NSString*)pref3 {
CGFloat y = self.y;
- (NSTextField*)entry:(NSString*)label help:(NSString*)ttip tip:(NSString*)extraTip
c1:(NSString*)pref1 c2:(NSString*)pref2 c3:(NSString*)pref3 c4:(NSString*)pref4
{
if (pref1) Checkbox(self, X___, pref1).accessibilityLabel = [NSString stringWithFormat:@"%@: %@", _columns[0], label];
if (pref2) Checkbox(self, _X__, pref2).accessibilityLabel = [NSString stringWithFormat:@"%@: %@", _columns[1], label];
if (pref3) Checkbox(self, __X_, pref3).accessibilityLabel = [NSString stringWithFormat:@"%@: %@", _columns[2], label];
if (pref4) Checkbox(self, ___X, pref4).accessibilityLabel = [NSString stringWithFormat:@"%@: %@", _columns[3], label];
if (extraTip != nil) {
label = [label stringByAppendingString:@" 💡"];
ttip = [ttip stringByAppendingFormat:@"\n\n💡 Tip: %@", extraTip];
}
NSTextField *lbl = [[[[NSView label:label] tooltip:ttip] placeIn:self.content x:lbl_start yTop:self.y] sizeToRight:PAD_WIN];
self.y += (PAD_S + HEIGHT_LABEL);
// TODO: localize: global, group, feed
if (pref1) Checkbox(self, X__ + 2, y + 2, pref1).accessibilityLabel = [label stringByAppendingString:@" (global)"];
if (pref2) Checkbox(self, _X_ + 2, y + 2, pref2).accessibilityLabel = [label stringByAppendingString:@" (group)"];
if (pref3) Checkbox(self, __X + 2, y + 2, pref3).accessibilityLabel = [label stringByAppendingString:@" (feed)"];
return [[[NSView label:label] placeIn:self x:PAD_WIN + 3 * colWidth yTop:y] sizeToRight:PAD_WIN];
return lbl;
}
// MARK: - Int Input Field
/// Create input field for integer numbers
- (NSTextField*)intInput:(NSString*)pref unit:(NSString*)unit label:(NSString*)label help:(NSString*)ttip {
// input field
NSTextField *rv = [[NSView integerField:@"" unit:unit width:3 * colWidth + IconSize] placeIn:self.content x:PAD_WIN yTop:self.y];
rv.placeholderString = NSLocalizedString(@"no limit", nil);
// sadly, setting `accessibilityLabel` will break VoiceOver on empty input.
// keep disabled so VoceOver will read the placeholder string if empty.
rv.accessibilityLabel = label;
rv.identifier = pref;
rv.delegate = self;
NSInteger val = UserPrefsInt(pref);
if (val >= 0) {
rv.stringValue = [NSString stringWithFormat:@"%ld", val];
} else {
rv.accessibilityValueDescription = rv.placeholderString;
}
// label
[[[[NSView label:label] tooltip:ttip] placeIn:self.content x:lbl_start yTop:self.y + (HEIGHT_INPUTFIELD - HEIGHT_LABEL) / 2] sizeToRight:PAD_WIN];
self.y += HEIGHT_INPUTFIELD + PAD_S;
return rv;
}
- (void)controlTextDidEndEditing:(NSNotification *)obj {
NSTextField *sender = obj.object;
NSString *pref = sender.identifier;
NSInteger newVal = sender.integerValue;
BOOL isEmpty = newVal == 0 && sender.stringValue.length == 0;
sender.accessibilityValueDescription = isEmpty ? sender.placeholderString : nil;
UserPrefsSetInt(pref, isEmpty ? -1 : newVal);
BOOL hitReturn = [[obj.userInfo valueForKey:NSTextMovementUserInfoKey] integerValue] == NSTextMovementReturn;
if (hitReturn) {
// Allow to deselect NSTextField (when pressing enter to confirm change)
[self.window performSelector:@selector(makeFirstResponder:) withObject:nil afterDelay:0];
}
}
// Allow to deselect all NSTextFields (via tab focus cycling)
// Also: opens view with no NSTextField selected.
- (BOOL)acceptsFirstResponder {
return YES;
}
// Allow to deselect all NSTextFields (by clicking outside / somewhere on the window)
- (void)mouseDown:(NSEvent *)event {
[self.window performSelector:@selector(makeFirstResponder:) withObject:nil afterDelay:0];
// perform selector because otherwise it will raise an issue of different QoS levels
}
@end

View File

@@ -3,8 +3,6 @@
#import "NSView+Ext.h"
#import "Constants.h"
@interface StrictUIntFormatter : NSFormatter
@end
@implementation ModalFeedEditView
@@ -34,7 +32,7 @@
self.name = [[[NSView inputField:NSLocalizedString(@"Example Title", nil) width:0] placeIn:self x:x yTop:rowHeight] sizeToRight:PAD_S + 18];
self.spinnerName = [[NSView activitySpinner] placeIn:self xRight:1 yTop:rowHeight + 2.5];
// 3. row
self.refreshNum = [[NSView inputField:@"30" width:85] placeIn:self x:x yTop:2*rowHeight];
self.refreshNum = [[NSView integerField:@"∞" unit:nil 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]
@@ -44,11 +42,11 @@
// initial state
self.url.accessibilityLabel = lbls[0];
self.name.accessibilityLabel = lbls[1];
self.favicon.accessibilityLabel = nil; // disable `accessibilityDescription` of `RSSImageDefaultRSSIcon`
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;
}
@@ -67,29 +65,3 @@
}
@end
#pragma mark - StrictUIntFormatter -
@implementation StrictUIntFormatter
/// Display object as integer formatted string.
- (NSString *)stringForObjectValue:(id)obj {
return [NSString stringWithFormat:@"%d", [[NSString stringWithFormat:@"%@", obj] intValue]];
}
/// Parse any pasted input as integer.
- (BOOL)getObjectValue:(out id _Nullable __autoreleasing *)obj forString:(NSString *)string errorDescription:(out NSString *__autoreleasing _Nullable *)error {
*obj = [[NSNumber numberWithInt:[string intValue]] stringValue];
return YES;
}
/// Only digits, no other character allowed
- (BOOL)isPartialStringValid:(NSString *__autoreleasing _Nonnull *)partialStringPtr proposedSelectedRange:(NSRangePointer)proposedSelRangePtr originalString:(NSString *)origString originalSelectedRange:(NSRange)origSelRange errorDescription:(NSString *__autoreleasing _Nullable *)error {
for (NSUInteger i = 0; i < [*partialStringPtr length]; i++) {
unichar c = [*partialStringPtr characterAtIndex:i];
if (c < '0' || c > '9')
return NO;
}
return YES;
}
@end

View File

@@ -17,7 +17,7 @@
if (self) {
self.controller = delegate; // make sure its first
self.outline = [self generateOutlineView]; // uses self.controller
[self wrapContent:self.outline inScrollView:NSMakeRect(0, 20, NSWidth(self.frame), NSHeight(self.frame) - 20)];
[[self.outline wrapInScrollView:NSMakeSize(NSWidth(self.frame), NSHeight(self.frame) - 20)] placeIn:self x:0 y:20];
self.outline.menu = [self generateCommandsMenu];
[self.outline.menu.itemArray makeObjectsPerformSelector:@selector(setTarget:) withObject:delegate];
CGFloat x = [self generateButtons]; // uses self.controller and self.outline
@@ -170,7 +170,7 @@
NSUserInterfaceItemIdentifier const CustomCellName = @"NameColumnCell";
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
self = [super initWithFrame:NSMakeRect(0, 0, 100, 0)];
self.identifier = CustomCellName;
self.imageView = [[NSView imageView:nil size:16] placeIn:self x:1 yTop:1];
self.imageView.accessibilityLabel = NSLocalizedString(@"Feed icon", nil);
@@ -195,7 +195,7 @@ NSUserInterfaceItemIdentifier const CustomCellName = @"NameColumnCell";
NSUserInterfaceItemIdentifier const CustomCellRefresh = @"RefreshColumnCell";
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
self = [super initWithFrame:NSMakeRect(0, 0, 100, 0)];
self.identifier = CustomCellRefresh;
self.textField = [[[[NSView label:@""] textRight] placeIn:self x:0 yTop:0] sizeToRight:0];
self.textField.accessibilityTitle = @" "; // otherwise groups and separators will say 'text'
@@ -210,7 +210,8 @@ NSUserInterfaceItemIdentifier const CustomCellRefresh = @"RefreshColumnCell";
}
self.textField.objectValue = str;
self.textField.textColor = (str.length > 1 ? [NSColor controlTextColor] : [NSColor disabledControlTextColor]);
self.textField.accessibilityLabel = (str.length > 1 ? NSLocalizedString(@"Refresh interval", nil) : nil);
self.textField.accessibilityLabel = (str.length > 0 ? NSLocalizedString(@"Refresh interval", nil) : nil);
[self.textField tooltip:(str.length == 1 ? NSLocalizedString(@"manually", nil) : nil)];
}
@end
@@ -224,7 +225,7 @@ NSUserInterfaceItemIdentifier const CustomCellRefresh = @"RefreshColumnCell";
NSUserInterfaceItemIdentifier const CustomCellSeparator = @"SeparatorColumnCell";
- (instancetype)initWithFrame:(NSRect)frameRect {
self = [super initWithFrame:frameRect];
self = [super initWithFrame:NSMakeRect(0, 0, 100, 0)];
self.identifier = CustomCellSeparator;
[[[[DrawSeparator alloc] initWithFrame:self.frame] placeIn:self x:0 y:0] sizableWidthAndHeight];
return self;

View File

@@ -59,7 +59,7 @@ static CGFloat const heightRow = PAD_S + HEIGHT_INPUTFIELD;
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)];
NSScrollView *scroll = [[tv wrapInScrollView:NSMakeSize(NSWidth(self.frame) + 2, heightHowTo)] placeIn:self x:-1 y:NSHeight(self.frame) - heightHowTo];
scroll.drawsBackground = NO;
scroll.borderType = NSNoBorder;
scroll.verticalScrollElasticity = NSScrollElasticityNone;
@@ -71,7 +71,7 @@ static CGFloat const heightRow = PAD_S + HEIGHT_INPUTFIELD;
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)];
[[tv wrapInScrollView:NSMakeSize(NSWidth(self.frame) + 2, heightOutput)] placeIn:self x:-1 y:0];
return tv;
}

View File

@@ -4,6 +4,7 @@
NS_ASSUME_NONNULL_BEGIN
@interface BarMenu : NSObject <NSMenuDelegate>
@property (assign) BOOL showHidden;
- (instancetype)init NS_UNAVAILABLE;
- (instancetype)initWithStatusItem:(BarStatusItem*)statusItem NS_DESIGNATED_INITIALIZER;
@end

View File

@@ -64,7 +64,7 @@
- (void)setFeedGroups:(NSArray<FeedGroup*>*)sortedList forMenu:(NSMenu*)menu {
[menu insertDefaultHeader];
for (FeedGroup *fg in sortedList) {
[menu insertFeedGroupItem:fg withUnread:self.unreadMap].submenu.delegate = self;
[menu insertFeedGroupItem:fg withUnread:self.unreadMap showHidden:_showHidden].submenu.delegate = self;
}
[menu setHeaderHasUnread:self.unreadMap[menu.titleIndexPath]];
}
@@ -72,17 +72,18 @@
/// Generate items for @c FeedArticles menu.
- (void)setArticles:(NSArray<FeedArticle*>*)sortedList forMenu:(NSMenu*)menu {
[menu insertDefaultHeader];
NSInteger mc = NSIntegerMax;
if (UserPrefsBool(Pref_feedLimitArticles))
mc = UserPrefsInt(Pref_articlesInMenuLimit);
BOOL onlyUnread = UserPrefsBool(Pref_feedUnreadOnly);
for (FeedArticle *fa in sortedList) {
if (onlyUnread && !fa.unread)
continue;
if (--mc < 0) // mc == 0 will first decrement to -1, then evaluate
break;
[menu addItem:[fa newMenuItem]];
NSInteger mc = UserPrefsInt(Pref_articleCountLimit);
if (mc < 0) mc = NSIntegerMax;
if (mc > 0) {
BOOL onlyUnread = UserPrefsBool(Pref_articleUnreadOnly);
for (FeedArticle *fa in sortedList) {
if (onlyUnread && !fa.unread && !_showHidden)
continue;
if (--mc < 0) // mc == 0 will first decrement to -1, then evaluate
break;
[menu addItem:[fa newMenuItem]];
}
}
[menu setHeaderHasUnread:self.unreadMap[menu.titleIndexPath]];
}

View File

@@ -14,6 +14,10 @@
@property (strong) NSStatusItem *statusItem;
@property (assign) NSInteger unreadCountTotal;
@property (weak) NSMenuItem *updateAllItem;
/// Set to `true` if user toggled the `"Show hidden feeds"` menu option.
@property (assign) BOOL optShowHidden;
/// Set to `true` if menu bar was opened while holding down option-key.
@property (assign) BOOL holdingOptKey;
@end
@implementation BarStatusItem
@@ -103,6 +107,9 @@
BOOL hasNet = [UpdateScheduler allowNetworkConnection];
BOOL tint = (self.unreadCountTotal > 0 && hasNet && UserPrefsBool(Pref_globalTintMenuIcon));
self.statusItem.button.image = [NSImage imageNamed:(hasNet ? RSSImageMenuBarIconActive : RSSImageMenuBarIconPaused)];
self.statusItem.button.accessibilityLabel = hasNet
? NSLocalizedString(@"RSS menu bar", nil)
: NSLocalizedString(@"RSS menu bar, paused", nil);
if (@available(macOS 11, *)) {
self.statusItem.button.image.template = !tint;
@@ -150,8 +157,10 @@
#pragma mark - Main Menu Handling
-(void)menuWillOpen:(NSMenu *)menu {
self.holdingOptKey = NSEvent.modifierFlags & NSEventModifierFlagOption;
_mainMenu = menu; // autoreleased once closed
self.barMenu = [[BarMenu alloc] initWithStatusItem:self];
self.barMenu.showHidden = self.optShowHidden || self.holdingOptKey;
[self insertMainMenuHeader:menu];
[self.barMenu menuNeedsUpdate:menu];
@@ -165,14 +174,29 @@
self.barMenu = nil;
self.statusItem.menu = [[NSMenu alloc] initWithTitle:@"M"];
self.statusItem.menu.delegate = self;
self.holdingOptKey = NO;
}
- (void)insertMainMenuHeader:(NSMenu*)menu {
// 'Pause Updates' item
NSMenuItem *pause = [menu addItemWithTitle:NSLocalizedString(@"Pause Updates", nil) action:@selector(pauseUpdates) keyEquivalent:@""];
NSMenuItem *pause = [menu addItemWithTitle:NSLocalizedString(@"Pause updates", nil) action:@selector(pauseUpdates) keyEquivalent:@""];
pause.target = self;
if ([UpdateScheduler isPaused])
pause.title = NSLocalizedString(@"Resume Updates", nil);
pause.title = NSLocalizedString(@"Resume updates", nil);
// 'show hidden feeds' item
if (UserPrefsBool(Pref_globalToggleHidden)) {
NSMenuItem *toggleHidden = [menu addItemWithTitle:NSLocalizedString(@"Show hidden feeds", nil) action:@selector(toggleHiddenArticles) keyEquivalent:@"h"];
toggleHidden.target = self;
toggleHidden.enabled = !self.holdingOptKey && (UserPrefsBool(Pref_groupUnreadOnly) || UserPrefsBool(Pref_feedUnreadOnly) || UserPrefsBool(Pref_articleUnreadOnly));
[toggleHidden setState:self.barMenu.showHidden ? NSControlStateValueOn : NSControlStateValueOff];
if (!toggleHidden.enabled) {
toggleHidden.toolTip = self.holdingOptKey
? NSLocalizedString(@"Option disabled because overwritten by holding down option-key.", nil)
: NSLocalizedString(@"Option disabled because appearance setting for “Show only unread” is disabled.", nil);
}
}
// 'Update all feeds' item
if (UserPrefsBool(Pref_globalUpdateAll)) {
NSMenuItem *updateAll = [menu addItemWithTitle:NSLocalizedString(@"Update all feeds", nil) action:@selector(updateAllFeeds) keyEquivalent:@""];
@@ -190,7 +214,13 @@
[self updateBarIcon];
}
/// Called when user clicks on 'Update all feeds' (main menu only).
/// Called when user clicks on `Show hidden feeds` (main menu only).
- (void)toggleHiddenArticles {
self.optShowHidden = !self.optShowHidden;
self.barMenu.showHidden = self.optShowHidden;
}
/// Called when user clicks on `Update all feeds` (main menu only).
- (void)updateAllFeeds {
// [self asyncReloadUnreadCount]; // should not be necessary
[UpdateScheduler forceUpdateAllFeeds];

View File

@@ -9,7 +9,7 @@ NS_ASSUME_NONNULL_BEGIN
@property (readonly) BOOL isFeedMenu;
// Generator
- (nullable NSMenuItem*)insertFeedGroupItem:(FeedGroup*)fg withUnread:(MapUnreadTotal*)unreadMap;
- (nullable NSMenuItem*)insertFeedGroupItem:(FeedGroup*)fg withUnread:(MapUnreadTotal*)unreadMap showHidden:(BOOL)showHidden;
- (void)insertDefaultHeader;
// Update menu
- (void)setHeaderHasUnread:(UnreadTotal*)count;

View File

@@ -44,7 +44,7 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
#pragma mark - Generator -
/// Create new @c NSMenuItem with empty submenu and append it to the menu. @return Inserted item.
- (nullable NSMenuItem*)insertFeedGroupItem:(FeedGroup*)fg withUnread:(MapUnreadTotal*)unreadMap {
- (nullable NSMenuItem*)insertFeedGroupItem:(FeedGroup*)fg withUnread:(MapUnreadTotal*)unreadMap showHidden:(BOOL)showHidden {
unichar chr = '-';
NSMenuItem *item = nil;
switch (fg.type) {
@@ -57,9 +57,9 @@ 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 || fg.type == GROUP)
&& UserPrefsBool(Pref_groupUnreadOnly)) {
if (unread == 0 && !showHidden
&& ((fg.type == GROUP && UserPrefsBool(Pref_groupUnreadOnly))
|| (fg.type == FEED && UserPrefsBool(Pref_feedUnreadOnly)))) {
item.hidden = YES;
}
@@ -75,8 +75,11 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
self.autoenablesItems = NO;
NSMenuItem *itm = [self addItemIfAllowed:TagOpenAllUnread title:NSLocalizedString(@"Open all unread", nil)];
if (itm) {
NSString *altTitle = [NSString stringWithFormat:NSLocalizedString(@"Open a few unread (%lu)", nil), UserPrefsUInt(Pref_openFewLinksLimit)];
[self addItem:[itm alternateWithTitle:altTitle]];
NSInteger limit = UserPrefsInt(Pref_openFewLinksLimit);
if (limit > 0) {
NSString *altTitle = [NSString stringWithFormat:NSLocalizedString(@"Open a few unread (%ld)", nil), limit];
[self addItem:[itm alternateWithTitle:altTitle]];
}
}
[self addItemIfAllowed:TagMarkAllRead title:NSLocalizedString(@"Mark all read", nil)];
[self addItemIfAllowed:TagMarkAllUnread title:NSLocalizedString(@"Mark all unread", nil)];
@@ -165,7 +168,7 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
BOOL openLinks = NO;
NSUInteger limit = 0;
if (sender.tag == TagOpenAllUnread) {
if (sender.isAlternate)
if (sender.isAlternate) // if reaches this far, limit is guaranteed to be >0
limit = UserPrefsUInt(Pref_openFewLinksLimit);
openLinks = YES;
} else if (sender.tag != TagMarkAllRead && sender.tag != TagMarkAllUnread) {
@@ -212,11 +215,12 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
if (loc != NSNotFound)
self.title = [self.title substringToIndex:loc];
}
if (count > 0 && UserPrefsBool(self.submenu.isFeedMenu ? Pref_feedUnreadCount : Pref_groupUnreadCount)) {
BOOL isFeed = self.submenu.isFeedMenu;
if (count > 0 && UserPrefsBool(isFeed ? Pref_feedUnreadCount : Pref_groupUnreadCount)) {
self.tag = TagTitleCountVisible; // apply new mask
self.title = [self.title stringByAppendingFormat:@" (%ld)", count];
self.onStateImage = [NSImage imageNamed:RSSImageMenuItemUnread];
if (UserPrefsBool(Pref_groupUnreadIndicator))
if (UserPrefsBool(isFeed ? Pref_feedUnreadIndicator : Pref_groupUnreadIndicator))
self.state = NSControlStateValueOn;
}
}