mirror of
https://github.com/Ranchero-Software/NetNewsWire
synced 2025-08-12 06:26:36 +00:00
Merge branch 'ios-ui-settings-localised' into localize_strings
# Conflicts: # Mac/AppDelegate.swift # Mac/Base.lproj/Preferences.storyboard # NetNewsWire.xcodeproj/project.pbxproj # Shared/Localizations/LocalizedNetNewsWireError.swift # iOS/MasterFeed/MasterFeedViewController.swift # iOS/Settings/Appearance/ArticleThemeManagerView.swift # iOS/Settings/Appearance/DisplayAndBehaviorsView.swift # iOS/Settings/General/SettingsView.swift
This commit is contained in:
@@ -18,8 +18,8 @@ import RSDatabase
|
||||
public final class AccountManager: UnreadCountProvider {
|
||||
|
||||
public static var shared: AccountManager!
|
||||
public static let netNewsWireNewsURL = "https://nnw.ranchero.com/feed.xml"
|
||||
private static let jsonNetNewsWireNewsURL = "https://nnw.ranchero.com/feed.json"
|
||||
public static let netNewsWireNewsURL = "https://netnewswire.blog/feed.xml"
|
||||
private static let jsonNetNewsWireNewsURL = "https://netnewswire.blog/feed.json"
|
||||
|
||||
public let defaultAccount: Account
|
||||
|
||||
@@ -246,7 +246,7 @@ public final class AccountManager: UnreadCountProvider {
|
||||
}
|
||||
}
|
||||
|
||||
public func refreshAll(errorHandler: @escaping (Error) -> Void, completion: (() -> Void)? = nil) {
|
||||
public func refreshAll(errorHandler: @escaping @MainActor (Error) -> Void, completion: (() -> Void)? = nil) {
|
||||
guard let reachability = try? Reachability(hostname: "apple.com"), reachability.connection != .unavailable else { return }
|
||||
|
||||
let group = DispatchGroup()
|
||||
@@ -259,7 +259,9 @@ public final class AccountManager: UnreadCountProvider {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
errorHandler(error)
|
||||
Task { @MainActor in
|
||||
errorHandler(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+19
-168
@@ -5,8 +5,26 @@
|
||||
<link>https://ranchero.com/downloads/netnewswire-beta.xml</link>
|
||||
<description>Most recent NetNewsWire changes with links to updates.</description>
|
||||
<language>en</language>
|
||||
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.1.2</title>
|
||||
<description><![CDATA[
|
||||
<p><b>Twitter integration has been removed:</b> Twitter has suspended NetNewsWire, and Twitter is removing free access to the Twitter API</p>
|
||||
<p>Since Twitter does not provide RSS feeds, we’ve had to use the Twitter API. Without free access to that API, we can’t read feeds from Twitter.</p>
|
||||
<p>We’ve left your Twitter feeds intact. If you have any starred items from those feeds, they will remain as long as you don’t delete those feeds.</p>
|
||||
<p>You can still read whatever you have already downloaded. However, those feeds will no longer update.</p>
|
||||
<p>Other changes…</p>
|
||||
<p>Fixed a crashing bug that could happen in the sidebar</p>
|
||||
<p>Fixed a bug that could prevent users from accessing BazQux if an article was missing a field</p>
|
||||
<p>Fixed an issue that could prevent Feedly users from syncing if they tried to mark too many articles as read at the same time</p>
|
||||
<p>Updated the Safari extension icon (credit to Louie Mantia for the new icon)</p>
|
||||
]]></description>
|
||||
<pubDate>Sat, 08 Apr 2023 10:30:00 -0800</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.1.2/NetNewsWire6.1.2.zip" sparkle:version="6114" sparkle:shortVersionString="6.1.2" length="10873540" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.1.1b1</title>
|
||||
<description><![CDATA[
|
||||
<p>Fixed a bug that could prevent users from accessing BazQux if an article was missing a field</p>
|
||||
@@ -17,172 +35,5 @@
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.1</title>
|
||||
<description><![CDATA[
|
||||
<p>Article themes. Several themes ship with the app, and you can create your own. You can change the theme in Preferences or by adding the theme switcher to the toolbar</p>
|
||||
<p>Copy URLs using repaired, rather than raw, feed links</p>
|
||||
<p>Restore article scroll position on relaunching app</p>
|
||||
<p>Added Copy Article URL and Copy External URL commands to the Edit menu</p>
|
||||
<p>Fixed a bug where using cmd-Q wouldn’t always quit the app as quickly as one might prefer</p>
|
||||
<p>Disallow creation of iCloud account in the app if iCloud and iCloud Drive aren’t both enabled</p>
|
||||
<p>Fixed bug showing quote tweets that only included an image</p>
|
||||
<p>Added a hidden pref to suppress downloading/syncing on start: `defaults write com.ranchero.NetNewsWire-Evergreen DevroeSuppressSyncOnLaunch -bool true`</p>
|
||||
<p>Video autoplay is now disallowed</p>
|
||||
<p>Article view now supports RTL layout</p>
|
||||
<p>Fixed a few font and sizing issues</p>
|
||||
<p>Updated built-in feeds</p>
|
||||
<p>Better alignment for items in General Preferences pane</p>
|
||||
]]></description>
|
||||
<pubDate>Thu, 07 Apr 2022 10:05:00 -0700</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.1/NetNewsWire6.1.zip" sparkle:version="6106" sparkle:shortVersionString="6.1" length="10923258" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.1b5</title>
|
||||
<description><![CDATA[
|
||||
<p>Updated built-in feeds</p>
|
||||
<p>Building on a new Apple Silicon Mac — testing to make sure all’s well</p>
|
||||
]]></description>
|
||||
<pubDate>Mon, 04 Apr 2022 22:10:00 -0700</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.1b5/NetNewsWire6.1b5.zip" sparkle:version="6104" sparkle:shortVersionString="6.1b5" length="10922681" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.1b4</title>
|
||||
<description><![CDATA[
|
||||
<p>Fixed a few font and sizing issues.</p>
|
||||
]]></description>
|
||||
<pubDate>Sun, 27 Feb 2022 21:50:00 -0800</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.1b4/NetNewsWire6.1b4.zip" sparkle:version="6103" sparkle:shortVersionString="6.1b4" length="10897726" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.1b3</title>
|
||||
<description><![CDATA[
|
||||
<p>Two new themes: Hyperlegible and NewsFax</p>
|
||||
<p>Change in how built-in themes work: they’re part of the app bundle and they’re not copied into the Themes folder. When a built-in theme changes in a new app release, anyone using that new version gets the changes to the built-in theme</p>
|
||||
]]></description>
|
||||
<pubDate>Thu, 10 Feb 2022 21:35:00 -0800</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.1b3/NetNewsWire6.1b3.zip" sparkle:version="6102" sparkle:shortVersionString="6.1b3" length="10893987" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.1b2</title>
|
||||
<description><![CDATA[
|
||||
<p>Article themes. Several themes ship with the app, and you can create your own. You can change the theme in Preferences or by adding the theme switcher to the toolbar.</p>
|
||||
<p>Copy URLs using repaired, rather than raw, feed links.</p>
|
||||
<p>Restore article scroll position on relaunching app.</p>
|
||||
<p>Added Copy Article URL and Copy External URL commands to the Edit menu.</p>
|
||||
<p>Fixed a bug where using cmd-Q wouldn’t always quit the app as quickly as one might prefer.</p>
|
||||
<p>Disallow creation of iCloud account in the app if iCloud and iCloud Drive aren’t both enabled.</p>
|
||||
<p>Fixed bug showing quote tweets that only included an image.</p>
|
||||
<p>Added a hidden pref to suppress downloading/syncing on start: `defaults write com.ranchero.NetNewsWire-Evergreen DevroeSuppressSyncOnLaunch -bool true`</p>
|
||||
<p>Video autoplay is now disallowed.</p>
|
||||
<p>Article view now supports RTL layout.</p>
|
||||
]]></description>
|
||||
<pubDate>Mon, 17 Jan 2022 17:45:00 -0800</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.1b2/NetNewsWire6.1b2.zip" sparkle:version="6101" sparkle:shortVersionString="6.1b2" length="10841318" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.0.3</title>
|
||||
<description><![CDATA[
|
||||
<p>Same as 6.0.3b2 except for the version number.</p>
|
||||
]]></description>
|
||||
<pubDate>Sun, 05 Sep 2021 12:20:00 -0700</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.0.3/NetNewsWire6.0.3.zip" sparkle:version="6035" sparkle:shortVersionString="6.0.3" length="10477066" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.0.3b2</title>
|
||||
<description><![CDATA[
|
||||
<p>Feedly: preserve custom feed names with Feedly when moving them between folders</p>
|
||||
<p>Preferences: use full-width row style in accounts and extensions panes</p>
|
||||
<p>Fixed a crashing bug triggered by running some UI code outside of main thread</p>
|
||||
<p>Fixed a crashing bug that could happen when the app tries to find a feed for a website</p>
|
||||
<p>Fixed a crashing bug that could happen when rendering tweets</p>
|
||||
<p>Changed how images are placed in Twitter articles so that you can better see who Tweeted the image</p>
|
||||
<p>Fixed bug where iCloud syncing could stop prematurely when the sync database has records not in the local database</p>
|
||||
<p>Fixed bug where favicons wouldn’t be found when a home page URL has non-ASCII characters</p>
|
||||
<p>Fixed bug where external URLs in Feedbin feeds might be lost</p>
|
||||
<p>Fixed bug where words prepended with $ wouldn’t appear in Twitter feeds</p>
|
||||
<p>Fixed bug where newlines would be just a space in Twitter feeds</p>
|
||||
<p>Fixed bug where BazQux-synced feeds might stop updating</p>
|
||||
]]></description>
|
||||
<pubDate>Sun, 29 Aug 2021 15:25:00 -0700</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.0.3b2/NetNewsWire6.0.3b2.zip" sparkle:version="6034" sparkle:shortVersionString="6.0.3b2" length="10477006" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.0.3b1</title>
|
||||
<description><![CDATA[
|
||||
<p>Feedly: handle API change with deleting and don’t show a spurious error</p>
|
||||
<p>NewsBlur: don’t fetch articles marked hidden by NewsBlur</p>
|
||||
<p>FreshRSS: add API endpoint URL example in setup form</p>
|
||||
<p>iCloud: fixed bug not retaining feeds in a folder where the folder hasn’t been synced yet</p>
|
||||
<p>Feeds list: smart feeds remain visible despite Hide Read Feeds setting</p>
|
||||
<p>Keyboard shortcuts: fixed regression where L key wouldn’t go to next unread when feed is all read</p>
|
||||
<p>Twitter extension: fixed weird bug where an extra https:/ could appear in tweet text</p>
|
||||
]]></description>
|
||||
<pubDate>Thu, 20 May 2021 20:00:00 -0700</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.0.3b1/NetNewsWire6.0.3b1.zip" sparkle:version="6033" sparkle:shortVersionString="6.0.3b1" length="10463681" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.0.2</title>
|
||||
<description><![CDATA[
|
||||
<p>Same as 6.0.2b1 — no changes other than version</p>
|
||||
]]></description>
|
||||
<pubDate>Tue, 20 Apr 2021 17:40:00 -0700</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.0.2/NetNewsWire6.0.2.zip" sparkle:version="6032" sparkle:shortVersionString="6.0.2" length="10453603" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.0.2b1</title>
|
||||
<description><![CDATA[
|
||||
<p>Inoreader sync: fixed (hopefully) cause of rate limit errors — now doing background sync of statuses much less often - note that this fix needs to be rolled out across all NetNewsWire users in order for it to have full effect</p>
|
||||
<p>Fixed regression with the L key — now works properly again</p>
|
||||
]]></description>
|
||||
<pubDate>Thu, 15 Apr 2021 19:15:00 -0700</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.0.2b1/NetNewsWire6.0.2b1.zip" sparkle:version="6031" sparkle:shortVersionString="6.0.2b1" length="10453628" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.0.1</title>
|
||||
<description><![CDATA[
|
||||
<p>Adjusted layout of the add account sheet so that it fits on smaller monitors</p>
|
||||
<p>Sidebar: properly scale the smart feed icons when sidebar is set to large size in System Preferences </p>
|
||||
]]></description>
|
||||
<pubDate>Thu, 01 Apr 2021 20:22:00 -0700</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.0.1/NetNewsWire6.0.1.zip" sparkle:version="6030" sparkle:shortVersionString="6.0.1" length="10448727" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.0.1b2</title>
|
||||
<description><![CDATA[
|
||||
<p>Twitter: fixed a date parsing bug that could affect people in some locales, which would prevent Twitter feeds from working for them</p>
|
||||
<p>Feeds list: fixed bug where newly added feed would be called Untitled past the time when the app actually knows its name</p>
|
||||
<p>Fixed bug where next-unread command wouldn’t wrap around when you got to the bottom of the Feeds list</p>
|
||||
]]></description>
|
||||
<pubDate>Mon, 29 Mar 2021 20:55:00 -0700</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.0.1b2/NetNewsWire6.0.1b2.zip" sparkle:version="6029" sparkle:shortVersionString="6.0.1b2" length="10445067" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
|
||||
</channel>
|
||||
</rss>
|
||||
|
||||
@@ -6,6 +6,24 @@
|
||||
<description>Most recent NetNewsWire releases (not test builds).</description>
|
||||
<language>en</language>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.1.2</title>
|
||||
<description><![CDATA[
|
||||
<p><b>Twitter integration has been removed:</b> Twitter has suspended NetNewsWire, and Twitter is removing free access to the Twitter API</p>
|
||||
<p>Since Twitter does not provide RSS feeds, we’ve had to use the Twitter API. Without free access to that API, we can’t read feeds from Twitter.</p>
|
||||
<p>We’ve left your Twitter feeds intact. If you have any starred items from those feeds, they will remain as long as you don’t delete those feeds.</p>
|
||||
<p>You can still read whatever you have already downloaded. However, those feeds will no longer update.</p>
|
||||
<p>Other changes…</p>
|
||||
<p>Fixed a crashing bug that could happen in the sidebar</p>
|
||||
<p>Fixed a bug that could prevent users from accessing BazQux if an article was missing a field</p>
|
||||
<p>Fixed an issue that could prevent Feedly users from syncing if they tried to mark too many articles as read at the same time</p>
|
||||
<p>Updated the Safari extension icon (credit to Louie Mantia for the new icon)</p>
|
||||
]]></description>
|
||||
<pubDate>Sat, 08 Apr 2023 10:30:00 -0800</pubDate>
|
||||
<enclosure url="https://github.com/Ranchero-Software/NetNewsWire/releases/download/mac-6.1.2/NetNewsWire6.1.2.zip" sparkle:version="6114" sparkle:shortVersionString="6.1.2" length="10873540" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.15.0</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>NetNewsWire 6.1</title>
|
||||
<description><![CDATA[
|
||||
|
||||
@@ -11,7 +11,7 @@ import Foundation
|
||||
// Threading rules:
|
||||
// * Main-thread only
|
||||
// * Except: may be created on background thread by StatusesTable.
|
||||
// Which is safe, because at creation time it’t not yet shared,
|
||||
// Which is safe, because at creation time it’s not yet shared,
|
||||
// and it won’t be mutated ever on a background thread.
|
||||
|
||||
public final class ArticleStatus: Hashable {
|
||||
|
||||
+31
-22
@@ -15,9 +15,9 @@ enum FontSize: Int {
|
||||
}
|
||||
|
||||
final class AppDefaults {
|
||||
|
||||
|
||||
static let defaultThemeName = "Default"
|
||||
|
||||
|
||||
static var shared = AppDefaults()
|
||||
private init() {}
|
||||
|
||||
@@ -44,6 +44,7 @@ final class AppDefaults {
|
||||
static let currentThemeName = "currentThemeName"
|
||||
static let hasSeenNotAllArticlesHaveURLsAlert = "hasSeenNotAllArticlesHaveURLsAlert"
|
||||
static let twitterDeprecationAlertShown = "twitterDeprecationAlertShown"
|
||||
static let markArticlesAsReadOnScroll = "markArticlesAsReadOnScroll"
|
||||
|
||||
// Hidden prefs
|
||||
static let showDebugMenu = "ShowDebugMenu"
|
||||
@@ -67,7 +68,7 @@ final class AppDefaults {
|
||||
}
|
||||
return false
|
||||
}()
|
||||
|
||||
|
||||
var isFirstRun: Bool = {
|
||||
if let _ = UserDefaults.standard.object(forKey: Key.firstRunDate) as? Date {
|
||||
return false
|
||||
@@ -75,7 +76,7 @@ final class AppDefaults {
|
||||
firstRunDate = Date()
|
||||
return true
|
||||
}()
|
||||
|
||||
|
||||
var windowState: [AnyHashable : Any]? {
|
||||
get {
|
||||
return UserDefaults.standard.object(forKey: Key.windowState) as? [AnyHashable : Any]
|
||||
@@ -84,7 +85,7 @@ final class AppDefaults {
|
||||
UserDefaults.standard.set(newValue, forKey: Key.windowState)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var activeExtensionPointIDs: [[AnyHashable : AnyHashable]]? {
|
||||
get {
|
||||
return UserDefaults.standard.object(forKey: Key.activeExtensionPointIDs) as? [[AnyHashable : AnyHashable]]
|
||||
@@ -93,7 +94,7 @@ final class AppDefaults {
|
||||
UserDefaults.standard.set(newValue, forKey: Key.activeExtensionPointIDs)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var lastImageCacheFlushDate: Date? {
|
||||
get {
|
||||
return AppDefaults.date(for: Key.lastImageCacheFlushDate)
|
||||
@@ -102,7 +103,7 @@ final class AppDefaults {
|
||||
AppDefaults.setDate(for: Key.lastImageCacheFlushDate, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var openInBrowserInBackground: Bool {
|
||||
get {
|
||||
return AppDefaults.bool(for: Key.openInBrowserInBackground)
|
||||
@@ -168,7 +169,7 @@ final class AppDefaults {
|
||||
AppDefaults.setString(for: Key.addWebFeedAccountID, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var addWebFeedFolderName: String? {
|
||||
get {
|
||||
return AppDefaults.string(for: Key.addWebFeedFolderName)
|
||||
@@ -186,7 +187,7 @@ final class AppDefaults {
|
||||
AppDefaults.setString(for: Key.addFolderAccountID, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var importOPMLAccountID: String? {
|
||||
get {
|
||||
return AppDefaults.string(for: Key.importOPMLAccountID)
|
||||
@@ -195,7 +196,7 @@ final class AppDefaults {
|
||||
AppDefaults.setString(for: Key.importOPMLAccountID, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var exportOPMLAccountID: String? {
|
||||
get {
|
||||
return AppDefaults.string(for: Key.exportOPMLAccountID)
|
||||
@@ -213,7 +214,7 @@ final class AppDefaults {
|
||||
AppDefaults.setString(for: Key.defaultBrowserID, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var currentThemeName: String? {
|
||||
get {
|
||||
return AppDefaults.string(for: Key.currentThemeName)
|
||||
@@ -231,7 +232,7 @@ final class AppDefaults {
|
||||
UserDefaults.standard.set(newValue, forKey: Key.hasSeenNotAllArticlesHaveURLsAlert)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var showTitleOnMainWindow: Bool {
|
||||
return AppDefaults.bool(for: Key.showTitleOnMainWindow)
|
||||
}
|
||||
@@ -286,7 +287,7 @@ final class AppDefaults {
|
||||
AppDefaults.setSortDirection(for: Key.timelineSortDirection, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var timelineGroupByFeed: Bool {
|
||||
get {
|
||||
return AppDefaults.bool(for: Key.timelineGroupByFeed)
|
||||
@@ -295,7 +296,7 @@ final class AppDefaults {
|
||||
AppDefaults.setBool(for: Key.timelineGroupByFeed, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var timelineShowsSeparators: Bool {
|
||||
return AppDefaults.bool(for: Key.timelineShowsSeparators)
|
||||
}
|
||||
@@ -319,7 +320,7 @@ final class AppDefaults {
|
||||
UserDefaults.standard.set(newValue.rawValue, forKey: Key.refreshInterval)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var twitterDeprecationAlertShown: Bool {
|
||||
get {
|
||||
return AppDefaults.bool(for: Key.twitterDeprecationAlertShown)
|
||||
@@ -328,7 +329,15 @@ final class AppDefaults {
|
||||
AppDefaults.setBool(for: Key.twitterDeprecationAlertShown, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var markArticlesAsReadOnScroll: Bool {
|
||||
get {
|
||||
return AppDefaults.bool(for: Key.markArticlesAsReadOnScroll)
|
||||
}
|
||||
set {
|
||||
AppDefaults.setBool(for: Key.markArticlesAsReadOnScroll, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
func registerDefaults() {
|
||||
#if DEBUG
|
||||
@@ -401,19 +410,19 @@ private extension AppDefaults {
|
||||
// }
|
||||
// return FontSize(rawValue: rawFontSize)!
|
||||
}
|
||||
|
||||
|
||||
static func setFontSize(for key: String, _ fontSize: FontSize) {
|
||||
setInt(for: key, fontSize.rawValue)
|
||||
}
|
||||
|
||||
|
||||
static func string(for key: String) -> String? {
|
||||
return UserDefaults.standard.string(forKey: key)
|
||||
}
|
||||
|
||||
|
||||
static func setString(for key: String, _ value: String?) {
|
||||
UserDefaults.standard.set(value, forKey: key)
|
||||
}
|
||||
|
||||
|
||||
static func bool(for key: String) -> Bool {
|
||||
return UserDefaults.standard.bool(forKey: key)
|
||||
}
|
||||
@@ -425,11 +434,11 @@ private extension AppDefaults {
|
||||
static func int(for key: String) -> Int {
|
||||
return UserDefaults.standard.integer(forKey: key)
|
||||
}
|
||||
|
||||
|
||||
static func setInt(for key: String, _ x: Int) {
|
||||
UserDefaults.standard.set(x, forKey: key)
|
||||
}
|
||||
|
||||
|
||||
static func date(for key: String) -> Date? {
|
||||
return UserDefaults.standard.object(forKey: key) as? Date
|
||||
}
|
||||
|
||||
+187
-188
@@ -32,11 +32,11 @@ var appDelegate: AppDelegate!
|
||||
@NSApplicationMain
|
||||
class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate, Logging
|
||||
{
|
||||
|
||||
|
||||
private struct WindowRestorationIdentifiers {
|
||||
static let mainWindow = "mainWindow"
|
||||
}
|
||||
|
||||
|
||||
var userNotificationManager: UserNotificationManager!
|
||||
var faviconDownloader: FaviconDownloader!
|
||||
var imageDownloader: ImageDownloader!
|
||||
@@ -44,13 +44,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
var webFeedIconDownloader: WebFeedIconDownloader!
|
||||
var extensionContainersFile: ExtensionContainersFile!
|
||||
var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile!
|
||||
|
||||
|
||||
var appName: String!
|
||||
|
||||
|
||||
var refreshTimer: AccountRefreshTimer?
|
||||
var syncTimer: ArticleStatusSyncTimer?
|
||||
var lastRefreshInterval = AppDefaults.shared.refreshInterval
|
||||
|
||||
|
||||
var shuttingDown = false {
|
||||
didSet {
|
||||
if shuttingDown {
|
||||
@@ -61,9 +61,9 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isShutDownSyncDone = false
|
||||
|
||||
|
||||
@IBOutlet var shareMenuItem: NSMenuItem!
|
||||
@IBOutlet var fileMenuItem: NSMenuItem!
|
||||
@IBOutlet var debugMenuItem: NSMenuItem!
|
||||
@@ -71,7 +71,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
@IBOutlet var sortByNewestArticleOnTopMenuItem: NSMenuItem!
|
||||
@IBOutlet var groupArticlesByFeedMenuItem: NSMenuItem!
|
||||
@IBOutlet var checkForUpdatesMenuItem: NSMenuItem!
|
||||
|
||||
|
||||
var unreadCount = 0 {
|
||||
didSet {
|
||||
if unreadCount != oldValue {
|
||||
@@ -80,7 +80,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private var mainWindowController: MainWindowController? {
|
||||
var bestController: MainWindowController?
|
||||
for candidateController in mainWindowControllers {
|
||||
@@ -94,7 +94,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
}
|
||||
return bestController
|
||||
}
|
||||
|
||||
|
||||
private var mainWindowControllers = [MainWindowController]()
|
||||
private var preferencesWindowController: NSWindowController?
|
||||
private var addFeedController: AddFeedController?
|
||||
@@ -109,50 +109,50 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
private var softwareUpdater: SPUUpdater!
|
||||
private var crashReporter: PLCrashReporter!
|
||||
#endif
|
||||
|
||||
override init() {
|
||||
|
||||
@MainActor override init() {
|
||||
NSWindow.allowsAutomaticWindowTabbing = false
|
||||
super.init()
|
||||
|
||||
|
||||
#if !MAC_APP_STORE
|
||||
let crashReporterConfig = PLCrashReporterConfig.defaultConfiguration()
|
||||
crashReporter = PLCrashReporter(configuration: crashReporterConfig)
|
||||
crashReporter.enable()
|
||||
#endif
|
||||
|
||||
|
||||
SecretsManager.provider = Secrets()
|
||||
AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!)
|
||||
ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!)
|
||||
FeedProviderManager.shared.delegate = ExtensionPointManager.shared
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil)
|
||||
NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil)
|
||||
|
||||
|
||||
appDelegate = self
|
||||
|
||||
|
||||
presentTwitterDeprecationAlertIfRequired()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - API
|
||||
func showAddFolderSheetOnWindow(_ window: NSWindow) {
|
||||
@MainActor func showAddFolderSheetOnWindow(_ window: NSWindow) {
|
||||
addFolderWindowController = AddFolderWindowController()
|
||||
addFolderWindowController!.runSheetOnWindow(window)
|
||||
}
|
||||
|
||||
func showAddWebFeedSheetOnWindow(_ window: NSWindow, urlString: String?, name: String?, account: Account?, folder: Folder?) {
|
||||
|
||||
@MainActor func showAddWebFeedSheetOnWindow(_ window: NSWindow, urlString: String?, name: String?, account: Account?, folder: Folder?) {
|
||||
addFeedController = AddFeedController(hostWindow: window)
|
||||
addFeedController?.showAddFeedSheet(.webFeed, urlString, name, account, folder)
|
||||
}
|
||||
|
||||
|
||||
// MARK: - NSApplicationDelegate
|
||||
|
||||
func applicationWillFinishLaunching(_ notification: Notification) {
|
||||
|
||||
@MainActor func applicationWillFinishLaunching(_ notification: Notification) {
|
||||
installAppleEventHandlers()
|
||||
|
||||
|
||||
CacheCleaner.purgeIfNecessary()
|
||||
|
||||
|
||||
// Try to establish a cache in the Caches folder, but if it fails for some reason fall back to a temporary dir
|
||||
let cacheFolder: String
|
||||
if let userCacheFolder = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path {
|
||||
@@ -162,25 +162,25 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String)
|
||||
cacheFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier)
|
||||
}
|
||||
|
||||
|
||||
let faviconsFolder = (cacheFolder as NSString).appendingPathComponent("Favicons")
|
||||
let faviconsFolderURL = URL(fileURLWithPath: faviconsFolder)
|
||||
try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil)
|
||||
faviconDownloader = FaviconDownloader(folder: faviconsFolder)
|
||||
|
||||
|
||||
let imagesFolder = (cacheFolder as NSString).appendingPathComponent("Images")
|
||||
let imagesFolderURL = URL(fileURLWithPath: imagesFolder)
|
||||
try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil)
|
||||
imageDownloader = ImageDownloader(folder: imagesFolder)
|
||||
|
||||
|
||||
authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader)
|
||||
webFeedIconDownloader = WebFeedIconDownloader(imageDownloader: imageDownloader, folder: cacheFolder)
|
||||
|
||||
|
||||
appName = (Bundle.main.infoDictionary!["CFBundleExecutable"]! as! String)
|
||||
}
|
||||
|
||||
func applicationDidFinishLaunching(_ note: Notification) {
|
||||
|
||||
|
||||
@MainActor func applicationDidFinishLaunching(_ note: Notification) {
|
||||
|
||||
#if MAC_APP_STORE || TEST
|
||||
checkForUpdatesMenuItem.isHidden = true
|
||||
#else
|
||||
@@ -188,7 +188,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
let hostBundle = Bundle.main
|
||||
let updateDriver = SPUStandardUserDriver(hostBundle: hostBundle, delegate: self)
|
||||
self.softwareUpdater = SPUUpdater(hostBundle: hostBundle, applicationBundle: hostBundle, userDriver: updateDriver, delegate: self)
|
||||
|
||||
|
||||
do {
|
||||
try self.softwareUpdater.start()
|
||||
}
|
||||
@@ -196,55 +196,55 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
logger.error("Failed to start software updater with error: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
AppDefaults.shared.registerDefaults()
|
||||
let isFirstRun = AppDefaults.shared.isFirstRun
|
||||
if isFirstRun {
|
||||
logger.debug("Is first run")
|
||||
}
|
||||
let localAccount = AccountManager.shared.defaultAccount
|
||||
|
||||
|
||||
if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() {
|
||||
// Import feeds. Either old NNW 3 feeds or the default feeds.
|
||||
if !NNW3ImportController.importSubscriptionsIfFileExists(account: localAccount) {
|
||||
DefaultFeedsImporter.importDefaultFeeds(account: localAccount)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateSortMenuItems()
|
||||
updateGroupByFeedMenuItem()
|
||||
|
||||
|
||||
if mainWindowController == nil {
|
||||
let mainWindowController = createAndShowMainWindow()
|
||||
mainWindowController.restoreStateFromUserDefaults()
|
||||
}
|
||||
|
||||
|
||||
fileMenuItem.submenu?.delegate = self
|
||||
shareMenuItem.submenu?.delegate = self
|
||||
|
||||
|
||||
if isFirstRun {
|
||||
mainWindowController?.window?.center()
|
||||
}
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
self.unreadCount = AccountManager.shared.unreadCount
|
||||
}
|
||||
|
||||
|
||||
if InspectorWindowController.shouldOpenAtStartup {
|
||||
self.toggleInspectorWindow(self)
|
||||
}
|
||||
|
||||
|
||||
extensionContainersFile = ExtensionContainersFile()
|
||||
extensionFeedAddRequestFile = ExtensionFeedAddRequestFile()
|
||||
|
||||
|
||||
refreshTimer = AccountRefreshTimer()
|
||||
syncTimer = ArticleStatusSyncTimer()
|
||||
|
||||
|
||||
UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .alert, .sound]) { (granted, error) in }
|
||||
|
||||
|
||||
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
|
||||
if settings.authorizationStatus == .authorized {
|
||||
DispatchQueue.main.async {
|
||||
@@ -252,10 +252,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
userNotificationManager = UserNotificationManager()
|
||||
|
||||
|
||||
#if DEBUG
|
||||
refreshTimer!.update()
|
||||
syncTimer!.update()
|
||||
@@ -270,7 +270,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
if AppDefaults.shared.showDebugMenu {
|
||||
// The Web Inspector uses SPI and can never appear in a MAC_APP_STORE build.
|
||||
#if MAC_APP_STORE
|
||||
@@ -283,24 +283,24 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
} else {
|
||||
debugMenuItem.menu?.removeItem(debugMenuItem)
|
||||
}
|
||||
|
||||
|
||||
#if !MAC_APP_STORE
|
||||
DispatchQueue.main.async {
|
||||
CrashReporter.check(crashReporter: self.crashReporter)
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
}
|
||||
|
||||
func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool {
|
||||
|
||||
@MainActor func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool {
|
||||
guard let mainWindowController = mainWindowController else {
|
||||
return false
|
||||
}
|
||||
mainWindowController.handle(userActivity)
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
||||
|
||||
@MainActor func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
|
||||
// https://github.com/brentsimmons/NetNewsWire/issues/522
|
||||
// I couldn’t reproduce the crashing bug, but it appears to happen on creating a main window
|
||||
// and its views and view controllers. The check below is so that the app does nothing
|
||||
@@ -313,43 +313,43 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
mainWindowController.showWindow(self)
|
||||
return false
|
||||
}
|
||||
|
||||
func applicationDidBecomeActive(_ notification: Notification) {
|
||||
|
||||
@MainActor func applicationDidBecomeActive(_ notification: Notification) {
|
||||
fireOldTimers()
|
||||
}
|
||||
|
||||
func applicationDidResignActive(_ notification: Notification) {
|
||||
|
||||
@MainActor func applicationDidResignActive(_ notification: Notification) {
|
||||
ArticleStringFormatter.emptyCaches()
|
||||
saveState()
|
||||
}
|
||||
|
||||
func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) {
|
||||
|
||||
@MainActor func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) {
|
||||
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo)
|
||||
}
|
||||
|
||||
func application(_ sender: NSApplication, openFile filename: String) -> Bool {
|
||||
|
||||
@MainActor func application(_ sender: NSApplication, openFile filename: String) -> Bool {
|
||||
guard filename.hasSuffix(ArticleTheme.nnwThemeSuffix) else { return false }
|
||||
importTheme(filename: filename)
|
||||
return true
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ notification: Notification) {
|
||||
|
||||
@MainActor func applicationWillTerminate(_ notification: Notification) {
|
||||
shuttingDown = true
|
||||
saveState()
|
||||
|
||||
|
||||
ArticleThemeDownloader.shared.cleanUp()
|
||||
|
||||
|
||||
AccountManager.shared.sendArticleStatusAll() {
|
||||
self.isShutDownSyncDone = true
|
||||
}
|
||||
|
||||
|
||||
let timeout = Date().addingTimeInterval(2)
|
||||
while !isShutDownSyncDone && RunLoop.current.run(mode: .default, before: timeout) && timeout > Date() { }
|
||||
}
|
||||
|
||||
func presentThemeImportError(_ error: Error) {
|
||||
|
||||
@MainActor func presentThemeImportError(_ error: Error) {
|
||||
var informativeText: String = ""
|
||||
|
||||
|
||||
if let decodingError = error as? DecodingError {
|
||||
switch decodingError {
|
||||
case .typeMismatch(let type, _):
|
||||
@@ -369,14 +369,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
}
|
||||
let localizedError = NSLocalizedString("alert.error.theme-data-corruption.%@", comment: "This theme cannot be used because of data corruption in the Info.plist: %@.")
|
||||
informativeText = NSString.localizedStringWithFormat(localizedError as NSString, debugDescription) as String
|
||||
|
||||
|
||||
default:
|
||||
informativeText = error.localizedDescription
|
||||
}
|
||||
} else {
|
||||
informativeText = error.localizedDescription
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .warning
|
||||
@@ -385,20 +385,20 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
alert.addButton(withTitle: NSLocalizedString("button.title.ok", comment: "OK"))
|
||||
|
||||
alert.buttons[0].keyEquivalent = "\r"
|
||||
|
||||
let response = alert.runModal()
|
||||
|
||||
_ = alert.runModal()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc func unreadCountDidChange(_ note: Notification) {
|
||||
|
||||
@MainActor @objc func unreadCountDidChange(_ note: Notification) {
|
||||
if note.object is AccountManager {
|
||||
unreadCount = AccountManager.shared.unreadCount
|
||||
}
|
||||
}
|
||||
|
||||
@objc func webFeedSettingDidChange(_ note: Notification) {
|
||||
|
||||
@MainActor @objc func webFeedSettingDidChange(_ note: Notification) {
|
||||
guard let feed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else {
|
||||
return
|
||||
}
|
||||
@@ -406,31 +406,31 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
let _ = faviconDownloader.favicon(for: feed)
|
||||
}
|
||||
}
|
||||
|
||||
@objc func inspectableObjectsDidChange(_ note: Notification) {
|
||||
|
||||
@MainActor @objc func inspectableObjectsDidChange(_ note: Notification) {
|
||||
guard let inspectorWindowController = inspectorWindowController, inspectorWindowController.isOpen else {
|
||||
return
|
||||
}
|
||||
inspectorWindowController.objects = objectsForInspector()
|
||||
}
|
||||
|
||||
@objc func userDefaultsDidChange(_ note: Notification) {
|
||||
|
||||
@MainActor @objc func userDefaultsDidChange(_ note: Notification) {
|
||||
updateSortMenuItems()
|
||||
updateGroupByFeedMenuItem()
|
||||
|
||||
|
||||
if lastRefreshInterval != AppDefaults.shared.refreshInterval {
|
||||
refreshTimer?.update()
|
||||
lastRefreshInterval = AppDefaults.shared.refreshInterval
|
||||
}
|
||||
|
||||
|
||||
updateDockBadge()
|
||||
}
|
||||
|
||||
@objc func didWakeNotification(_ note: Notification) {
|
||||
|
||||
@MainActor @objc func didWakeNotification(_ note: Notification) {
|
||||
fireOldTimers()
|
||||
}
|
||||
|
||||
@objc func importDownloadedTheme(_ note: Notification) {
|
||||
|
||||
@MainActor @objc func importDownloadedTheme(_ note: Notification) {
|
||||
guard let userInfo = note.userInfo,
|
||||
let url = userInfo["url"] as? URL else {
|
||||
return
|
||||
@@ -439,108 +439,108 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
self.importTheme(filename: url.path)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Main Window
|
||||
|
||||
func createMainWindowController() -> MainWindowController {
|
||||
|
||||
@MainActor func createMainWindowController() -> MainWindowController {
|
||||
let controller: MainWindowController
|
||||
controller = windowControllerWithName("MainWindow") as! MainWindowController
|
||||
|
||||
|
||||
if !(mainWindowController?.isOpen ?? false) {
|
||||
mainWindowControllers.removeAll()
|
||||
}
|
||||
mainWindowControllers.append(controller)
|
||||
return controller
|
||||
}
|
||||
|
||||
func windowControllerWithName(_ storyboardName: String) -> NSWindowController {
|
||||
|
||||
@MainActor func windowControllerWithName(_ storyboardName: String) -> NSWindowController {
|
||||
let storyboard = NSStoryboard(name: NSStoryboard.Name(storyboardName), bundle: nil)
|
||||
return storyboard.instantiateInitialController()! as! NSWindowController
|
||||
}
|
||||
|
||||
|
||||
@discardableResult
|
||||
func createAndShowMainWindow() -> MainWindowController {
|
||||
@MainActor func createAndShowMainWindow() -> MainWindowController {
|
||||
let controller = createMainWindowController()
|
||||
controller.showWindow(self)
|
||||
|
||||
|
||||
if let window = controller.window {
|
||||
window.restorationClass = Self.self
|
||||
window.identifier = NSUserInterfaceItemIdentifier(rawValue: WindowRestorationIdentifiers.mainWindow)
|
||||
}
|
||||
|
||||
|
||||
return controller
|
||||
}
|
||||
|
||||
func createAndShowMainWindowIfNecessary() {
|
||||
|
||||
@MainActor func createAndShowMainWindowIfNecessary() {
|
||||
if mainWindowController == nil {
|
||||
createAndShowMainWindow()
|
||||
} else {
|
||||
mainWindowController?.showWindow(self)
|
||||
}
|
||||
}
|
||||
|
||||
func removeMainWindow(_ windowController: MainWindowController) {
|
||||
|
||||
@MainActor func removeMainWindow(_ windowController: MainWindowController) {
|
||||
guard mainWindowControllers.count > 1 else { return }
|
||||
if let index = mainWindowControllers.firstIndex(of: windowController) {
|
||||
mainWindowControllers.remove(at: index)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: NSUserInterfaceValidations
|
||||
func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
||||
@MainActor func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
|
||||
if shuttingDown {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
let isDisplayingSheet = mainWindowController?.isDisplayingSheet ?? false
|
||||
let isSpecialAccountAvailable = AccountManager.shared.activeAccounts.contains(where: { $0.type == .onMyMac || $0.type == .cloudKit })
|
||||
|
||||
|
||||
if item.action == #selector(refreshAll(_:)) {
|
||||
return !AccountManager.shared.refreshInProgress && !AccountManager.shared.activeAccounts.isEmpty
|
||||
}
|
||||
|
||||
|
||||
if item.action == #selector(importOPMLFromFile(_:)) {
|
||||
return AccountManager.shared.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) })
|
||||
}
|
||||
|
||||
|
||||
if item.action == #selector(addAppNews(_:)) {
|
||||
return !isDisplayingSheet && !AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() && !AccountManager.shared.activeAccounts.isEmpty
|
||||
}
|
||||
|
||||
|
||||
if item.action == #selector(sortByNewestArticleOnTop(_:)) || item.action == #selector(sortByOldestArticleOnTop(_:)) {
|
||||
return mainWindowController?.isOpen ?? false
|
||||
}
|
||||
|
||||
|
||||
if item.action == #selector(showAddWebFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) {
|
||||
return !isDisplayingSheet && !AccountManager.shared.activeAccounts.isEmpty
|
||||
}
|
||||
|
||||
|
||||
if item.action == #selector(showAddRedditFeedWindow(_:)) {
|
||||
guard !isDisplayingSheet && isSpecialAccountAvailable && ExtensionPointManager.shared.isRedditEnabled else {
|
||||
return false
|
||||
}
|
||||
return ExtensionPointManager.shared.isRedditEnabled
|
||||
}
|
||||
|
||||
|
||||
#if !MAC_APP_STORE
|
||||
if item.action == #selector(toggleWebInspectorEnabled(_:)) {
|
||||
(item as! NSMenuItem).state = AppDefaults.shared.webInspectorEnabled ? .on : .off
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// MARK: UNUserNotificationCenterDelegate
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
|
||||
@MainActor func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
completionHandler([.banner, .badge, .sound])
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
|
||||
|
||||
@MainActor func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
|
||||
|
||||
switch response.actionIdentifier {
|
||||
case "MARK_AS_READ":
|
||||
handleMarkAsRead(userInfo: userInfo)
|
||||
@@ -551,11 +551,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
}
|
||||
completionHandler()
|
||||
}
|
||||
|
||||
|
||||
// MARK: Add Feed
|
||||
func addWebFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) {
|
||||
@MainActor func addWebFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) {
|
||||
createAndShowMainWindowIfNecessary()
|
||||
|
||||
|
||||
if mainWindowController!.isDisplayingSheet {
|
||||
return
|
||||
}
|
||||
@@ -564,13 +564,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
}
|
||||
|
||||
// MARK: - Dock Badge
|
||||
@objc func updateDockBadge() {
|
||||
@MainActor @objc func updateDockBadge() {
|
||||
let label = unreadCount > 0 ? "\(unreadCount)" : ""
|
||||
NSApplication.shared.dockTile.badgeLabel = label
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
@IBAction func showPreferences(_ sender: Any?) {
|
||||
@MainActor @IBAction func showPreferences(_ sender: Any?) {
|
||||
if preferencesWindowController == nil {
|
||||
preferencesWindowController = windowControllerWithName("Preferences")
|
||||
}
|
||||
@@ -578,35 +578,35 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
preferencesWindowController!.showWindow(self)
|
||||
}
|
||||
|
||||
@IBAction func newMainWindow(_ sender: Any?) {
|
||||
@MainActor @IBAction func newMainWindow(_ sender: Any?) {
|
||||
createAndShowMainWindow()
|
||||
}
|
||||
|
||||
@IBAction func showMainWindow(_ sender: Any?) {
|
||||
@MainActor @IBAction func showMainWindow(_ sender: Any?) {
|
||||
createAndShowMainWindowIfNecessary()
|
||||
mainWindowController?.window?.makeKey()
|
||||
}
|
||||
|
||||
@IBAction func refreshAll(_ sender: Any?) {
|
||||
@MainActor @IBAction func refreshAll(_ sender: Any?) {
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present)
|
||||
}
|
||||
|
||||
@IBAction func showAddWebFeedWindow(_ sender: Any?) {
|
||||
@MainActor @IBAction func showAddWebFeedWindow(_ sender: Any?) {
|
||||
addWebFeed(nil)
|
||||
}
|
||||
|
||||
@IBAction func showAddRedditFeedWindow(_ sender: Any?) {
|
||||
@MainActor @IBAction func showAddRedditFeedWindow(_ sender: Any?) {
|
||||
createAndShowMainWindowIfNecessary()
|
||||
addFeedController = AddFeedController(hostWindow: mainWindowController!.window!)
|
||||
addFeedController?.showAddFeedSheet(.redditFeed)
|
||||
}
|
||||
|
||||
@IBAction func showAddFolderWindow(_ sender: Any?) {
|
||||
@MainActor @IBAction func showAddFolderWindow(_ sender: Any?) {
|
||||
createAndShowMainWindowIfNecessary()
|
||||
showAddFolderSheetOnWindow(mainWindowController!.window!)
|
||||
}
|
||||
|
||||
@IBAction func showKeyboardShortcutsWindow(_ sender: Any?) {
|
||||
@MainActor @IBAction func showKeyboardShortcutsWindow(_ sender: Any?) {
|
||||
if keyboardShortcutsWindowController == nil {
|
||||
|
||||
keyboardShortcutsWindowController = WebViewWindowController(title: NSLocalizedString("window.title.keyboard-shortcuts", comment: "Keyboard Shortcuts"))
|
||||
@@ -619,13 +619,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
let minSize = NSSize(width: 400, height: 400)
|
||||
window.setPointAndSizeAdjustingForScreen(point: point, size: size, minimumSize: minSize)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
keyboardShortcutsWindowController!.showWindow(self)
|
||||
}
|
||||
|
||||
@IBAction func toggleInspectorWindow(_ sender: Any?) {
|
||||
@MainActor @IBAction func toggleInspectorWindow(_ sender: Any?) {
|
||||
if inspectorWindowController == nil {
|
||||
inspectorWindowController = (windowControllerWithName("Inspector") as! InspectorWindowController)
|
||||
}
|
||||
@@ -639,58 +639,58 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func importOPMLFromFile(_ sender: Any?) {
|
||||
@MainActor @IBAction func importOPMLFromFile(_ sender: Any?) {
|
||||
createAndShowMainWindowIfNecessary()
|
||||
if mainWindowController!.isDisplayingSheet {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
importOPMLController = ImportOPMLWindowController()
|
||||
importOPMLController?.runSheetOnWindow(mainWindowController!.window!)
|
||||
}
|
||||
|
||||
@IBAction func importNNW3FromFile(_ sender: Any?) {
|
||||
|
||||
@MainActor @IBAction func importNNW3FromFile(_ sender: Any?) {
|
||||
createAndShowMainWindowIfNecessary()
|
||||
if mainWindowController!.isDisplayingSheet {
|
||||
return
|
||||
}
|
||||
NNW3ImportController.askUserToImportNNW3Subscriptions(window: mainWindowController!.window!)
|
||||
}
|
||||
|
||||
@IBAction func exportOPML(_ sender: Any?) {
|
||||
|
||||
@MainActor @IBAction func exportOPML(_ sender: Any?) {
|
||||
createAndShowMainWindowIfNecessary()
|
||||
if mainWindowController!.isDisplayingSheet {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
exportOPMLController = ExportOPMLWindowController()
|
||||
exportOPMLController?.runSheetOnWindow(mainWindowController!.window!)
|
||||
}
|
||||
|
||||
@IBAction func addAppNews(_ sender: Any?) {
|
||||
|
||||
@MainActor @IBAction func addAppNews(_ sender: Any?) {
|
||||
if AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() {
|
||||
return
|
||||
}
|
||||
addWebFeed(AccountManager.netNewsWireNewsURL, name: "NetNewsWire News")
|
||||
}
|
||||
|
||||
@IBAction func openWebsite(_ sender: Any?) {
|
||||
@MainActor @IBAction func openWebsite(_ sender: Any?) {
|
||||
|
||||
Browser.open("https://netnewswire.com/", inBackground: false)
|
||||
}
|
||||
|
||||
@IBAction func showHelp(_ sender: Any?) {
|
||||
|
||||
@MainActor @IBAction func showHelp(_ sender: Any?) {
|
||||
|
||||
Browser.open("https://netnewswire.com/help/mac/6.1/en/", inBackground: false)
|
||||
}
|
||||
|
||||
@IBAction func gotoToday(_ sender: Any?) {
|
||||
@MainActor @IBAction func gotoToday(_ sender: Any?) {
|
||||
|
||||
createAndShowMainWindowIfNecessary()
|
||||
mainWindowController!.gotoToday(sender)
|
||||
}
|
||||
|
||||
@IBAction func gotoAllUnread(_ sender: Any?) {
|
||||
@MainActor @IBAction func gotoAllUnread(_ sender: Any?) {
|
||||
|
||||
createAndShowMainWindowIfNecessary()
|
||||
mainWindowController!.gotoAllUnread(sender)
|
||||
@@ -702,27 +702,27 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
mainWindowController!.gotoStarred(sender)
|
||||
}
|
||||
|
||||
@IBAction func sortByOldestArticleOnTop(_ sender: Any?) {
|
||||
@MainActor @IBAction func sortByOldestArticleOnTop(_ sender: Any?) {
|
||||
|
||||
AppDefaults.shared.timelineSortDirection = .orderedAscending
|
||||
}
|
||||
|
||||
@IBAction func sortByNewestArticleOnTop(_ sender: Any?) {
|
||||
@MainActor @IBAction func sortByNewestArticleOnTop(_ sender: Any?) {
|
||||
|
||||
AppDefaults.shared.timelineSortDirection = .orderedDescending
|
||||
}
|
||||
|
||||
@IBAction func groupByFeedToggled(_ sender: NSMenuItem) {
|
||||
|
||||
@MainActor @IBAction func groupByFeedToggled(_ sender: NSMenuItem) {
|
||||
AppDefaults.shared.timelineGroupByFeed.toggle()
|
||||
}
|
||||
|
||||
@IBAction func checkForUpdates(_ sender: Any?) {
|
||||
@MainActor @IBAction func checkForUpdates(_ sender: Any?) {
|
||||
#if !MAC_APP_STORE && !TEST
|
||||
self.softwareUpdater.checkForUpdates()
|
||||
#endif
|
||||
}
|
||||
|
||||
@IBAction func showAbout(_ sender: Any?) {
|
||||
|
||||
@MainActor @IBAction func showAbout(_ sender: Any?) {
|
||||
if #available(macOS 12, *) {
|
||||
for window in NSApplication.shared.windows {
|
||||
if window.identifier == .aboutNetNewsWire {
|
||||
@@ -741,26 +741,26 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
||||
|
||||
// MARK: - NSMenuDelegate
|
||||
|
||||
extension AppDelegate: NSMenuDelegate {
|
||||
@MainActor extension AppDelegate: NSMenuDelegate {
|
||||
|
||||
public func menuNeedsUpdate(_ menu: NSMenu) {
|
||||
let newShareMenu = mainWindowController?.shareMenu
|
||||
|
||||
|
||||
guard menu != fileMenuItem.submenu else {
|
||||
shareMenuItem.isEnabled = newShareMenu != nil
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
menu.removeAllItems()
|
||||
if let newShareMenu = newShareMenu {
|
||||
menu.takeItems(from: newShareMenu)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Debug Menu
|
||||
extension AppDelegate {
|
||||
@MainActor extension AppDelegate {
|
||||
|
||||
@IBAction func debugSearch(_ sender: Any?) {
|
||||
AccountManager.shared.defaultAccount.debugRunSearch()
|
||||
@@ -787,7 +787,7 @@ extension AppDelegate {
|
||||
let configuration = NSWorkspace.OpenConfiguration()
|
||||
configuration.createsNewApplicationInstance = true
|
||||
NSWorkspace.shared.openApplication(at: Bundle.main.bundleURL, configuration: configuration)
|
||||
|
||||
|
||||
NSApp.terminate(self)
|
||||
}
|
||||
}
|
||||
@@ -832,7 +832,7 @@ extension AppDelegate {
|
||||
|
||||
}
|
||||
|
||||
internal extension AppDelegate {
|
||||
@MainActor internal extension AppDelegate {
|
||||
|
||||
func fireOldTimers() {
|
||||
// It’s possible there’s a refresh timer set to go off in the past.
|
||||
@@ -840,7 +840,7 @@ internal extension AppDelegate {
|
||||
refreshTimer?.fireOldTimer()
|
||||
syncTimer?.fireOldTimer()
|
||||
}
|
||||
|
||||
|
||||
func objectsForInspector() -> [Any]? {
|
||||
guard let window = NSApplication.shared.mainWindow, let windowController = window.windowController as? MainWindowController else {
|
||||
return nil
|
||||
@@ -858,15 +858,15 @@ internal extension AppDelegate {
|
||||
sortByNewestArticleOnTopMenuItem.state = sortByNewestOnTop ? .on : .off
|
||||
sortByOldestArticleOnTopMenuItem.state = sortByNewestOnTop ? .off : .on
|
||||
}
|
||||
|
||||
|
||||
func updateGroupByFeedMenuItem() {
|
||||
let groupByFeedEnabled = AppDefaults.shared.timelineGroupByFeed
|
||||
groupArticlesByFeedMenuItem.state = groupByFeedEnabled ? .on : .off
|
||||
}
|
||||
|
||||
|
||||
func importTheme(filename: String) {
|
||||
guard let window = mainWindowController?.window else { return }
|
||||
|
||||
|
||||
do {
|
||||
let theme = try ArticleTheme(path: filename, isAppTheme: false)
|
||||
let alert = NSAlert()
|
||||
@@ -874,11 +874,11 @@ internal extension AppDelegate {
|
||||
|
||||
let localizedMessageText = NSLocalizedString("alert.title.install-theme.%@.%@", comment: "Install theme “%@” by %@? — the order of the variables is theme name, author name")
|
||||
alert.messageText = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name, theme.creatorName) as String
|
||||
|
||||
|
||||
var attrs = [NSAttributedString.Key : Any]()
|
||||
attrs[.font] = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
|
||||
attrs[.foregroundColor] = NSColor.textColor
|
||||
|
||||
|
||||
let titleParagraphStyle = NSMutableParagraphStyle()
|
||||
titleParagraphStyle.alignment = .center
|
||||
attrs[.paragraphStyle] = titleParagraphStyle
|
||||
@@ -912,7 +912,7 @@ internal extension AppDelegate {
|
||||
logger.error("Error importing theme: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
alert.beginSheetModal(for: window) { result in
|
||||
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
|
||||
|
||||
@@ -940,10 +940,10 @@ internal extension AppDelegate {
|
||||
presentThemeImportError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func confirmImportSuccess(themeName: String) {
|
||||
guard let window = mainWindowController?.window else { return }
|
||||
|
||||
|
||||
let alert = NSAlert()
|
||||
alert.alertStyle = .informational
|
||||
alert.messageText = NSLocalizedString("alert.title.theme-installed", comment: "Theme installed")
|
||||
@@ -964,13 +964,13 @@ internal extension AppDelegate {
|
||||
if currentDate > expiryDate {
|
||||
return // If after August 9th, don't show
|
||||
}
|
||||
|
||||
|
||||
if AccountManager.shared.anyLocalOriCloudAccountHasAtLeastOneTwitterFeed() {
|
||||
showTwitterDeprecationAlert()
|
||||
}
|
||||
AppDefaults.shared.twitterDeprecationAlertShown = true
|
||||
}
|
||||
|
||||
|
||||
private func showTwitterDeprecationAlert() {
|
||||
DispatchQueue.main.async {
|
||||
let alert = NSAlert()
|
||||
@@ -982,13 +982,12 @@ internal extension AppDelegate {
|
||||
alert.runModal()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
the ScriptingAppDelegate protocol exposes a narrow set of accessors with
|
||||
internal visibility which are very similar to some private vars.
|
||||
|
||||
|
||||
These would be unnecessary if the similar accessors were marked internal rather than private,
|
||||
but for now, we'll keep the stratification of visibility
|
||||
*/
|
||||
@@ -1007,8 +1006,8 @@ extension AppDelegate : ScriptingAppDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
extension AppDelegate: NSWindowRestoration {
|
||||
|
||||
@MainActor extension AppDelegate: NSWindowRestoration {
|
||||
|
||||
@objc static func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier, state: NSCoder, completionHandler: @escaping (NSWindow?, Error?) -> Void) {
|
||||
var mainWindow: NSWindow? = nil
|
||||
if identifier.rawValue == WindowRestorationIdentifiers.mainWindow {
|
||||
@@ -1016,39 +1015,39 @@ extension AppDelegate: NSWindowRestoration {
|
||||
}
|
||||
completionHandler(mainWindow, nil)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Handle Notification Actions
|
||||
|
||||
private extension AppDelegate {
|
||||
|
||||
@MainActor private extension AppDelegate {
|
||||
|
||||
func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
|
||||
markArticle(userInfo: userInfo, statusKey: .read)
|
||||
}
|
||||
|
||||
|
||||
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
|
||||
markArticle(userInfo: userInfo, statusKey: .starred)
|
||||
}
|
||||
|
||||
|
||||
func markArticle(userInfo: [AnyHashable: Any], statusKey: ArticleStatus.Key) {
|
||||
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any],
|
||||
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
|
||||
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let account = AccountManager.shared.existingAccount(with: accountID) else {
|
||||
logger.debug("No account found from notification.")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let articles = try? account.fetchArticles(.articleIDs([articleID])), !articles.isEmpty else {
|
||||
logger.debug("No article found from search using: \(articleID, privacy: .public)")
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
account.mark(articles: articles, statusKey: statusKey, flag: true) { _ in }
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -32,22 +32,30 @@
|
||||
<objects>
|
||||
<viewController title="General" storyboardIdentifier="General" id="iuH-lz-18x" customClass="GeneralPreferencesViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<view key="view" id="WnV-px-wCT">
|
||||
<rect key="frame" x="0.0" y="0.0" width="509" height="338"/>
|
||||
<rect key="frame" x="0.0" y="0.0" width="509" height="361"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<customView horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" verticalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Ut3-yd-q6G">
|
||||
<rect key="frame" x="54" y="16" width="400" height="306"/>
|
||||
<subviews>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pR2-Bf-7Fd">
|
||||
<rect key="frame" x="6" y="285" width="106" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Article Text Size:" id="xQu-QV-91i">
|
||||
<rect key="frame" x="6" y="267" width="105" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Article Text Size:" id="xQu-QV-91i">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="4AW-o5-47e">
|
||||
<rect key="frame" x="52" y="309" width="59" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Timeline:" id="wi9-yM-Ri0">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Z6O-Zt-V1g">
|
||||
<rect key="frame" x="115" y="278" width="289" height="25"/>
|
||||
<rect key="frame" x="114" y="260" width="289" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Medium" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" tag="2" imageScaling="proportionallyDown" inset="2" selectedItem="jMV-2o-5Oh" id="6pw-Vq-tjM">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
@@ -76,7 +84,7 @@
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ISO-Wu-R60">
|
||||
<rect key="frame" x="115" y="244" width="289" height="25"/>
|
||||
<rect key="frame" x="114" y="226" width="289" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Default" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="Pkl-EA-Goa" id="vN9-pm-Gls">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
@@ -91,7 +99,7 @@
|
||||
</connections>
|
||||
</popUpButton>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="1w0-nA-DEO">
|
||||
<rect key="frame" x="111" y="207" width="161" height="32"/>
|
||||
<rect key="frame" x="110" y="189" width="161" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Open Themes Folder" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ySX-5i-SP1">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -104,15 +112,15 @@
|
||||
<rect key="frame" x="0.0" y="197" width="400" height="5"/>
|
||||
</box>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Wsb-Lr-8Q7">
|
||||
<rect key="frame" x="6" y="166" width="106" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="right" title="Browser:" id="CgU-dE-Qtb">
|
||||
<rect key="frame" x="53" y="154" width="58" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Browser:" id="CgU-dE-Qtb">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Ci4-fW-KjU">
|
||||
<rect key="frame" x="115" y="159" width="289" height="25"/>
|
||||
<rect key="frame" x="114" y="147" width="289" height="25"/>
|
||||
<popUpButtonCell key="cell" type="push" title="Safari" bezelStyle="rounded" alignment="left" lineBreakMode="truncatingTail" state="on" borderStyle="borderAndBezel" imageScaling="proportionallyDown" inset="2" selectedItem="ObP-qK-qDJ" id="hrm-aT-Rc2" userLabel="Popup">
|
||||
<behavior key="behavior" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="menu"/>
|
||||
@@ -127,7 +135,7 @@
|
||||
</connections>
|
||||
</popUpButton>
|
||||
<button horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="Ubm-Pk-l7x">
|
||||
<rect key="frame" x="116" y="132" width="284" height="18"/>
|
||||
<rect key="frame" x="115" y="122" width="284" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Open web pages in background in browser" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="t0a-LN-rCv">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -147,7 +155,7 @@
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="j0t-Wa-UTL">
|
||||
<rect key="frame" x="135" y="109" width="235" height="16"/>
|
||||
<rect key="frame" x="134" y="99" width="235" height="16"/>
|
||||
<textFieldCell key="cell" controlSize="small" title="Press the Shift key to do the opposite." id="EMq-9M-zTJ">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
@@ -158,15 +166,15 @@
|
||||
<rect key="frame" x="0.0" y="38" width="400" height="5"/>
|
||||
</box>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="ucw-vG-yLt">
|
||||
<rect key="frame" x="6" y="7" width="106" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="right" title="Refresh Feeds:" id="F7c-lm-g97">
|
||||
<rect key="frame" x="16" y="7" width="95" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Refresh Feeds:" id="F7c-lm-g97">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<popUpButton verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="SFF-mL-yc8">
|
||||
<rect key="frame" x="115" y="0.0" width="289" height="25"/>
|
||||
<rect key="frame" x="114" y="0.0" width="289" height="25"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" relation="greaterThanOrEqual" constant="200" id="N1a-qV-4Os"/>
|
||||
</constraints>
|
||||
@@ -201,15 +209,15 @@
|
||||
</popUpButtonCell>
|
||||
</popUpButton>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="yrg-M3-Dbz">
|
||||
<rect key="frame" x="6" y="79" width="106" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="truncatingTail" sendsActionOnEndEditing="YES" alignment="right" title="Safari Extension:" id="Eth-o0-pWM">
|
||||
<rect key="frame" x="7" y="71" width="106" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" alignment="right" title="Safari Extension:" id="Eth-o0-pWM">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="controlColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="wtY-Zd-Ps9">
|
||||
<rect key="frame" x="116" y="78" width="284" height="18"/>
|
||||
<rect key="frame" x="115" y="70" width="284" height="18"/>
|
||||
<buttonCell key="cell" type="radio" title="Open feeds in NetNewsWire" bezelStyle="regularSquare" imagePosition="left" alignment="left" state="on" inset="2" id="uvx-O8-HvU">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -223,7 +231,7 @@
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Yrc-6Q-kx8">
|
||||
<rect key="frame" x="116" y="56" width="284" height="18"/>
|
||||
<rect key="frame" x="115" y="48" width="284" height="18"/>
|
||||
<buttonCell key="cell" type="radio" title="Open feeds in default news reader" bezelStyle="regularSquare" imagePosition="left" alignment="left" inset="2" id="SkZ-tE-blK">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
@@ -233,43 +241,62 @@
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="S2Z-bG-jYk">
|
||||
<rect key="frame" x="6" y="251" width="106" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" alignment="right" title="Article Theme:" id="MQe-Za-N8J">
|
||||
<rect key="frame" x="18" y="233" width="93" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Article Theme:" id="MQe-Za-N8J">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button horizontalHuggingPriority="1000" verticalHuggingPriority="1000" horizontalCompressionResistancePriority="1000" translatesAutoresizingMaskIntoConstraints="NO" id="qmW-lo-ERe">
|
||||
<rect key="frame" x="115" y="308" width="208" height="18"/>
|
||||
<buttonCell key="cell" type="check" title="Mark articles as read on scroll" bezelStyle="regularSquare" imagePosition="left" state="on" inset="2" id="ANv-PZ-pn6">
|
||||
<behavior key="behavior" changeContents="YES" doesNotDimImage="YES" lightByContents="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<binding destination="mAF-gO-1PI" name="value" keyPath="values.markArticlesAsReadOnScroll" id="75p-kw-A3m">
|
||||
<dictionary key="options">
|
||||
<bool key="NSAllowsEditingMultipleValuesSelection" value="NO"/>
|
||||
<bool key="NSConditionallySetsEnabled" value="NO"/>
|
||||
<integer key="NSMultipleValuesPlaceholder" value="0"/>
|
||||
<integer key="NSNoSelectionPlaceholder" value="0"/>
|
||||
<integer key="NSNotApplicablePlaceholder" value="0"/>
|
||||
<integer key="NSNullPlaceholder" value="0"/>
|
||||
<bool key="NSRaisesForNotApplicableKeys" value="NO"/>
|
||||
</dictionary>
|
||||
</binding>
|
||||
</connections>
|
||||
</button>
|
||||
<box verticalHuggingPriority="750" boxType="separator" translatesAutoresizingMaskIntoConstraints="NO" id="cXT-QS-qRf">
|
||||
<rect key="frame" x="0.0" y="294" width="399" height="5"/>
|
||||
</box>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="Z6O-Zt-V1g" firstAttribute="top" secondItem="cXT-QS-qRf" secondAttribute="bottom" constant="12" id="04m-cu-OZo"/>
|
||||
<constraint firstItem="Wsb-Lr-8Q7" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="17A-5m-ZG0"/>
|
||||
<constraint firstItem="Z6O-Zt-V1g" firstAttribute="leading" secondItem="pR2-Bf-7Fd" secondAttribute="trailing" constant="8" symbolic="YES" id="2wM-K6-eAF"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="Ubm-Pk-l7x" secondAttribute="trailing" id="3h4-m7-pMW"/>
|
||||
<constraint firstItem="cXT-QS-qRf" firstAttribute="leading" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="4zW-Zw-kDb"/>
|
||||
<constraint firstItem="Yrc-6Q-kx8" firstAttribute="top" secondItem="wtY-Zd-Ps9" secondAttribute="bottom" constant="6" symbolic="YES" id="59s-XY-cPN"/>
|
||||
<constraint firstItem="Wsb-Lr-8Q7" firstAttribute="width" secondItem="pR2-Bf-7Fd" secondAttribute="width" id="5IG-Sz-w9C"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="j0t-Wa-UTL" secondAttribute="trailing" id="7Oh-pf-X12"/>
|
||||
<constraint firstItem="Ubm-Pk-l7x" firstAttribute="leading" secondItem="SFF-mL-yc8" secondAttribute="leading" id="7cy-O4-Zz2"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="1w0-nA-DEO" secondAttribute="trailing" constant="20" symbolic="YES" id="7mc-lc-2SG"/>
|
||||
<constraint firstItem="yrg-M3-Dbz" firstAttribute="width" secondItem="pR2-Bf-7Fd" secondAttribute="width" id="7pB-AA-oez"/>
|
||||
<constraint firstItem="yrg-M3-Dbz" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="8KD-aE-Zer"/>
|
||||
<constraint firstItem="Ci4-fW-KjU" firstAttribute="width" secondItem="SFF-mL-yc8" secondAttribute="width" id="AE4-am-IWK"/>
|
||||
<constraint firstItem="wtY-Zd-Ps9" firstAttribute="firstBaseline" secondItem="yrg-M3-Dbz" secondAttribute="baseline" id="AeO-w1-7yq"/>
|
||||
<constraint firstItem="1w0-nA-DEO" firstAttribute="leading" secondItem="Ci4-fW-KjU" secondAttribute="leading" id="FZM-a2-She"/>
|
||||
<constraint firstItem="Ubm-Pk-l7x" firstAttribute="top" secondItem="Ci4-fW-KjU" secondAttribute="bottom" constant="14" id="GNx-7d-yAo"/>
|
||||
<constraint firstItem="Wsb-Lr-8Q7" firstAttribute="trailing" secondItem="pR2-Bf-7Fd" secondAttribute="trailing" id="GRf-bX-t7o"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="qmW-lo-ERe" secondAttribute="trailing" id="CRn-3q-DHe"/>
|
||||
<constraint firstItem="Ubm-Pk-l7x" firstAttribute="top" secondItem="Ci4-fW-KjU" secondAttribute="bottom" constant="12" id="GNx-7d-yAo"/>
|
||||
<constraint firstItem="ISO-Wu-R60" firstAttribute="leading" secondItem="Z6O-Zt-V1g" secondAttribute="leading" id="GxL-2l-CYb"/>
|
||||
<constraint firstItem="4AW-o5-47e" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="HgL-ri-piv"/>
|
||||
<constraint firstItem="qmW-lo-ERe" firstAttribute="leading" secondItem="4AW-o5-47e" secondAttribute="trailing" constant="8" symbolic="YES" id="Ino-V2-58K"/>
|
||||
<constraint firstItem="hQy-ng-ijd" firstAttribute="leading" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="KEI-R5-rzD"/>
|
||||
<constraint firstItem="S2Z-bG-jYk" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="KQI-3T-s6M"/>
|
||||
<constraint firstItem="4AW-o5-47e" firstAttribute="firstBaseline" secondItem="qmW-lo-ERe" secondAttribute="firstBaseline" id="KdH-x0-OD4"/>
|
||||
<constraint firstItem="pR2-Bf-7Fd" firstAttribute="leading" secondItem="Ut3-yd-q6G" secondAttribute="leading" constant="8" id="LRG-HZ-yxh"/>
|
||||
<constraint firstAttribute="trailing" secondItem="SFF-mL-yc8" secondAttribute="trailing" id="N39-Q9-X5Q"/>
|
||||
<constraint firstItem="Wsb-Lr-8Q7" firstAttribute="trailing" secondItem="pR2-Bf-7Fd" secondAttribute="trailing" id="Ore-Y8-DM8"/>
|
||||
<constraint firstItem="ISO-Wu-R60" firstAttribute="trailing" secondItem="Z6O-Zt-V1g" secondAttribute="trailing" id="P3r-hD-nE8"/>
|
||||
<constraint firstItem="S2Z-bG-jYk" firstAttribute="width" secondItem="pR2-Bf-7Fd" secondAttribute="width" id="QCg-QQ-rJf"/>
|
||||
<constraint firstItem="ISO-Wu-R60" firstAttribute="leading" secondItem="S2Z-bG-jYk" secondAttribute="trailing" constant="8" symbolic="YES" id="QDj-xS-6Ox"/>
|
||||
<constraint firstItem="S2Z-bG-jYk" firstAttribute="trailing" secondItem="pR2-Bf-7Fd" secondAttribute="trailing" id="QF9-uC-T7a"/>
|
||||
<constraint firstAttribute="trailing" secondItem="hQy-ng-ijd" secondAttribute="trailing" id="RbT-jK-fBb"/>
|
||||
<constraint firstItem="ucw-vG-yLt" firstAttribute="trailing" secondItem="pR2-Bf-7Fd" secondAttribute="trailing" id="S1Y-iY-pca"/>
|
||||
<constraint firstItem="Z6O-Zt-V1g" firstAttribute="top" secondItem="Ut3-yd-q6G" secondAttribute="top" constant="4" id="SKa-kI-qdW"/>
|
||||
<constraint firstItem="Ubm-Pk-l7x" firstAttribute="width" secondItem="SFF-mL-yc8" secondAttribute="width" id="TX4-iO-J5E"/>
|
||||
<constraint firstItem="j0t-Wa-UTL" firstAttribute="leading" secondItem="Ubm-Pk-l7x" secondAttribute="leading" constant="19" id="UKq-8p-lyR"/>
|
||||
<constraint firstItem="j0t-Wa-UTL" firstAttribute="top" secondItem="Ubm-Pk-l7x" secondAttribute="bottom" constant="8" id="XTw-Ef-FD3"/>
|
||||
@@ -277,31 +304,32 @@
|
||||
<constraint firstItem="1w0-nA-DEO" firstAttribute="top" secondItem="ISO-Wu-R60" secondAttribute="bottom" constant="14" id="ZlG-V3-AAd"/>
|
||||
<constraint firstItem="pR2-Bf-7Fd" firstAttribute="firstBaseline" secondItem="Z6O-Zt-V1g" secondAttribute="firstBaseline" id="aO5-iE-L7A"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Z6O-Zt-V1g" secondAttribute="trailing" id="aS9-KA-vSH"/>
|
||||
<constraint firstItem="wtY-Zd-Ps9" firstAttribute="top" secondItem="j0t-Wa-UTL" secondAttribute="bottom" constant="14" id="aod-td-Gim"/>
|
||||
<constraint firstItem="qmW-lo-ERe" firstAttribute="leading" secondItem="Z6O-Zt-V1g" secondAttribute="leading" id="aaj-KG-JNC"/>
|
||||
<constraint firstItem="wtY-Zd-Ps9" firstAttribute="top" secondItem="j0t-Wa-UTL" secondAttribute="bottom" constant="12" id="aod-td-Gim"/>
|
||||
<constraint firstItem="SFF-mL-yc8" firstAttribute="firstBaseline" secondItem="ucw-vG-yLt" secondAttribute="firstBaseline" id="aqn-St-DJy"/>
|
||||
<constraint firstItem="Tdg-6Y-gvW" firstAttribute="leading" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="b3I-JF-If3"/>
|
||||
<constraint firstItem="ucw-vG-yLt" firstAttribute="width" secondItem="pR2-Bf-7Fd" secondAttribute="width" id="cxz-v5-yCz"/>
|
||||
<constraint firstItem="wtY-Zd-Ps9" firstAttribute="leading" secondItem="SFF-mL-yc8" secondAttribute="leading" id="dTq-cu-Z2s"/>
|
||||
<constraint firstItem="Yrc-6Q-kx8" firstAttribute="trailing" secondItem="wtY-Zd-Ps9" secondAttribute="trailing" id="e6V-q6-WJq"/>
|
||||
<constraint firstItem="SFF-mL-yc8" firstAttribute="top" secondItem="hQy-ng-ijd" secondAttribute="bottom" constant="16" id="eM7-OM-Qsz"/>
|
||||
<constraint firstItem="SFF-mL-yc8" firstAttribute="top" secondItem="hQy-ng-ijd" secondAttribute="bottom" constant="12" id="eM7-OM-Qsz"/>
|
||||
<constraint firstItem="cXT-QS-qRf" firstAttribute="top" secondItem="4AW-o5-47e" secondAttribute="bottom" constant="12" id="eQ9-Mk-aYj"/>
|
||||
<constraint firstItem="Yrc-6Q-kx8" firstAttribute="leading" secondItem="wtY-Zd-Ps9" secondAttribute="leading" id="gNX-Yc-DdD"/>
|
||||
<constraint firstItem="1w0-nA-DEO" firstAttribute="leading" secondItem="ISO-Wu-R60" secondAttribute="leading" id="gWR-OU-qcO"/>
|
||||
<constraint firstItem="Ci4-fW-KjU" firstAttribute="top" secondItem="Tdg-6Y-gvW" secondAttribute="bottom" constant="16" id="hXl-1D-lTD"/>
|
||||
<constraint firstItem="hQy-ng-ijd" firstAttribute="top" secondItem="Yrc-6Q-kx8" secondAttribute="bottom" constant="16" id="i2g-cZ-EV4"/>
|
||||
<constraint firstItem="SFF-mL-yc8" firstAttribute="leading" secondItem="Ci4-fW-KjU" secondAttribute="leading" id="jfK-1b-DYd"/>
|
||||
<constraint firstAttribute="trailing" secondItem="cXT-QS-qRf" secondAttribute="trailing" id="hGN-Qp-fMS"/>
|
||||
<constraint firstItem="Ci4-fW-KjU" firstAttribute="top" secondItem="Tdg-6Y-gvW" secondAttribute="bottom" constant="12" id="hXl-1D-lTD"/>
|
||||
<constraint firstItem="wtY-Zd-Ps9" firstAttribute="leading" secondItem="yrg-M3-Dbz" secondAttribute="trailing" constant="6" symbolic="YES" id="hpP-sx-veV"/>
|
||||
<constraint firstItem="hQy-ng-ijd" firstAttribute="top" secondItem="Yrc-6Q-kx8" secondAttribute="bottom" constant="12" id="i2g-cZ-EV4"/>
|
||||
<constraint firstItem="ucw-vG-yLt" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="Ut3-yd-q6G" secondAttribute="leading" id="lDL-JN-ANP"/>
|
||||
<constraint firstItem="Tdg-6Y-gvW" firstAttribute="top" secondItem="1w0-nA-DEO" secondAttribute="bottom" constant="14" id="lEK-yl-TCM"/>
|
||||
<constraint firstItem="Tdg-6Y-gvW" firstAttribute="top" secondItem="1w0-nA-DEO" secondAttribute="bottom" constant="12" id="lEK-yl-TCM"/>
|
||||
<constraint firstItem="Z6O-Zt-V1g" firstAttribute="width" secondItem="SFF-mL-yc8" secondAttribute="width" id="noW-Jf-Xbs"/>
|
||||
<constraint firstItem="qmW-lo-ERe" firstAttribute="top" secondItem="Ut3-yd-q6G" secondAttribute="top" constant="4" id="oto-bS-L2S"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Tdg-6Y-gvW" secondAttribute="trailing" id="qzz-gu-8kO"/>
|
||||
<constraint firstItem="Wsb-Lr-8Q7" firstAttribute="firstBaseline" secondItem="Ci4-fW-KjU" secondAttribute="firstBaseline" id="rPX-je-OG5"/>
|
||||
<constraint firstItem="Ci4-fW-KjU" firstAttribute="leading" secondItem="Wsb-Lr-8Q7" secondAttribute="trailing" constant="8" symbolic="YES" id="rcx-B6-zLP"/>
|
||||
<constraint firstItem="yrg-M3-Dbz" firstAttribute="trailing" secondItem="pR2-Bf-7Fd" secondAttribute="trailing" id="wHt-nz-9FO"/>
|
||||
<constraint firstItem="S2Z-bG-jYk" firstAttribute="firstBaseline" secondItem="ISO-Wu-R60" secondAttribute="firstBaseline" id="xt6-ua-xz8"/>
|
||||
<constraint firstItem="SFF-mL-yc8" firstAttribute="leading" secondItem="ucw-vG-yLt" secondAttribute="trailing" constant="8" symbolic="YES" id="yBm-Dc-lGA"/>
|
||||
<constraint firstAttribute="bottom" secondItem="SFF-mL-yc8" secondAttribute="bottom" constant="4" id="zIa-Ca-y3J"/>
|
||||
<constraint firstItem="ISO-Wu-R60" firstAttribute="top" secondItem="Z6O-Zt-V1g" secondAttribute="bottom" constant="14" id="zaM-J3-VcP"/>
|
||||
<constraint firstAttribute="trailing" secondItem="Ci4-fW-KjU" secondAttribute="trailing" id="zbx-Ch-NEt"/>
|
||||
<constraint firstItem="ucw-vG-yLt" firstAttribute="trailing" secondItem="pR2-Bf-7Fd" secondAttribute="trailing" id="zkC-Ma-Dz8"/>
|
||||
</constraints>
|
||||
</customView>
|
||||
</subviews>
|
||||
@@ -427,35 +455,40 @@
|
||||
</binding>
|
||||
</connections>
|
||||
</button>
|
||||
<textField verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="OSS-A0-uUS" customClass="LinkTextField" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="121" y="1" width="88" height="16"/>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" sendsActionOnEndEditing="YES" title="Privacy Policy" allowsEditingTextAttributes="YES" id="rJu-r1-AW4">
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="uuc-f2-OFX">
|
||||
<rect key="frame" x="84" y="-6" width="148" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Privacy Policy" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="kSv-Wu-NYx">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="linkColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="showPrivacyPolicy:" target="VX1-M3-K0J" id="s1x-cP-hGd"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="MzL-QQ-2oL" secondAttribute="trailing" id="04t-Su-3fv"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="T4A-0o-p2w" secondAttribute="trailing" id="0zv-Cr-GlR"/>
|
||||
<constraint firstItem="uuc-f2-OFX" firstAttribute="width" secondItem="TKI-a9-bRX" secondAttribute="width" id="4ZH-zo-sNF"/>
|
||||
<constraint firstItem="QCu-J4-0yV" firstAttribute="leading" secondItem="T4A-0o-p2w" secondAttribute="leading" id="4c4-16-5yq"/>
|
||||
<constraint firstItem="TKI-a9-bRX" firstAttribute="leading" secondItem="CeE-AE-hRG" secondAttribute="leading" id="6Sm-VV-Qda"/>
|
||||
<constraint firstItem="EH5-aS-E55" firstAttribute="leading" secondItem="uJD-OF-YVY" secondAttribute="leading" id="6eS-X9-PTK"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="UHg-1l-FlD" secondAttribute="trailing" id="7gl-UP-wqg"/>
|
||||
<constraint firstAttribute="bottom" secondItem="OSS-A0-uUS" secondAttribute="bottom" constant="1" id="BFY-9B-ITb"/>
|
||||
<constraint firstItem="UHg-1l-FlD" firstAttribute="firstBaseline" secondItem="SUN-k3-ZEb" secondAttribute="firstBaseline" id="MAL-Ip-mEN"/>
|
||||
<constraint firstItem="MzL-QQ-2oL" firstAttribute="leading" secondItem="uJD-OF-YVY" secondAttribute="leading" id="MMt-v0-0gl"/>
|
||||
<constraint firstItem="CeE-AE-hRG" firstAttribute="leading" secondItem="T4A-0o-p2w" secondAttribute="leading" id="NWB-BO-GtL"/>
|
||||
<constraint firstItem="uuc-f2-OFX" firstAttribute="leading" secondItem="TKI-a9-bRX" secondAttribute="leading" id="PQj-is-Zlx"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="QCu-J4-0yV" secondAttribute="trailing" id="QVh-z8-aNJ"/>
|
||||
<constraint firstItem="UHg-1l-FlD" firstAttribute="leading" secondItem="CeE-AE-hRG" secondAttribute="leading" id="QlP-bI-uga"/>
|
||||
<constraint firstItem="EH5-aS-E55" firstAttribute="top" secondItem="uJD-OF-YVY" secondAttribute="top" id="VDU-as-fdx"/>
|
||||
<constraint firstAttribute="bottom" secondItem="uuc-f2-OFX" secondAttribute="bottom" constant="1" id="YA7-Xm-cFO"/>
|
||||
<constraint firstItem="Q6M-Iz-Ypx" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="uJD-OF-YVY" secondAttribute="leading" id="Ygv-ha-RLn"/>
|
||||
<constraint firstItem="uuc-f2-OFX" firstAttribute="top" secondItem="UHg-1l-FlD" secondAttribute="bottom" constant="20" symbolic="YES" id="aqe-xY-QDd"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="TKI-a9-bRX" secondAttribute="trailing" id="bLP-TU-TeL"/>
|
||||
<constraint firstItem="OSS-A0-uUS" firstAttribute="centerX" secondItem="uJD-OF-YVY" secondAttribute="centerX" id="bsc-Gf-dIS"/>
|
||||
<constraint firstItem="SUN-k3-ZEb" firstAttribute="trailing" secondItem="Q6M-Iz-Ypx" secondAttribute="trailing" id="c23-mt-Mfd"/>
|
||||
<constraint firstItem="SUN-k3-ZEb" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="uJD-OF-YVY" secondAttribute="leading" id="dj1-Uj-ibG"/>
|
||||
<constraint firstItem="MzL-QQ-2oL" firstAttribute="top" secondItem="CeE-AE-hRG" secondAttribute="bottom" constant="8" symbolic="YES" id="emd-u5-hgZ"/>
|
||||
<constraint firstAttribute="trailing" relation="greaterThanOrEqual" secondItem="uuc-f2-OFX" secondAttribute="trailing" id="gBp-7A-zG6"/>
|
||||
<constraint firstItem="CeE-AE-hRG" firstAttribute="top" secondItem="QCu-J4-0yV" secondAttribute="bottom" constant="6" symbolic="YES" id="hYd-1l-oMg"/>
|
||||
<constraint firstItem="T4A-0o-p2w" firstAttribute="firstBaseline" secondItem="EH5-aS-E55" secondAttribute="firstBaseline" id="jvM-Qd-rbB"/>
|
||||
<constraint firstItem="TKI-a9-bRX" firstAttribute="top" secondItem="MzL-QQ-2oL" secondAttribute="bottom" constant="8" symbolic="YES" id="lIt-NB-IL8"/>
|
||||
@@ -475,7 +508,6 @@
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="privacyPolicyTextField" destination="OSS-A0-uUS" id="tmS-6L-sxr"/>
|
||||
<outlet property="releaseBuildsButton" destination="QCu-J4-0yV" id="mjo-l6-P3b"/>
|
||||
<outlet property="testBuildsButton" destination="CeE-AE-hRG" id="mFo-DS-g83"/>
|
||||
</connections>
|
||||
@@ -642,7 +674,7 @@
|
||||
</viewController>
|
||||
<customObject id="AgZ-2t-A2h" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-558" y="830"/>
|
||||
<point key="canvasLocation" x="-558" y="935"/>
|
||||
</scene>
|
||||
<!--Container-->
|
||||
<scene sceneID="fzS-hg-3TF">
|
||||
@@ -809,7 +841,7 @@
|
||||
</viewController>
|
||||
<customObject id="Cne-wm-w1Q" userLabel="First Responder" customClass="NSResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="-36" y="826"/>
|
||||
<point key="canvasLocation" x="-36" y="931"/>
|
||||
</scene>
|
||||
</scenes>
|
||||
<resources>
|
||||
|
||||
+1
-1
@@ -9,7 +9,7 @@
|
||||
import Foundation
|
||||
import RSWeb
|
||||
|
||||
struct Browser {
|
||||
@MainActor struct Browser {
|
||||
|
||||
/// The user-specified default browser for opening web pages.
|
||||
///
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
final class CrashReportWindowController: NSWindowController {
|
||||
@MainActor final class CrashReportWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet var textView: NSTextView! {
|
||||
didSet {
|
||||
|
||||
@@ -16,7 +16,7 @@ import CrashReporter
|
||||
// At some point this code should probably move into RSCore, so Rainier and any other
|
||||
// future apps can use it.
|
||||
|
||||
struct CrashReporter {
|
||||
@MainActor struct CrashReporter {
|
||||
|
||||
struct DefaultsKey {
|
||||
static let sendCrashLogsAutomaticallyKey = "SendCrashLogsAutomatically"
|
||||
|
||||
@@ -10,16 +10,13 @@ import AppKit
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
struct ErrorHandler: Logging {
|
||||
@MainActor struct ErrorHandler: Logging {
|
||||
|
||||
|
||||
|
||||
public static func present(_ error: Error) {
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
|
||||
|
||||
public static func log(_ error: Error) {
|
||||
ErrorHandler.logger.error("\(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector {
|
||||
@MainActor final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector {
|
||||
|
||||
@IBOutlet var nameTextField: NSTextField?
|
||||
@IBOutlet weak var smartFeedImageView: NSImageView!
|
||||
|
||||
@@ -10,7 +10,7 @@ import AppKit
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
final class FolderInspectorViewController: NSViewController, Inspector {
|
||||
@MainActor final class FolderInspectorViewController: NSViewController, Inspector {
|
||||
|
||||
@IBOutlet var nameTextField: NSTextField?
|
||||
@IBOutlet weak var folderImageView: NSImageView!
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
protocol Inspector: AnyObject {
|
||||
@MainActor protocol Inspector: AnyObject {
|
||||
|
||||
var objects: [Any]? { get set }
|
||||
var isFallbackInspector: Bool { get } // Can handle nothing-to-inspect or unexpected type of objects.
|
||||
@@ -20,7 +20,7 @@ protocol Inspector: AnyObject {
|
||||
typealias InspectorViewController = Inspector & NSViewController
|
||||
|
||||
|
||||
final class InspectorWindowController: NSWindowController {
|
||||
@MainActor final class InspectorWindowController: NSWindowController {
|
||||
|
||||
class var shouldOpenAtStartup: Bool {
|
||||
return UserDefaults.standard.bool(forKey: DefaultsKey.windowIsOpen)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
final class NothingInspectorViewController: NSViewController, Inspector {
|
||||
@MainActor final class NothingInspectorViewController: NSViewController, Inspector {
|
||||
|
||||
@IBOutlet var nothingTextField: NSTextField?
|
||||
@IBOutlet var multipleTextField: NSTextField?
|
||||
|
||||
@@ -11,7 +11,7 @@ import Articles
|
||||
import Account
|
||||
import UserNotifications
|
||||
|
||||
final class WebFeedInspectorViewController: NSViewController, Inspector {
|
||||
@MainActor final class WebFeedInspectorViewController: NSViewController, Inspector {
|
||||
|
||||
@IBOutlet weak var iconView: IconView!
|
||||
@IBOutlet weak var nameTextField: NSTextField?
|
||||
|
||||
@@ -66,7 +66,9 @@ struct CreditsNetNewsWireView: View, LoadableAboutData {
|
||||
.onTapGesture {
|
||||
guard let url = appCredit.url else { return }
|
||||
if let _ = URL(string: url) {
|
||||
Browser.open(url, inBackground: false)
|
||||
Task { @MainActor in
|
||||
Browser.open(url, inBackground: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ import RSParser
|
||||
// Else,
|
||||
// display error sheet.
|
||||
|
||||
class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
@MainActor final class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
|
||||
private let hostWindow: NSWindow
|
||||
private var addFeedWindowController: AddFeedWindowController?
|
||||
|
||||
@@ -14,7 +14,7 @@ enum AddFeedWindowControllerType {
|
||||
case redditFeed
|
||||
}
|
||||
|
||||
protocol AddFeedWindowControllerDelegate: AnyObject {
|
||||
@MainActor protocol AddFeedWindowControllerDelegate: AnyObject {
|
||||
|
||||
// userEnteredURL will have already been validated and normalized.
|
||||
func addFeedWindowController(_: AddFeedWindowController, userEnteredURL: URL, userEnteredTitle: String?, container: Container)
|
||||
@@ -22,7 +22,7 @@ protocol AddFeedWindowControllerDelegate: AnyObject {
|
||||
|
||||
}
|
||||
|
||||
protocol AddFeedWindowController {
|
||||
@MainActor protocol AddFeedWindowController {
|
||||
|
||||
var window: NSWindow? { get }
|
||||
func runSheetOnWindow(_ hostWindow: NSWindow)
|
||||
|
||||
@@ -12,7 +12,7 @@ import RSTree
|
||||
import Articles
|
||||
import Account
|
||||
|
||||
class AddWebFeedWindowController : NSWindowController, AddFeedWindowController {
|
||||
@MainActor final class AddWebFeedWindowController : NSWindowController, AddFeedWindowController {
|
||||
|
||||
@IBOutlet var urlTextField: NSTextField!
|
||||
@IBOutlet var nameTextField: NSTextField!
|
||||
|
||||
@@ -11,7 +11,7 @@ import RSCore
|
||||
import RSTree
|
||||
import Account
|
||||
|
||||
class FolderTreeMenu {
|
||||
@MainActor final class FolderTreeMenu {
|
||||
|
||||
static func createFolderPopupMenu(with rootNode: Node, restrictToSpecialAccounts: Bool = false) -> NSMenu {
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import AppKit
|
||||
import Articles
|
||||
import Account
|
||||
|
||||
class AddFolderWindowController : NSWindowController {
|
||||
@MainActor final class AddFolderWindowController : NSWindowController {
|
||||
|
||||
@IBOutlet var folderNameTextField: NSTextField!
|
||||
@IBOutlet var accountPopupButton: NSPopUpButton!
|
||||
|
||||
@@ -12,7 +12,7 @@ import RSTree
|
||||
import Articles
|
||||
import Account
|
||||
|
||||
class AddRedditFeedWindowController : NSWindowController, AddFeedWindowController {
|
||||
@MainActor final class AddRedditFeedWindowController : NSWindowController, AddFeedWindowController {
|
||||
|
||||
@IBOutlet weak var typePopupButton: NSPopUpButton!
|
||||
@IBOutlet weak var typeDescriptionLabel: NSTextField!
|
||||
|
||||
@@ -15,7 +15,7 @@ enum ArticleExtractorButtonState {
|
||||
case off
|
||||
}
|
||||
|
||||
class ArticleExtractorButton: NSButton {
|
||||
@MainActor final class ArticleExtractorButton: NSButton {
|
||||
|
||||
public var rightClickAction: Selector?
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
import WebKit
|
||||
import Articles
|
||||
|
||||
class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
final class DetailIconSchemeHandler: NSObject, WKURLSchemeHandler {
|
||||
|
||||
var currentArticle: Article?
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import Articles
|
||||
|
||||
final class DetailStatusBarView: NSView {
|
||||
@MainActor final class DetailStatusBarView: NSView {
|
||||
|
||||
@IBOutlet var urlLabel: NSTextField!
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ enum DetailState: Equatable {
|
||||
case extracted(Article, ExtractedArticle, CGFloat?)
|
||||
}
|
||||
|
||||
final class DetailViewController: NSViewController, WKUIDelegate {
|
||||
@MainActor final class DetailViewController: NSViewController, WKUIDelegate {
|
||||
|
||||
@IBOutlet var containerView: DetailContainerView!
|
||||
@IBOutlet var statusBarView: DetailStatusBarView!
|
||||
|
||||
@@ -10,7 +10,7 @@ import AppKit
|
||||
import WebKit
|
||||
import RSCore
|
||||
|
||||
final class DetailWebView: WKWebView {
|
||||
@MainActor final class DetailWebView: WKWebView {
|
||||
|
||||
weak var keyboardDelegate: KeyboardDelegate?
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ protocol DetailWebViewControllerDelegate: AnyObject {
|
||||
func mouseDidExit(_: DetailWebViewController)
|
||||
}
|
||||
|
||||
final class DetailWebViewController: NSViewController {
|
||||
@MainActor final class DetailWebViewController: NSViewController {
|
||||
|
||||
weak var delegate: DetailWebViewControllerDelegate?
|
||||
var webView: DetailWebView!
|
||||
@@ -90,6 +90,7 @@ final class DetailWebViewController: NSViewController {
|
||||
configuration.defaultWebpagePreferences = webpagePrefs
|
||||
configuration.preferences = preferences
|
||||
configuration.setURLSchemeHandler(detailIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme)
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = .audio
|
||||
|
||||
let userContentController = WKUserContentController()
|
||||
userContentController.add(self, name: MessageName.windowDidScroll)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Foundation
|
||||
|
||||
class MainWindow: NSWindow {
|
||||
@MainActor class MainWindow: NSWindow {
|
||||
|
||||
override func sendEvent(_ event: NSEvent) {
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ enum TimelineSourceMode {
|
||||
case regular, search
|
||||
}
|
||||
|
||||
class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
@MainActor final class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
||||
|
||||
@IBOutlet weak var articleThemePopUpButton: NSPopUpButton?
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import Account
|
||||
|
||||
struct NNW3ImportController {
|
||||
@MainActor struct NNW3ImportController {
|
||||
|
||||
/// Import NNW3 subscriptions if they exist.
|
||||
/// Return true if Subscriptions.plist was found and subscriptions were imported.
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import Account
|
||||
|
||||
final class NNW3OpenPanelAccessoryViewController: NSViewController {
|
||||
@MainActor final class NNW3OpenPanelAccessoryViewController: NSViewController {
|
||||
|
||||
@IBOutlet weak var accountPopUpButton: NSPopUpButton!
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import Account
|
||||
|
||||
class ExportOPMLWindowController: NSWindowController {
|
||||
@MainActor final class ExportOPMLWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet weak var accountPopUpButton: NSPopUpButton!
|
||||
private weak var hostWindow: NSWindow?
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import Account
|
||||
|
||||
class ImportOPMLWindowController: NSWindowController {
|
||||
@MainActor final class ImportOPMLWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet weak var accountPopUpButton: NSPopUpButton!
|
||||
private weak var hostWindow: NSWindow?
|
||||
|
||||
@@ -11,7 +11,7 @@ import RSCore
|
||||
import Account
|
||||
import RSTree
|
||||
|
||||
class SidebarCell : NSTableCellView {
|
||||
@MainActor final class SidebarCell : NSTableCellView {
|
||||
|
||||
var iconImage: IconImage? {
|
||||
didSet {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
struct SidebarCellAppearance: Equatable {
|
||||
@MainActor struct SidebarCellAppearance: Equatable {
|
||||
|
||||
let imageSize: CGSize
|
||||
let imageMarginRight: CGFloat = 4.0
|
||||
|
||||
@@ -11,7 +11,7 @@ import RSCore
|
||||
|
||||
// image - title - unreadCount
|
||||
|
||||
struct SidebarCellLayout {
|
||||
@MainActor struct SidebarCellLayout {
|
||||
|
||||
let faviconRect: CGRect
|
||||
let titleRect: CGRect
|
||||
|
||||
@@ -13,7 +13,7 @@ protocol RenameWindowControllerDelegate {
|
||||
func renameWindowController(_ windowController: RenameWindowController, didRenameObject: Any, withNewName: String)
|
||||
}
|
||||
|
||||
final class RenameWindowController: NSWindowController {
|
||||
@MainActor final class RenameWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet var renamePrompt: NSTextField!
|
||||
@IBOutlet var newTitleTextField: NSTextField!
|
||||
|
||||
@@ -10,7 +10,7 @@ import AppKit
|
||||
import RSTree
|
||||
import Account
|
||||
|
||||
enum SidebarDeleteItemsAlert {
|
||||
@MainActor enum SidebarDeleteItemsAlert {
|
||||
|
||||
/// Builds a delete confirmation dialog for the supplied nodes
|
||||
static func build(_ nodes: [Node]) -> NSAlert {
|
||||
|
||||
@@ -492,7 +492,7 @@ private extension SidebarOutlineDataSource {
|
||||
return true
|
||||
}
|
||||
|
||||
func acceptSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardWebFeed, _ parentNode: Node, _ index: Int) -> Bool {
|
||||
@MainActor func acceptSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardWebFeed, _ parentNode: Node, _ index: Int) -> Bool {
|
||||
guard nodeIsDropTarget(parentNode), index == NSOutlineViewDropOnItemIndex else {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import AppKit
|
||||
import RSCore
|
||||
import RSTree
|
||||
|
||||
class SidebarOutlineView : NSOutlineView {
|
||||
@MainActor class SidebarOutlineView : NSOutlineView {
|
||||
|
||||
@IBOutlet var keyboardDelegate: KeyboardDelegate!
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ import Articles
|
||||
import RSWeb
|
||||
import Account
|
||||
|
||||
final class SidebarStatusBarView: NSView {
|
||||
@MainActor final class SidebarStatusBarView: NSView {
|
||||
|
||||
@IBOutlet var progressIndicator: NSProgressIndicator!
|
||||
@IBOutlet var progressLabel: NSTextField!
|
||||
|
||||
@@ -27,7 +27,7 @@ class UnreadCountView : NSView {
|
||||
}
|
||||
}
|
||||
var unreadCountString: String {
|
||||
return unreadCount < 1 ? "" : "\(unreadCount)"
|
||||
return unreadCount < 1 ? "" : numberFormatter.string(from: NSNumber(value: unreadCount))!
|
||||
}
|
||||
|
||||
private var intrinsicContentSizeIsValid = false
|
||||
@@ -92,5 +92,21 @@ class UnreadCountView : NSView {
|
||||
unreadCountString.draw(at: textRect().origin, withAttributes: Appearance.textAttributes)
|
||||
}
|
||||
}
|
||||
|
||||
var numberFormatter: NumberFormatter!
|
||||
|
||||
override init(frame frameRect: NSRect) {
|
||||
super.init(frame: frameRect)
|
||||
self.frame = frameRect
|
||||
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = Locale.current
|
||||
formatter.numberStyle = .decimal
|
||||
numberFormatter = formatter
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
final class TimelineContainerView: NSView {
|
||||
@MainActor final class TimelineContainerView: NSView {
|
||||
|
||||
private var contentViewConstraints: [NSLayoutConstraint]?
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ protocol TimelineContainerViewControllerDelegate: AnyObject {
|
||||
|
||||
}
|
||||
|
||||
final class TimelineContainerViewController: NSViewController {
|
||||
@MainActor final class TimelineContainerViewController: NSViewController {
|
||||
|
||||
@IBOutlet weak var viewOptionsPopUpButton: NSPopUpButton!
|
||||
@IBOutlet weak var newestToOldestMenuItem: NSMenuItem!
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
class TimelineTableRowView : NSTableRowView {
|
||||
@MainActor final class TimelineTableRowView : NSTableRowView {
|
||||
|
||||
private var separator: NSView?
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import RSCore
|
||||
|
||||
class TimelineTableView: NSTableView {
|
||||
@MainActor final class TimelineTableView: NSTableView {
|
||||
|
||||
weak var keyboardDelegate: KeyboardDelegate?
|
||||
|
||||
|
||||
@@ -157,7 +157,7 @@ private extension TimelineViewController {
|
||||
func menu(for articles: [Article]) -> NSMenu? {
|
||||
let menu = NSMenu(title: "")
|
||||
|
||||
if articles.anyArticleIsUnread() {
|
||||
if articles.anyArticleIsUnreadAndCanMarkRead() {
|
||||
menu.addItem(markReadMenuItem(articles))
|
||||
}
|
||||
if articles.anyArticleIsReadAndCanMarkUnread() {
|
||||
@@ -169,10 +169,10 @@ private extension TimelineViewController {
|
||||
if articles.anyArticleIsStarred() {
|
||||
menu.addItem(markUnstarredMenuItem(articles))
|
||||
}
|
||||
if let first = articles.first, self.articles.articlesAbove(article: first).canMarkAllAsRead() {
|
||||
if let first = articles.first, self.articles.articlesAbove(article: first).canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) {
|
||||
menu.addItem(markAboveReadMenuItem(articles))
|
||||
}
|
||||
if let last = articles.last, self.articles.articlesBelow(article: last).canMarkAllAsRead() {
|
||||
if let last = articles.last, self.articles.articlesBelow(article: last).canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles) {
|
||||
menu.addItem(markBelowReadMenuItem(articles))
|
||||
}
|
||||
|
||||
|
||||
@@ -124,6 +124,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
|
||||
directlyMarkedAsUnreadArticles = Set<Article>()
|
||||
lastVerticalPosition = 0
|
||||
articleRowMap = [String: [Int]]()
|
||||
tableView.reloadData()
|
||||
}
|
||||
@@ -194,6 +195,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
private let keyboardDelegate = TimelineKeyboardDelegate()
|
||||
private var timelineShowsSeparatorsObserver: NSKeyValueObservation?
|
||||
|
||||
private var markAsReadOnScrollWorkItem: DispatchWorkItem?
|
||||
private var markAsReadOnScrollStart: Int?
|
||||
private var markAsReadOnScrollEnd: Int?
|
||||
private var lastVerticalPosition: CGFloat = 0
|
||||
|
||||
convenience init(delegate: TimelineDelegate) {
|
||||
self.init(nibName: "TimelineTableView", bundle: nil)
|
||||
self.delegate = delegate
|
||||
@@ -224,6 +230,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidDirectMarking(_:)), name: .MarkStatusCommandDidDirectMarking, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(markStatusCommandDidUndoDirectMarking(_:)), name: .MarkStatusCommandDidUndoDirectMarking, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(scrollViewDidScroll), name: NSScrollView.didLiveScrollNotification, object: tableView.enclosingScrollView)
|
||||
didRegisterForNotifications = true
|
||||
}
|
||||
}
|
||||
@@ -235,6 +242,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
// MARK: - API
|
||||
|
||||
func markAllAsRead(completion: (() -> Void)? = nil) {
|
||||
markAllAsRead(articles, completion: completion)
|
||||
}
|
||||
|
||||
func markAllAsRead(_ articles: [Article], completion: (() -> Void)? = nil) {
|
||||
let markableArticles = Set(articles).subtracting(directlyMarkedAsUnreadArticles)
|
||||
guard let undoManager = undoManager,
|
||||
let markReadCommand = MarkStatusCommand(initialArticles: markableArticles,
|
||||
@@ -248,7 +259,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
}
|
||||
|
||||
func canMarkAllAsRead() -> Bool {
|
||||
return articles.canMarkAllAsRead()
|
||||
return articles.canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles)
|
||||
}
|
||||
|
||||
func canMarkSelectedArticlesAsRead() -> Bool {
|
||||
@@ -329,6 +340,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
let urlStrings = selectedArticles.compactMap { $0.preferredLink }
|
||||
Browser.open(urlStrings, fromWindow: self.view.window, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
|
||||
}
|
||||
|
||||
@objc func scrollViewDidScroll(notification: Notification) {
|
||||
markAsReadOnScroll()
|
||||
}
|
||||
|
||||
@IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) {
|
||||
guard !selectedArticles.isEmpty else {
|
||||
@@ -474,7 +489,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
func markReadCommandStatus() -> MarkCommandValidationStatus {
|
||||
let articles = selectedArticles
|
||||
|
||||
if articles.anyArticleIsUnread() {
|
||||
if articles.anyArticleIsUnreadAndCanMarkRead() {
|
||||
return .canMark
|
||||
}
|
||||
|
||||
@@ -499,12 +514,12 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
||||
|
||||
func canMarkAboveArticlesAsRead() -> Bool {
|
||||
guard let first = selectedArticles.first else { return false }
|
||||
return articles.articlesAbove(article: first).canMarkAllAsRead()
|
||||
return articles.articlesAbove(article: first).canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles)
|
||||
}
|
||||
|
||||
func canMarkBelowArticlesAsRead() -> Bool {
|
||||
guard let last = selectedArticles.last else { return false }
|
||||
return articles.articlesBelow(article: last).canMarkAllAsRead()
|
||||
return articles.articlesBelow(article: last).canMarkAllAsRead(exemptArticles: directlyMarkedAsUnreadArticles)
|
||||
}
|
||||
|
||||
func markOlderArticlesRead(_ selectedArticles: [Article]) {
|
||||
@@ -1326,4 +1341,51 @@ private extension TimelineViewController {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func markAsReadOnScroll() {
|
||||
guard AppDefaults.shared.markArticlesAsReadOnScroll else { return }
|
||||
|
||||
// Only try to mark if we are scrolling up
|
||||
defer {
|
||||
lastVerticalPosition = tableView.enclosingScrollView?.documentVisibleRect.origin.y ?? 0
|
||||
}
|
||||
guard lastVerticalPosition < tableView.enclosingScrollView?.documentVisibleRect.origin.y ?? 0 else {
|
||||
return
|
||||
}
|
||||
|
||||
// Make sure we are a little past the visible area so that marking isn't too touchy
|
||||
let firstVisibleRowIndex = tableView.rows(in: tableView.visibleRect).location
|
||||
guard let firstVisibleRowRect = tableView.rowView(atRow: firstVisibleRowIndex, makeIfNecessary: false)?.frame,
|
||||
tableView.convert(firstVisibleRowRect, to: tableView.enclosingScrollView).origin.y < tableView.safeAreaInsets.top - 20 else {
|
||||
return
|
||||
}
|
||||
|
||||
// We only mark immediately after scrolling stops, not during, to prevent scroll hitching
|
||||
markAsReadOnScrollWorkItem?.cancel()
|
||||
markAsReadOnScrollWorkItem = DispatchWorkItem { [weak self] in
|
||||
defer {
|
||||
self?.markAsReadOnScrollStart = nil
|
||||
self?.markAsReadOnScrollEnd = nil
|
||||
}
|
||||
|
||||
guard let start: Int = self?.markAsReadOnScrollStart,
|
||||
let end: Int = self?.markAsReadOnScrollEnd ?? self?.markAsReadOnScrollStart,
|
||||
start <= end,
|
||||
let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
let articles = self.articles[start...end].filter({ $0.status.read == false })
|
||||
self.markAllAsRead(articles)
|
||||
}
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3, execute: markAsReadOnScrollWorkItem!)
|
||||
|
||||
// Here we are creating a range of rows to attempt to mark later with the work item
|
||||
guard markAsReadOnScrollStart != nil else {
|
||||
markAsReadOnScrollStart = max(firstVisibleRowIndex - 5, 0)
|
||||
return
|
||||
}
|
||||
markAsReadOnScrollEnd = max(markAsReadOnScrollEnd ?? 0, firstVisibleRowIndex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import RSCore
|
||||
|
||||
extension URLPasteboardWriter {
|
||||
@MainActor extension URLPasteboardWriter {
|
||||
|
||||
/// Copy URL strings, alerting the user the first time the array of URL strings contains `nil`.
|
||||
/// - Parameters:
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
class AccountCell: NSTableCellView {
|
||||
@MainActor class AccountCell: NSTableCellView {
|
||||
|
||||
private var originalImage: NSImage?
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ enum AccountsAddCloudKitWindowControllerError: LocalizedError {
|
||||
}
|
||||
}
|
||||
|
||||
class AccountsAddCloudKitWindowController: NSWindowController {
|
||||
@MainActor class AccountsAddCloudKitWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet weak var limitationsAndSolutionsTextField: NSTextField!
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import Account
|
||||
|
||||
class AccountsAddLocalWindowController: NSWindowController {
|
||||
@MainActor class AccountsAddLocalWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet private weak var nameTextField: NSTextField!
|
||||
@IBOutlet private weak var localAccountNameTextField: NSTextField!
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import Account
|
||||
|
||||
final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate {
|
||||
@MainActor final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate {
|
||||
|
||||
@IBOutlet weak var typeLabel: NSTextField!
|
||||
@IBOutlet weak var nameTextField: NSTextField!
|
||||
|
||||
@@ -12,7 +12,7 @@ import RSCore
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
class AccountsFeedbinWindowController: NSWindowController, Logging {
|
||||
@MainActor class AccountsFeedbinWindowController: NSWindowController, Logging {
|
||||
|
||||
@IBOutlet weak var signInTextField: NSTextField!
|
||||
@IBOutlet weak var noAccountTextField: NSTextField!
|
||||
|
||||
@@ -12,7 +12,7 @@ import RSWeb
|
||||
import RSCore
|
||||
import Secrets
|
||||
|
||||
class AccountsNewsBlurWindowController: NSWindowController, Logging {
|
||||
@MainActor class AccountsNewsBlurWindowController: NSWindowController, Logging {
|
||||
|
||||
@IBOutlet weak var signInTextField: NSTextField!
|
||||
@IBOutlet weak var noAccountTextField: NSTextField!
|
||||
|
||||
@@ -17,7 +17,7 @@ protocol AccountsPreferencesAddAccountDelegate {
|
||||
}
|
||||
|
||||
// MARK: - AccountsPreferencesViewController
|
||||
final class AccountsPreferencesViewController: NSViewController {
|
||||
@MainActor final class AccountsPreferencesViewController: NSViewController {
|
||||
|
||||
@IBOutlet weak var tableView: NSTableView!
|
||||
@IBOutlet weak var detailView: NSView!
|
||||
|
||||
@@ -12,7 +12,7 @@ import RSWeb
|
||||
import RSCore
|
||||
import Secrets
|
||||
|
||||
class AccountsReaderAPIWindowController: NSWindowController, Logging {
|
||||
@MainActor class AccountsReaderAPIWindowController: NSWindowController, Logging {
|
||||
|
||||
@IBOutlet weak var titleImageView: NSImageView!
|
||||
@IBOutlet weak var titleLabel: NSTextField!
|
||||
|
||||
@@ -8,11 +8,10 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
final class AdvancedPreferencesViewController: NSViewController {
|
||||
@MainActor final class AdvancedPreferencesViewController: NSViewController {
|
||||
|
||||
@IBOutlet var releaseBuildsButton: NSButton!
|
||||
@IBOutlet var testBuildsButton: NSButton!
|
||||
@IBOutlet weak var privacyPolicyTextField: NSTextField!
|
||||
|
||||
let releaseBuildsURL = Bundle.main.infoDictionary!["SUFeedURL"]! as! String
|
||||
let testBuildsURL = Bundle.main.infoDictionary!["FeedURLForTestBuilds"]! as! String
|
||||
@@ -35,7 +34,6 @@ final class AdvancedPreferencesViewController: NSViewController {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
didRegisterForNotification = true
|
||||
}
|
||||
privacyPolicyTextField.attributedStringValue = AppAssets.privacyPolicyLink
|
||||
}
|
||||
|
||||
@IBAction func updateTypeButtonClicked(_ sender: Any?) {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import Cocoa
|
||||
|
||||
class ExtensionPointDetailViewController: NSViewController {
|
||||
@MainActor class ExtensionPointDetailViewController: NSViewController {
|
||||
|
||||
@IBOutlet weak var imageView: NSImageView!
|
||||
@IBOutlet weak var titleLabel: NSTextField!
|
||||
|
||||
@@ -11,7 +11,7 @@ import AuthenticationServices
|
||||
import OAuthSwift
|
||||
import Secrets
|
||||
|
||||
class ExtensionPointEnableWindowController: NSWindowController {
|
||||
@MainActor class ExtensionPointEnableWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet weak var imageView: NSImageView!
|
||||
@IBOutlet weak var titleLabel: NSTextField!
|
||||
|
||||
@@ -16,7 +16,7 @@ protocol ExtensionPointPreferencesEnabler: AnyObject {
|
||||
func enable(_ extensionPointType: ExtensionPoint.Type)
|
||||
}
|
||||
|
||||
final class ExtensionPointPreferencesViewController: NSViewController {
|
||||
@MainActor final class ExtensionPointPreferencesViewController: NSViewController {
|
||||
|
||||
@IBOutlet weak var tableView: NSTableView!
|
||||
@IBOutlet weak var detailView: NSView!
|
||||
|
||||
@@ -11,7 +11,7 @@ import RSCore
|
||||
import RSWeb
|
||||
import UserNotifications
|
||||
|
||||
final class GeneralPreferencesViewController: NSViewController {
|
||||
@MainActor final class GeneralPreferencesViewController: NSViewController {
|
||||
|
||||
private var userNotificationSettings: UNNotificationSettings?
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import AppKit
|
||||
import RSCore
|
||||
|
||||
final class PreferencesControlsBackgroundView: NSView {
|
||||
@MainActor final class PreferencesControlsBackgroundView: NSView {
|
||||
|
||||
private let lightModeFillColor = NSColor(white: 0.97, alpha: 1.0)
|
||||
private let darkModeFillColor = NSColor(red: 0.32, green: 0.34, blue: 0.35, alpha: 1.0)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import AppKit
|
||||
|
||||
final class PreferencesTableViewBackgroundView: NSView {
|
||||
@MainActor final class PreferencesTableViewBackgroundView: NSView {
|
||||
|
||||
let lightBorderColor = NSColor(white: 0.71, alpha: 1.0)
|
||||
let darkBorderColor = NSColor(red: 0.41, green: 0.43, blue: 0.44, alpha: 1.0)
|
||||
|
||||
@@ -28,7 +28,7 @@ private struct ToolbarItemIdentifier {
|
||||
static let Advanced = "Advanced"
|
||||
}
|
||||
|
||||
class PreferencesWindowController : NSWindowController, NSToolbarDelegate {
|
||||
@MainActor class PreferencesWindowController : NSWindowController, NSToolbarDelegate {
|
||||
|
||||
private let windowWidth = CGFloat(512.0) // Width is constant for all views; only the height changes
|
||||
private var viewControllers = [String: NSViewController]()
|
||||
|
||||
@@ -62,7 +62,7 @@
|
||||
<key>NSAppleScriptEnabled</key>
|
||||
<true/>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>Copyright © 2002-2022 Brent Simmons. All rights reserved.</string>
|
||||
<string>Copyright © 2002-2023 Brent Simmons. All rights reserved.</string>
|
||||
<key>NSMainStoryboardFile</key>
|
||||
<string>Main</string>
|
||||
<key>NSPrincipalClass</key>
|
||||
|
||||
@@ -39,7 +39,7 @@ extension AppDelegate : AppDelegateAppleEvents {
|
||||
NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(AppDelegate.getURL(_:_:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
|
||||
}
|
||||
|
||||
@objc func getURL(_ event: NSAppleEventDescriptor, _ withReplyEvent: NSAppleEventDescriptor) {
|
||||
@MainActor @objc func getURL(_ event: NSAppleEventDescriptor, _ withReplyEvent: NSAppleEventDescriptor) {
|
||||
|
||||
guard var urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue else {
|
||||
return
|
||||
@@ -63,7 +63,9 @@ extension AppDelegate : AppDelegateAppleEvents {
|
||||
do {
|
||||
try ArticleThemeDownloader.shared.handleFile(at: location)
|
||||
} catch {
|
||||
self.presentThemeImportError(error)
|
||||
Task { @MainActor in
|
||||
self.presentThemeImportError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
task.resume()
|
||||
|
||||
@@ -225,7 +225,6 @@
|
||||
51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */; };
|
||||
517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
|
||||
517630052336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; };
|
||||
517630232336657E00E15FFF /* WebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517630222336657E00E15FFF /* WebViewProvider.swift */; };
|
||||
5177C21227B07C9E00643901 /* NewsFax.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = DFD6AACB27ADE80900463FAD /* NewsFax.nnwtheme */; };
|
||||
5177C21327B07CFE00643901 /* NewsFax.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = DFD6AACB27ADE80900463FAD /* NewsFax.nnwtheme */; };
|
||||
5177C21427B07D1E00643901 /* NewsFax.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = DFD6AACB27ADE80900463FAD /* NewsFax.nnwtheme */; };
|
||||
@@ -377,7 +376,6 @@
|
||||
51DC07992552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; };
|
||||
51DC079A2552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; };
|
||||
51DC07AC255209E200A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; };
|
||||
51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */; };
|
||||
51DEE81226FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; };
|
||||
51DEE81326FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; };
|
||||
51DEE81426FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; };
|
||||
@@ -1268,7 +1266,6 @@
|
||||
516AE9DE2372269A007DEEAA /* IconImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImage.swift; sourceTree = "<group>"; };
|
||||
51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = "<group>"; };
|
||||
517630032336215100E15FFF /* main.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = main.js; sourceTree = "<group>"; };
|
||||
517630222336657E00E15FFF /* WebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProvider.swift; sourceTree = "<group>"; };
|
||||
517A745A2443665000B553B9 /* UIPageViewController-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIPageViewController-Extensions.swift"; sourceTree = "<group>"; };
|
||||
5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicLabel.swift; sourceTree = "<group>"; };
|
||||
5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicImageView.swift; sourceTree = "<group>"; };
|
||||
@@ -1342,7 +1339,6 @@
|
||||
51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = "<group>"; };
|
||||
51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = "<group>"; };
|
||||
51DC07972552083500A3F79F /* ArticleTextSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleTextSize.swift; sourceTree = "<group>"; };
|
||||
51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloadedWebView.swift; sourceTree = "<group>"; };
|
||||
51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Appanoose.nnwtheme; sourceTree = "<group>"; };
|
||||
51DEE81726FBFF84006DAA56 /* Promenade.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Promenade.nnwtheme; sourceTree = "<group>"; };
|
||||
51E36E70239D6610006F47A5 /* AddFeedSelectFolderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedSelectFolderTableViewCell.swift; sourceTree = "<group>"; };
|
||||
@@ -2184,9 +2180,7 @@
|
||||
518651D9235621840078E021 /* ImageTransition.swift */,
|
||||
5142192923522B5500E07E2C /* ImageViewController.swift */,
|
||||
512D554323C804DE0023FFFA /* OpenInSafariActivity.swift */,
|
||||
51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */,
|
||||
51AB8AB223B7F4C6008F147D /* WebViewController.swift */,
|
||||
517630222336657E00E15FFF /* WebViewProvider.swift */,
|
||||
51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */,
|
||||
D3A398632465054F00F9A366 /* FindInArticleActivity.swift */,
|
||||
D3555BF324664539005E48C3 /* ArticleSearchBar.swift */,
|
||||
@@ -4196,7 +4190,6 @@
|
||||
511B9807237DCAC90028BCAA /* UserInfoKey.swift in Sources */,
|
||||
51C45269226508F600C03939 /* MasterFeedTableViewCell.swift in Sources */,
|
||||
51F85BFD2275DCA800C787DC /* SingleLineUILabelSizer.swift in Sources */,
|
||||
517630232336657E00E15FFF /* WebViewProvider.swift in Sources */,
|
||||
51E43962238037C400015C31 /* AddFeedFolderViewController.swift in Sources */,
|
||||
51C4528F226509BD00C03939 /* UnreadFeed.swift in Sources */,
|
||||
51FD413B2342BD0500880194 /* MasterTimelineUnreadCountView.swift in Sources */,
|
||||
@@ -4323,7 +4316,6 @@
|
||||
51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */,
|
||||
514219372352510100E07E2C /* ImageScrollView.swift in Sources */,
|
||||
516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */,
|
||||
51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */,
|
||||
D3555BF524664566005E48C3 /* ArticleSearchBar.swift in Sources */,
|
||||
DF28B44D294ED52700C4D8CA /* View+DismissOnExternalContext.swift in Sources */,
|
||||
8454C3F3263F2D8700E3F9C7 /* IconImageCache.swift in Sources */,
|
||||
|
||||
@@ -40,10 +40,10 @@ class ArticleExtractor: Logging {
|
||||
|
||||
let clientURL = "https://extract.feedbin.com/parser"
|
||||
let username = SecretsManager.provider.mercuryClientId
|
||||
let signiture = articleLink.hmacUsingSHA1(key: SecretsManager.provider.mercuryClientSecret)
|
||||
let signature = articleLink.hmacUsingSHA1(key: SecretsManager.provider.mercuryClientSecret)
|
||||
|
||||
if let base64URL = articleLink.data(using: .utf8)?.base64EncodedString() {
|
||||
let fullURL = "\(clientURL)/\(username)/\(signiture)?base64_url=\(base64URL)"
|
||||
let fullURL = "\(clientURL)/\(username)/\(signature)?base64_url=\(base64URL)"
|
||||
if let url = URL(string: fullURL) {
|
||||
self.url = url
|
||||
return
|
||||
|
||||
@@ -36,24 +36,26 @@ a:hover {
|
||||
|
||||
:root {
|
||||
--header-table-border-color: rgba(0, 0, 0, 0.1);
|
||||
--header-color: rgba(0, 0, 0, 0.3);
|
||||
--body-code-color: #666;
|
||||
--header-color: rgba(0, 0, 0, 0.66);
|
||||
--body-code-color: #111;
|
||||
--code-background-color: #eee;
|
||||
--system-message-color: #cbcbcb;
|
||||
--feedlink-color: rgba(255, 0, 0, 0.6);
|
||||
--article-title-color: #333;
|
||||
--article-date-color: rgba(0, 0, 0, 0.3);
|
||||
--article-date-color: rgba(0, 0, 0, 0.5);
|
||||
--table-cell-border-color: lightgray;
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--header-color: rgba(94, 158, 244, 1);
|
||||
--body-code-color: #b2b2b2;
|
||||
--body-code-color: #dcdcdc;
|
||||
--system-message-color: #5f5f5f;
|
||||
--feedlink-color: rgba(94, 158, 244, 1);
|
||||
--article-title-color: #e0e0e0;
|
||||
--article-date-color: rgba(255, 255, 255, 0.5);
|
||||
--table-cell-border-color: dimgray;
|
||||
--code-background-color: #333;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,6 +108,8 @@ body > .systemMessage {
|
||||
.articleDateline {
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
font-variant-caps: all-small-caps;
|
||||
letter-spacing: 0.025em;
|
||||
}
|
||||
|
||||
.articleDateline a:link, .articleDateline a:visited {
|
||||
@@ -115,6 +119,7 @@ body > .systemMessage {
|
||||
.articleDatelineTitle {
|
||||
margin-bottom: 5px;
|
||||
font-weight: bold;
|
||||
font-variant-caps: all-small-caps;
|
||||
}
|
||||
|
||||
.articleDatelineTitle a:link, .articleDatelineTitle a:visited {
|
||||
@@ -122,19 +127,37 @@ body > .systemMessage {
|
||||
}
|
||||
|
||||
.externalLink {
|
||||
margin-bottom: 5px;
|
||||
margin-top: 15px;
|
||||
margin-bottom: 15px;
|
||||
/*
|
||||
font-variant-caps: all-small-caps;
|
||||
letter-spacing: 0.025em;
|
||||
*/
|
||||
font-size: 0.875em;
|
||||
font-style: italic;
|
||||
color: var(--article-date-color);
|
||||
width: 100%;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.externalLink a {
|
||||
font-family: "SF Mono", Menlo, Courier, monospace;
|
||||
font-size: 0.85em;
|
||||
font-variant-caps: normal;
|
||||
letter-spacing: 0em;
|
||||
}
|
||||
|
||||
|
||||
.articleBody {
|
||||
margin-top: 20px;
|
||||
line-height: 1.6em;
|
||||
}
|
||||
|
||||
.articleBody a {
|
||||
padding: 0px 1px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
line-height: 1.15em;
|
||||
font-weight: bold;
|
||||
@@ -149,6 +172,7 @@ pre {
|
||||
overflow-y: hidden;
|
||||
word-wrap: normal;
|
||||
word-break: normal;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
pre {
|
||||
@@ -156,9 +180,15 @@ pre {
|
||||
}
|
||||
|
||||
code, pre {
|
||||
font-family: "SF Mono", Menlo, "Courier New", Courier, monospace;
|
||||
font-family: "SF Mono", Menlo, Courier, monospace;
|
||||
font-size: 1em;
|
||||
-webkit-hyphens: none;
|
||||
background: var(--code-background-color);
|
||||
}
|
||||
|
||||
code {
|
||||
padding: 1px 2px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
pre code {
|
||||
@@ -219,10 +249,6 @@ img, figure, video, div, object {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
video {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
iframe {
|
||||
max-width: 100%;
|
||||
margin: 0 auto;
|
||||
@@ -238,7 +264,6 @@ figure {
|
||||
}
|
||||
|
||||
figcaption {
|
||||
margin-top: 0.5em;
|
||||
font-size: 14px;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
@@ -286,6 +311,30 @@ blockquote {
|
||||
border-top: 1px solid var(--header-table-border-color);
|
||||
}
|
||||
|
||||
/* Twitter */
|
||||
|
||||
.twitterAvatar {
|
||||
vertical-align: middle;
|
||||
border-radius: 4px;
|
||||
height: 1.7em;
|
||||
width: 1.7em;
|
||||
}
|
||||
|
||||
.twitterUsername {
|
||||
line-height: 1.2;
|
||||
margin-left: 4px;
|
||||
display: inline-block;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.twitterScreenName {
|
||||
font-size: 66%;
|
||||
}
|
||||
|
||||
.twitterTimestamp {
|
||||
font-size: 66%;
|
||||
}
|
||||
|
||||
/* Newsfoot theme for light mode (default) */
|
||||
.newsfoot-footnote-popover {
|
||||
background: #ccc;
|
||||
@@ -359,7 +408,6 @@ a.footnote:hover,
|
||||
padding-right: 20px;
|
||||
|
||||
word-break: break-word;
|
||||
-webkit-hyphens: auto;
|
||||
-webkit-text-size-adjust: none;
|
||||
}
|
||||
|
||||
@@ -370,7 +418,8 @@ a.footnote:hover,
|
||||
font-size: [[font-size]]px;
|
||||
--primary-accent-color: #086AEE;
|
||||
--secondary-accent-color: #086AEE;
|
||||
--block-quote-border-color: rgba(8, 106, 238, 0.75);
|
||||
--block-quote-border-color: rgba(0, 0, 0, 0.25);
|
||||
--ios-hover-color: lightgray; /* placeholder */
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: dark) {
|
||||
@@ -379,24 +428,44 @@ a.footnote:hover,
|
||||
--secondary-accent-color: #5E9EF4;
|
||||
--block-quote-border-color: rgba(94, 158, 244, 0.75);
|
||||
--header-table-border-color: rgba(255, 255, 255, 0.2);
|
||||
--ios-hover-color: #444444; /* placeholder */
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
body a, body a:visited, body a * {
|
||||
color: var(--secondary-accent-color);
|
||||
}
|
||||
|
||||
.externalLink a {
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.articleBody a:link, .articleBody a:visited {
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid var(--primary-accent-color);
|
||||
color: var(--secondary-accent-color);
|
||||
}
|
||||
|
||||
body .header {
|
||||
font: -apple-system-body;
|
||||
font-size: [[font-size]]px;
|
||||
}
|
||||
|
||||
body .header a:link, body .header a:visited {
|
||||
color: var(--primary-accent-color);
|
||||
color: var(--secondary-accent-color);
|
||||
}
|
||||
|
||||
@media (hover: hover) and (pointer: coarse) {
|
||||
.articleBody a:hover {
|
||||
background: var(--ios-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pre {
|
||||
/*
|
||||
border: 1px solid var(--secondary-accent-color);
|
||||
*/
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
@@ -439,15 +508,19 @@ a.footnote:hover,
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
--accent-color: rgba(8, 106, 238, 1);
|
||||
--block-quote-border-color: rgba(8, 106, 238, .50);
|
||||
--accent-color: rgba( 8, 106, 238, 1);
|
||||
--block-quote-border-color: rgba( 0, 0, 0, 0.25);
|
||||
--hover-gradient-color-start: rgba(60, 146, 251, 1);
|
||||
--hover-gradient-color-end: rgba(67, 149, 251, 1);
|
||||
}
|
||||
|
||||
@media(prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--accent-color: rgba(94, 158, 244, 1);
|
||||
--block-quote-border-color: rgba(94, 158, 244, .50);
|
||||
--header-table-border-color: rgba(255, 255, 255, 0.1);
|
||||
--accent-color: rgba( 94, 158, 244, 1);
|
||||
--block-quote-border-color: rgba( 94, 158, 244, 0.50);
|
||||
--header-table-border-color: rgba(255, 255, 255, 0.1);
|
||||
--hover-gradient-color-start: rgba( 41, 121, 213, 1);
|
||||
--hover-gradient-color-end: rgba( 42, 120, 212, 1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -455,8 +528,26 @@ a.footnote:hover,
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.articleBody a:link: not(a > img, a > code), .articleBody a:visited: not(a > img, a > code) {
|
||||
/* text-decoration: underline; */
|
||||
border-bottom: 1px solid var(--accent-color);
|
||||
}
|
||||
.articleBody a:hover {
|
||||
border-radius: 2px;
|
||||
/*
|
||||
background: var(--accent-color);
|
||||
*/
|
||||
background: linear-gradient(0deg, var(--hover-gradient-color-start) 0%, var(--hover-gradient-color-end) 100%);
|
||||
border-bottom: 1px solid var(--hover-gradient-color-end);
|
||||
color: white;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
|
||||
pre {
|
||||
/*
|
||||
border: 1px solid var(--accent-color);
|
||||
*/
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
|
||||
@@ -6,9 +6,17 @@
|
||||
// Copyright © 2015 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
#if os(macOS)
|
||||
import AppKit
|
||||
#else
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
import RSCore
|
||||
import Combine
|
||||
#if canImport(AppKit)
|
||||
import AppKit
|
||||
#endif
|
||||
|
||||
public extension Notification.Name {
|
||||
static let ArticleThemeNamesDidChangeNotification = Notification.Name("ArticleThemeNamesDidChangeNotification")
|
||||
@@ -21,9 +29,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable
|
||||
public let folderPath: String
|
||||
|
||||
lazy var presentedItemOperationQueue = OperationQueue.main
|
||||
var presentedItemURL: URL? {
|
||||
return URL(fileURLWithPath: folderPath)
|
||||
}
|
||||
var presentedItemURL: URL?
|
||||
|
||||
var currentThemeName: String {
|
||||
get {
|
||||
@@ -35,6 +41,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable
|
||||
currentTheme = try articleThemeWithThemeName(newValue)
|
||||
AppDefaults.shared.currentThemeName = newValue
|
||||
objectWillChange.send()
|
||||
updateFilePresenter()
|
||||
} catch {
|
||||
logger.error("Unable to set new theme: \(error.localizedDescription, privacy: .public)")
|
||||
}
|
||||
@@ -75,16 +82,22 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable
|
||||
assertionFailure("Could not create folder for Themes.")
|
||||
abort()
|
||||
}
|
||||
|
||||
NSFileCoordinator.addFilePresenter(self)
|
||||
|
||||
#if os(macOS)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: NSApplication.didBecomeActiveNotification, object: nil)
|
||||
#else
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil)
|
||||
#endif
|
||||
|
||||
updateFilePresenter()
|
||||
}
|
||||
|
||||
func presentedSubitemDidChange(at url: URL) {
|
||||
if url.lastPathComponent.localizedCaseInsensitiveContains("nnwtheme") {
|
||||
themeNames = buildThemeNames()
|
||||
do {
|
||||
currentTheme = try articleThemeWithThemeName(currentThemeName)
|
||||
} catch {
|
||||
themeNames = buildThemeNames()
|
||||
do {
|
||||
currentTheme = try articleThemeWithThemeName(currentThemeName)
|
||||
} catch {
|
||||
Task { @MainActor in
|
||||
appDelegate.presentThemeImportError(error)
|
||||
}
|
||||
}
|
||||
@@ -108,6 +121,8 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable
|
||||
|
||||
try FileManager.default.copyItem(atPath: filename, toPath: toFilename)
|
||||
objectWillChange.send()
|
||||
|
||||
themeNames = buildThemeNames()
|
||||
}
|
||||
|
||||
func articleThemeWithThemeName(_ themeName: String) throws -> ArticleTheme {
|
||||
@@ -129,15 +144,78 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable
|
||||
|
||||
return try ArticleTheme(path: path, isAppTheme: isAppTheme)
|
||||
}
|
||||
|
||||
func themesByDeveloper() -> (builtIn: [ArticleTheme], other: [ArticleTheme]) {
|
||||
let installedProvidedThemes = themeNames.map({ try? articleThemeWithThemeName($0) }).compactMap({ $0 }).filter({ $0.isAppTheme }).sorted(by: { $0.name < $1.name }).filter({ $0.name != AppDefaults.defaultThemeName })
|
||||
|
||||
let installedOtherThemes = themeNames.map({ try? articleThemeWithThemeName($0) }).compactMap({ $0 }).filter({ !$0.isAppTheme }).sorted(by: { $0.name < $1.name })
|
||||
|
||||
return (installedProvidedThemes, installedOtherThemes)
|
||||
}
|
||||
|
||||
#if os(macOS)
|
||||
func articleThemesMenu(for popUpButton: NSPopUpButton?) -> NSMenu {
|
||||
let menu = NSMenu()
|
||||
menu.autoenablesItems = false
|
||||
menu.removeAllItems()
|
||||
|
||||
let defaultMenuItem = NSMenuItem()
|
||||
defaultMenuItem.title = ArticleTheme.defaultTheme.name
|
||||
defaultMenuItem.action = #selector(updateThemeSelection(_:))
|
||||
defaultMenuItem.state = currentTheme.name == defaultMenuItem.title ? .on : .off
|
||||
defaultMenuItem.target = self
|
||||
menu.addItem(defaultMenuItem)
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
let rancheroHeading = NSMenuItem(title: "Built-in Themes", action: nil, keyEquivalent: "")
|
||||
rancheroHeading.attributedTitle = NSAttributedString(string: "Built-in Themes", attributes: [NSAttributedString.Key.foregroundColor : NSColor.secondaryLabelColor, NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 12)])
|
||||
rancheroHeading.isEnabled = false
|
||||
menu.addItem(rancheroHeading)
|
||||
|
||||
let installedThemes = ArticleThemesManager.shared.themesByDeveloper()
|
||||
|
||||
for theme in installedThemes.0 {
|
||||
let item = NSMenuItem()
|
||||
item.title = theme.name
|
||||
item.action = #selector(updateThemeSelection(_:))
|
||||
item.state = currentTheme.name == theme.name ? .on : .off
|
||||
item.target = self
|
||||
menu.addItem(item)
|
||||
}
|
||||
|
||||
menu.addItem(NSMenuItem.separator())
|
||||
|
||||
let thirdPartyHeading = NSMenuItem(title: "Other Themes", action: nil, keyEquivalent: "")
|
||||
thirdPartyHeading.attributedTitle = NSAttributedString(string: "Other Themes", attributes: [NSAttributedString.Key.foregroundColor : NSColor.secondaryLabelColor, NSAttributedString.Key.font: NSFont.boldSystemFont(ofSize: 12)])
|
||||
thirdPartyHeading.isEnabled = false
|
||||
menu.addItem(thirdPartyHeading)
|
||||
|
||||
for theme in installedThemes.1 {
|
||||
let item = NSMenuItem()
|
||||
item.title = theme.name
|
||||
item.action = #selector(updateThemeSelection(_:))
|
||||
item.state = currentTheme.name == theme.name ? .on : .off
|
||||
item.target = self
|
||||
menu.addItem(item)
|
||||
}
|
||||
popUpButton?.selectItem(withTitle: ArticleThemesManager.shared.currentThemeName)
|
||||
if popUpButton?.indexOfSelectedItem == -1 {
|
||||
popUpButton?.selectItem(withTitle: ArticleTheme.defaultTheme.name)
|
||||
}
|
||||
return menu
|
||||
}
|
||||
|
||||
@objc
|
||||
func updateThemeSelection(_ menuItem: NSMenuItem) {
|
||||
currentThemeName = menuItem.title
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
func deleteTheme(themeName: String) {
|
||||
if let filename = pathForThemeName(themeName, folder: folderPath) {
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: filename)
|
||||
} catch {
|
||||
logger.error("\(error.localizedDescription)")
|
||||
}
|
||||
|
||||
try? FileManager.default.removeItem(atPath: filename)
|
||||
themeNames = buildThemeNames()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,6 +224,19 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging, Observable
|
||||
// MARK : Private
|
||||
|
||||
private extension ArticleThemesManager {
|
||||
|
||||
@objc func applicationDidBecomeActive(_ note: Notification) {
|
||||
themeNames = buildThemeNames()
|
||||
}
|
||||
|
||||
func updateFilePresenter() {
|
||||
guard let currentThemePath = currentTheme.path else {
|
||||
return
|
||||
}
|
||||
NSFileCoordinator.removeFilePresenter(self)
|
||||
presentedItemURL = URL(fileURLWithPath: currentThemePath)
|
||||
NSFileCoordinator.addFilePresenter(self)
|
||||
}
|
||||
|
||||
func buildThemeNames() -> [String] {
|
||||
let appThemeFilenames = Bundle.main.paths(forResourcesOfType: ArticleTheme.nnwThemeSuffix, inDirectory: nil)
|
||||
|
||||
@@ -56,7 +56,7 @@ final class MarkStatusCommand: UndoableCommand {
|
||||
}
|
||||
|
||||
convenience init?(initialArticles: [Article], statusKey: ArticleStatus.Key, flag: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
|
||||
self.init(initialArticles: Set(initialArticles), statusKey: .read, flag: flag, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion)
|
||||
self.init(initialArticles: Set(initialArticles), statusKey: statusKey, flag: flag, directlyMarked: directlyMarked, undoManager: undoManager, completion: completion)
|
||||
}
|
||||
|
||||
convenience init?(initialArticles: Set<Article>, markingRead: Bool, directlyMarked: Bool, undoManager: UndoManager, completion: (() -> Void)? = nil) {
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
struct OPMLExporter {
|
||||
@MainActor struct OPMLExporter {
|
||||
|
||||
static func OPMLString(with account: Account, title: String) -> String {
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
import RSWeb
|
||||
import RSCore
|
||||
|
||||
struct CacheCleaner: Logging {
|
||||
@MainActor struct CacheCleaner: Logging {
|
||||
|
||||
static func purgeIfNecessary() {
|
||||
|
||||
|
||||
@@ -122,7 +122,7 @@ final class FaviconDownloader: Logging {
|
||||
let url = homePageURL.normalizedURL
|
||||
|
||||
if let url = URL(string: homePageURL) {
|
||||
if url.host == "nnw.ranchero.com" {
|
||||
if url.host == "nnw.ranchero.com" || url.host == "netnewswire.blog" {
|
||||
return IconImage.appIcon
|
||||
}
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ public final class WebFeedIconDownloader {
|
||||
return cachedImage
|
||||
}
|
||||
|
||||
if let hpURLString = feed.homePageURL, let hpURL = URL(string: hpURLString), hpURL.host == "nnw.ranchero.com" {
|
||||
if let hpURLString = feed.homePageURL, let hpURL = URL(string: hpURLString), hpURL.host == "nnw.ranchero.com" || hpURL.host == "netnewswire.blog" {
|
||||
return IconImage.appIcon
|
||||
}
|
||||
|
||||
|
||||
@@ -4,35 +4,20 @@
|
||||
<title>Default Feeds</title>
|
||||
</head>
|
||||
<body>
|
||||
<<<<<<< HEAD
|
||||
<outline text="Colossal" title="Colossal" type="rss" version="RSS" htmlUrl="https://www.thisiscolossal.com/" xmlUrl="https://www.thisiscolossal.com/feed/"/>
|
||||
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="https://beckyhansmeyer.com" xmlUrl="https://beckyhansmeyer.com/feed/"/>
|
||||
<outline text="Maurice Parker" title="Maurice Parker" type="rss" version="RSS" htmlUrl="https://vincode.io/" xmlUrl="https://vincode.io/feed.xml"/>
|
||||
<outline text="Maurice Parker" title="Maurice Parker" type="rss" version="RSS" htmlUrl="https://vincode.io/" xmlUrl="https://vincode.io/feed.xml"/>
|
||||
<outline text="Daring Fireball" title="Daring Fireball" type="rss" version="RSS" htmlUrl="https://daringfireball.net/" xmlUrl="https://daringfireball.net/feeds/json"/>
|
||||
<outline text="Manton Reece" title="Manton Reece" type="rss" version="RSS" htmlUrl="https://manton.org/" xmlUrl="https://www.manton.org/feed/json"/>
|
||||
<outline text="inessential" title="inessential" type="rss" version="RSS" htmlUrl="https://inessential.com/" xmlUrl="https://inessential.com/feed.json"/>
|
||||
<outline text="Julia Evans" title="Julia Evans" type="rss" version="RSS" htmlUrl="https://jvns.ca/" xmlUrl="https://jvns.ca/atom.xml"/>
|
||||
<outline text="Jason Kottke" title="Jason Kottke" type="rss" version="RSS" htmlUrl="https://kottke.org/" xmlUrl="http://feeds.kottke.org/json"/>
|
||||
<outline text="Six Colors" title="Six Colors" type="rss" version="RSS" htmlUrl="https://sixcolors.com/" xmlUrl="https://feedpress.me/sixcolors?type=xml"/>
|
||||
<outline text="inessential" title="inessential" type="rss" version="RSS" htmlUrl="https://inessential.com/" xmlUrl="https://inessential.com/feed.json"/>
|
||||
<outline text="NetNewsWire Blog" title="NetNewsWire Blog" type="rss" version="RSS" htmlUrl="https://nnw.ranchero.com/" xmlUrl="https://nnw.ranchero.com/feed.json"/>
|
||||
<outline text="Erica Sadun" title="Erica Sadun" type="rss" version="RSS" htmlUrl="https://ericasadun.com/" xmlUrl="https://ericasadun.com/feed/"/>
|
||||
<outline text="One Foot Tsunami" title="One Foot Tsunami" type="rss" version="RSS" htmlUrl="https://onefoottsunami.com/" xmlUrl="https://onefoottsunami.com/feed/json/"/>
|
||||
<outline text="Craig Hockenberry" title="Craig Hockenberry" type="rss" version="RSS" htmlUrl="https://furbo.org/" xmlUrl="https://furbo.org/feed/json"/>
|
||||
<outline text="Rose Orchard" title="Rose Orchard" type="rss" version="RSS" htmlUrl="https://rosemaryorchard.com/" xmlUrl="https://rosemaryorchard.com/feed.xml"/>
|
||||
<outline text="Michael Tsai" title="Michael Tsai" type="rss" version="RSS" htmlUrl="https://mjtsai.com/blog/" xmlUrl="https://mjtsai.com/blog/feed/"/>
|
||||
=======
|
||||
<outline text="BBC News - World" title="BBC News - World" type="rss" version="RSS" htmlUrl="https://www.bbc.com/news" xmlUrl="https://feeds.bbci.co.uk/news/world/rss.xml"/>
|
||||
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="https://beckyhansmeyer.com" xmlUrl="https://beckyhansmeyer.com/feed/"/>
|
||||
<outline text="Allen Pike" title="Allen Pike" type="rss" version="RSS" htmlUrl="https://www.allenpike.com/" xmlUrl="https://feeds.allenpike.com/feed/"/>
|
||||
<outline text="Becky Hansmeyer" title="Becky Hansmeyer" type="rss" version="RSS" htmlUrl="https://www.beckyhansmeyer.com/" xmlUrl="https://www.beckyhansmeyer.com/feed/"/>
|
||||
<outline text="Colossal" title="Colossal" type="rss" version="RSS" htmlUrl="https://www.thisiscolossal.com/" xmlUrl="https://www.thisiscolossal.com/feed/"/>
|
||||
<outline text="Craig Hockenberry" title="Craig Hockenberry" type="rss" version="RSS" htmlUrl="https://furbo.org/" xmlUrl="https://furbo.org/feed/json"/>
|
||||
<outline text="Daring Fireball" title="Daring Fireball" type="rss" version="RSS" htmlUrl="https://daringfireball.net/" xmlUrl="https://daringfireball.net/feeds/json"/>
|
||||
<outline text="inessential" title="inessential" type="rss" version="RSS" htmlUrl="https://inessential.com/" xmlUrl="https://inessential.com/feed.json"/>
|
||||
<outline text="Jason Kottke" title="Jason Kottke" type="rss" version="RSS" htmlUrl="https://kottke.org/" xmlUrl="http://feeds.kottke.org/json"/>
|
||||
<outline text="Julia Evans" title="Julia Evans" type="rss" version="RSS" htmlUrl="https://jvns.ca/" xmlUrl="https://jvns.ca/atom.xml"/>
|
||||
<outline text="Manton Reece" title="Manton Reece" type="rss" version="RSS" htmlUrl="https://manton.org/" xmlUrl="https://www.manton.org/feed/json"/>
|
||||
<outline text="Maurice Parker" title="Maurice Parker" type="rss" version="RSS" htmlUrl="https://vincode.io/" xmlUrl="https://vincode.io/feed.xml"/>
|
||||
<outline text="NetNewsWire Blog" title="NetNewsWire Blog" type="rss" version="RSS" htmlUrl="https://nnw.ranchero.com/" xmlUrl="https://nnw.ranchero.com/feed.json"/>
|
||||
<outline text="Michael Tsai" title="Michael Tsai" type="rss" version="RSS" htmlUrl="https://mjtsai.com/blog/" xmlUrl="https://mjtsai.com/blog/feed/"/>
|
||||
<outline text="NetNewsWire Blog" title="NetNewsWire Blog" type="rss" version="RSS" htmlUrl="https://netnewswire.blog/" xmlUrl="https://netnewswire.blog/feed.json"/>
|
||||
<outline text="One Foot Tsunami" title="One Foot Tsunami" type="rss" version="RSS" htmlUrl="https://onefoottsunami.com/" xmlUrl="https://onefoottsunami.com/feed/json/"/>
|
||||
<outline text="Six Colors" title="Six Colors" type="rss" version="RSS" htmlUrl="https://sixcolors.com/" xmlUrl="https://feedpress.me/sixcolors?type=xml"/>
|
||||
>>>>>>> ios-release
|
||||
</body>
|
||||
</opml>
|
||||
|
||||
@@ -10,7 +10,7 @@ import Foundation
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
struct DefaultFeedsImporter {
|
||||
@MainActor struct DefaultFeedsImporter {
|
||||
|
||||
static func importDefaultFeeds(account: Account) {
|
||||
let defaultFeedsURL = Bundle.main.url(forResource: "DefaultFeeds", withExtension: "opml")!
|
||||
|
||||
@@ -30,7 +30,7 @@ public struct OPMLDocument: FileDocument {
|
||||
self.account = account
|
||||
}
|
||||
|
||||
public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
|
||||
@MainActor public func fileWrapper(configuration: WriteConfiguration) throws -> FileWrapper {
|
||||
let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces)
|
||||
let filename = "Subscriptions-\(accountName).opml"
|
||||
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
|
||||
|
||||
@@ -2,15 +2,7 @@
|
||||
%{
|
||||
import os
|
||||
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
secrets = ['MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'TWITTER_CONSUMER_KEY', 'TWITTER_CONSUMER_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY']
|
||||
=======
|
||||
secrets = ['FEED_WRANGLER_KEY', 'MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY']
|
||||
>>>>>>> mac-release
|
||||
=======
|
||||
secrets = ['FEED_WRANGLER_KEY', 'MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY']
|
||||
>>>>>>> ios-release
|
||||
secrets = ['MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'REDDIT_CONSUMER_KEY', 'INOREADER_APP_ID', 'INOREADER_APP_KEY']
|
||||
|
||||
def chunks(seq, size):
|
||||
return (seq[i:(i + size)] for i in range(0, len(seq), size))
|
||||
|
||||
@@ -54,8 +54,8 @@ extension Array where Element == Article {
|
||||
return ArticleSorter.sortedByDate(articles: self, sortDirection: sortDirection, groupByFeed: groupByFeed)
|
||||
}
|
||||
|
||||
func canMarkAllAsRead() -> Bool {
|
||||
return anyArticleIsUnread()
|
||||
func canMarkAllAsRead(exemptArticles: Set<Article> = .init()) -> Bool {
|
||||
return anyArticleIsUnreadAndCanMarkRead(exemptArticles: exemptArticles)
|
||||
}
|
||||
|
||||
func anyArticlePassesTest(_ test: ((Article) -> Bool)) -> Bool {
|
||||
@@ -71,8 +71,8 @@ extension Array where Element == Article {
|
||||
return anyArticlePassesTest { $0.status.read && $0.isAvailableToMarkUnread }
|
||||
}
|
||||
|
||||
func anyArticleIsUnread() -> Bool {
|
||||
return anyArticlePassesTest { !$0.status.read }
|
||||
func anyArticleIsUnreadAndCanMarkRead(exemptArticles: Set<Article> = .init()) -> Bool {
|
||||
return anyArticlePassesTest { !(exemptArticles.contains($0) || $0.status.read) }
|
||||
}
|
||||
|
||||
func anyArticleIsStarred() -> Bool {
|
||||
|
||||
@@ -73,7 +73,7 @@ class AccountRefreshTimer {
|
||||
lastTimedRefresh = Date()
|
||||
update()
|
||||
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log, completion: nil)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,6 +11,6 @@ Add the NetNewsWire News Feed
|
||||
|
||||
The NetNewsWire News Feed is part of the default subscription list installed the first time you launched NetNewsWire. If it’s no longer in your list, it’s easy to add it back.
|
||||
|
||||
In the menu bar, select **Help → Add NetNewsWire News Feed**. NetNewsWire will open the [New Feed dialog](adding-feeds.html) pre-filled with the name and address (`https://nnw.ranchero.com/feed.json`). Click Add.
|
||||
In the menu bar, select **Help → Add NetNewsWire News Feed**. NetNewsWire will open the [New Feed dialog](adding-feeds.html) pre-filled with the name and address (`https://netnewswire.blog/feed.json`). Click Add.
|
||||
|
||||
Now you’ll always be up to date with what’s happening with NetNewsWire.
|
||||
Now you’ll always be up to date with what’s happening with NetNewsWire.
|
||||
|
||||
@@ -1,8 +1,29 @@
|
||||
# Mac Release Notes
|
||||
|
||||
## 6.1.2 build 6114 8 Apr 2023
|
||||
|
||||
Update default feeds to remove feeds that don’t appear to be active anymore (sadly!).
|
||||
|
||||
## 6.1.1 build 6112 13 Mar 2023
|
||||
|
||||
Revised Twitter removal warning to not mention any specific month. We’re holding this release until Twitter shuts down free access to its API.
|
||||
|
||||
## 6.1.1 build 6111 9 Feb 2023
|
||||
|
||||
Same as 6.1.1b4 but with updated build and version number.
|
||||
|
||||
### 6.1.1b4 build 6110 9 Feb 2023
|
||||
|
||||
Update the Twitter removal warning to say “later in February” instead of “February 9,” since Twitter postponed the removal date to the 13th and might postpone it further.
|
||||
|
||||
### 6.1.1b3 build 6109 6 Feb 2023
|
||||
|
||||
Update Safari extension icon (credit to Louie Mantia for the new icon)
|
||||
|
||||
### 6.1.1b2 build 6108 5 Feb 2023
|
||||
|
||||
Remove Twitter integration. On first launch, for people with Twitter feeds, display an alert explaining what happened
|
||||
|
||||
Fix a crashing bug that could happen in the sidebar
|
||||
|
||||
### 6.1.1b1 build 6107 3 Nov 2022
|
||||
|
||||
@@ -61,6 +61,7 @@ final class AppDefaults: ObservableObject {
|
||||
static let useSystemBrowser = "useSystemBrowser"
|
||||
static let currentThemeName = "currentThemeName"
|
||||
static let twitterDeprecationAlertShown = "twitterDeprecationAlertShown"
|
||||
static let markArticlesAsReadOnScroll = "markArticlesAsReadOnScroll"
|
||||
}
|
||||
|
||||
let isDeveloperBuild: Bool = {
|
||||
@@ -266,6 +267,16 @@ final class AppDefaults: ObservableObject {
|
||||
}
|
||||
}
|
||||
|
||||
var markArticlesAsReadOnScroll: Bool {
|
||||
get {
|
||||
return AppDefaults.bool(for: Key.markArticlesAsReadOnScroll)
|
||||
}
|
||||
set {
|
||||
AppDefaults.setBool(for: Key.markArticlesAsReadOnScroll, newValue)
|
||||
AppDefaults.shared.objectWillChange.send()
|
||||
}
|
||||
}
|
||||
|
||||
static func registerDefaults() {
|
||||
let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue,
|
||||
Key.timelineGroupByFeed: false,
|
||||
|
||||
+12
-12
@@ -57,7 +57,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
var isSyncArticleStatusRunning = false
|
||||
var isWaitingForSyncTasks = false
|
||||
|
||||
override init() {
|
||||
@MainActor override init() {
|
||||
super.init()
|
||||
appDelegate = self
|
||||
|
||||
@@ -76,7 +76,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
@MainActor func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
AppDefaults.registerDefaults()
|
||||
|
||||
let isFirstRun = AppDefaults.shared.isFirstRun
|
||||
@@ -127,7 +127,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
|
||||
}
|
||||
|
||||
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
@MainActor func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
self.resumeDatabaseProcessingIfNecessary()
|
||||
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) {
|
||||
@@ -137,17 +137,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
}
|
||||
}
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
@MainActor func applicationWillTerminate(_ application: UIApplication) {
|
||||
shuttingDown = true
|
||||
}
|
||||
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
@MainActor func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
IconImageCache.shared.emptyCache()
|
||||
}
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc func unreadCountDidChange(_ note: Notification) {
|
||||
@MainActor @objc func unreadCountDidChange(_ note: Notification) {
|
||||
if note.object is AccountManager {
|
||||
unreadCount = AccountManager.shared.unreadCount
|
||||
}
|
||||
@@ -155,21 +155,21 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
|
||||
// MARK: - API
|
||||
|
||||
func manualRefresh(errorHandler: @escaping (Error) -> ()) {
|
||||
@MainActor func manualRefresh(errorHandler: @escaping (Error) -> ()) {
|
||||
UIApplication.shared.connectedScenes.compactMap( { $0.delegate as? SceneDelegate } ).forEach {
|
||||
$0.cleanUp(conditional: true)
|
||||
}
|
||||
AccountManager.shared.refreshAll(errorHandler: errorHandler)
|
||||
}
|
||||
|
||||
func resumeDatabaseProcessingIfNecessary() {
|
||||
@MainActor func resumeDatabaseProcessingIfNecessary() {
|
||||
if AccountManager.shared.isSuspended {
|
||||
AccountManager.shared.resumeAll()
|
||||
logger.info("Application processing resumed.")
|
||||
}
|
||||
}
|
||||
|
||||
func prepareAccountsForBackground() {
|
||||
@MainActor func prepareAccountsForBackground() {
|
||||
extensionFeedAddRequestFile.suspend()
|
||||
syncTimer?.invalidate()
|
||||
scheduleBackgroundFeedRefresh()
|
||||
@@ -178,7 +178,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
waitForSyncTasksToFinish()
|
||||
}
|
||||
|
||||
func prepareAccountsForForeground() {
|
||||
@MainActor func prepareAccountsForForeground() {
|
||||
extensionFeedAddRequestFile.resume()
|
||||
syncTimer?.update()
|
||||
|
||||
@@ -218,7 +218,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
|
||||
}
|
||||
|
||||
func presentThemeImportError(_ error: Error) {
|
||||
@MainActor func presentThemeImportError(_ error: Error) {
|
||||
let windowScene = {
|
||||
let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
|
||||
return scenes.filter { $0.activationState == .foregroundActive }.first ?? scenes.first
|
||||
@@ -231,7 +231,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
|
||||
// MARK: App Initialization
|
||||
|
||||
private extension AppDelegate {
|
||||
@MainActor private extension AppDelegate {
|
||||
|
||||
private func initializeDownloaders() {
|
||||
let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
|
||||
|
||||
@@ -15,7 +15,7 @@ enum ArticleExtractorButtonState {
|
||||
case off
|
||||
}
|
||||
|
||||
class ArticleExtractorButton: UIButton {
|
||||
@MainActor class ArticleExtractorButton: UIButton {
|
||||
|
||||
private var animatedLayer: CALayer?
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
import Articles
|
||||
|
||||
class ContextMenuPreviewViewController: UIViewController {
|
||||
@MainActor class ContextMenuPreviewViewController: UIViewController {
|
||||
|
||||
@IBOutlet weak var blogNameLabel: UILabel!
|
||||
@IBOutlet weak var blogAuthorLabel: UILabel!
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
@MainActor final class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
|
||||
private weak var webViewController: WebViewController?
|
||||
private let duration = 0.4
|
||||
|
||||
@@ -25,6 +25,16 @@ class ImageViewController: UIViewController {
|
||||
return imageScrollView.zoomedFrame
|
||||
}
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return [
|
||||
UIKeyCommand(
|
||||
title: NSLocalizedString("Close Image", comment: "Close Image"),
|
||||
action: #selector(done(_:)),
|
||||
input: " "
|
||||
)
|
||||
]
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
@@ -31,14 +31,15 @@ class WebViewController: UIViewController {
|
||||
private var topShowBarsViewConstraint: NSLayoutConstraint!
|
||||
private var bottomShowBarsViewConstraint: NSLayoutConstraint!
|
||||
|
||||
var webView: PreloadedWebView? {
|
||||
return view.subviews[0] as? PreloadedWebView
|
||||
var webView: WKWebView? {
|
||||
return view.subviews[0] as? WKWebView
|
||||
}
|
||||
|
||||
private lazy var contextMenuInteraction = UIContextMenuInteraction(delegate: self)
|
||||
public var isFullScreenAvailable: Bool {
|
||||
return traitCollection.userInterfaceIdiom == .phone && coordinator.isRootSplitCollapsed
|
||||
}
|
||||
private lazy var articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator);
|
||||
private lazy var transition = ImageTransition(controller: self)
|
||||
private var clickedImageCompletion: (() -> Void)?
|
||||
|
||||
@@ -352,14 +353,6 @@ extension WebViewController: UIContextMenuInteractionDelegate {
|
||||
|
||||
extension WebViewController: WKNavigationDelegate {
|
||||
|
||||
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
||||
for (index, view) in view.subviews.enumerated() {
|
||||
if index != 0, let oldWebView = view as? PreloadedWebView {
|
||||
oldWebView.removeFromSuperview()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
|
||||
if navigationAction.navigationType == .linkActivated {
|
||||
@@ -517,49 +510,57 @@ private extension WebViewController {
|
||||
return
|
||||
}
|
||||
|
||||
coordinator.webViewProvider.dequeueWebView() { webView in
|
||||
|
||||
webView.ready {
|
||||
|
||||
// Add the webview
|
||||
webView.translatesAutoresizingMaskIntoConstraints = false
|
||||
self.view.insertSubview(webView, at: 0)
|
||||
NSLayoutConstraint.activate([
|
||||
self.view.leadingAnchor.constraint(equalTo: webView.leadingAnchor),
|
||||
self.view.trailingAnchor.constraint(equalTo: webView.trailingAnchor),
|
||||
self.view.topAnchor.constraint(equalTo: webView.topAnchor),
|
||||
self.view.bottomAnchor.constraint(equalTo: webView.bottomAnchor)
|
||||
])
|
||||
|
||||
// UISplitViewController reports the wrong size to WKWebView which can cause horizontal
|
||||
// rubberbanding on the iPad. This interferes with our UIPageViewController preventing
|
||||
// us from easily swiping between WKWebViews. This hack fixes that.
|
||||
webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: -1, bottom: 0, right: 0)
|
||||
|
||||
webView.scrollView.setZoomScale(1.0, animated: false)
|
||||
|
||||
self.view.setNeedsLayout()
|
||||
self.view.layoutIfNeeded()
|
||||
|
||||
// Configure the webview
|
||||
webView.navigationDelegate = self
|
||||
webView.uiDelegate = self
|
||||
webView.scrollView.delegate = self
|
||||
self.configureContextMenuInteraction()
|
||||
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked)
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown)
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.showFeedInspector)
|
||||
|
||||
self.renderPage(webView)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
let preferences = WKPreferences()
|
||||
preferences.javaScriptCanOpenWindowsAutomatically = false
|
||||
|
||||
/// The defaults for `preferredContentMode` and `allowsContentJavaScript` are suitable
|
||||
/// and don't need to be explicitly set.
|
||||
/// `allowsContentJavaScript` replaces `WKPreferences.javascriptEnabled`.
|
||||
let webpagePreferences = WKWebpagePreferences()
|
||||
|
||||
let configuration = WKWebViewConfiguration()
|
||||
configuration.defaultWebpagePreferences = webpagePreferences
|
||||
configuration.preferences = preferences
|
||||
configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs")
|
||||
configuration.allowsInlineMediaPlayback = true
|
||||
configuration.mediaTypesRequiringUserActionForPlayback = .audio
|
||||
if #available(iOS 15.4, *) {
|
||||
configuration.preferences.isElementFullscreenEnabled = true
|
||||
}
|
||||
configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme)
|
||||
|
||||
let webView = WKWebView(frame: self.view.bounds, configuration: configuration)
|
||||
webView.isOpaque = false;
|
||||
webView.backgroundColor = .clear;
|
||||
|
||||
// Add the webview - using autolayout will cause fullscreen video to fail and lose the web view
|
||||
webView.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
self.view.insertSubview(webView, at: 0)
|
||||
|
||||
// UISplitViewController reports the wrong size to WKWebView which can cause horizontal
|
||||
// rubberbanding on the iPad. This interferes with our UIPageViewController preventing
|
||||
// us from easily swiping between WKWebViews. This hack fixes that.
|
||||
webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: -1, bottom: 0, right: 0)
|
||||
|
||||
webView.scrollView.setZoomScale(1.0, animated: false)
|
||||
|
||||
self.view.setNeedsLayout()
|
||||
self.view.layoutIfNeeded()
|
||||
|
||||
// Configure the webview
|
||||
webView.navigationDelegate = self
|
||||
webView.uiDelegate = self
|
||||
webView.scrollView.delegate = self
|
||||
self.configureContextMenuInteraction()
|
||||
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked)
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown)
|
||||
webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.showFeedInspector)
|
||||
|
||||
self.renderPage(webView)
|
||||
}
|
||||
|
||||
func renderPage(_ webView: PreloadedWebView?) {
|
||||
func renderPage(_ webView: WKWebView?) {
|
||||
guard let webView = webView else { return }
|
||||
|
||||
let theme = ArticleThemesManager.shared.currentTheme
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
//
|
||||
// WebViewProvider.swift
|
||||
// NetNewsWire-iOS
|
||||
//
|
||||
// Created by Maurice Parker on 9/21/19.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RSCore
|
||||
import WebKit
|
||||
|
||||
/// WKWebView has an awful behavior of a flash to white on first load when in dark mode.
|
||||
/// Keep a queue of WebViews where we've already done a trivial load so that by the time we need them in the UI, they're past the flash-to-shite part of their lifecycle.
|
||||
class WebViewProvider: NSObject {
|
||||
|
||||
private let articleIconSchemeHandler: ArticleIconSchemeHandler
|
||||
private let operationQueue = MainThreadOperationQueue()
|
||||
private var queue = NSMutableArray()
|
||||
|
||||
init(coordinator: SceneCoordinator) {
|
||||
articleIconSchemeHandler = ArticleIconSchemeHandler(coordinator: coordinator)
|
||||
super.init()
|
||||
replenishQueueIfNeeded()
|
||||
}
|
||||
|
||||
func replenishQueueIfNeeded() {
|
||||
operationQueue.add(WebViewProviderReplenishQueueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler))
|
||||
}
|
||||
|
||||
func dequeueWebView(completion: @escaping (PreloadedWebView) -> ()) {
|
||||
operationQueue.add(WebViewProviderDequeueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler, completion: completion))
|
||||
operationQueue.add(WebViewProviderReplenishQueueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class WebViewProviderReplenishQueueOperation: MainThreadOperation {
|
||||
|
||||
// MainThreadOperation
|
||||
public var isCanceled = false
|
||||
public var id: Int?
|
||||
public weak var operationDelegate: MainThreadOperationDelegate?
|
||||
public var name: String? = "WebViewProviderReplenishQueueOperation"
|
||||
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||
|
||||
private let minimumQueueDepth = 3
|
||||
|
||||
private var queue: NSMutableArray
|
||||
private var articleIconSchemeHandler: ArticleIconSchemeHandler
|
||||
|
||||
init(queue: NSMutableArray, articleIconSchemeHandler: ArticleIconSchemeHandler) {
|
||||
self.queue = queue
|
||||
self.articleIconSchemeHandler = articleIconSchemeHandler
|
||||
}
|
||||
|
||||
func run() {
|
||||
while queue.count < minimumQueueDepth {
|
||||
let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler)
|
||||
webView.preload()
|
||||
queue.insert(webView, at: 0)
|
||||
}
|
||||
self.operationDelegate?.operationDidComplete(self)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class WebViewProviderDequeueOperation: MainThreadOperation {
|
||||
|
||||
// MainThreadOperation
|
||||
public var isCanceled = false
|
||||
public var id: Int?
|
||||
public weak var operationDelegate: MainThreadOperationDelegate?
|
||||
public var name: String? = "WebViewProviderFlushQueueOperation"
|
||||
public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock?
|
||||
|
||||
private var queue: NSMutableArray
|
||||
private var articleIconSchemeHandler: ArticleIconSchemeHandler
|
||||
private var completion: (PreloadedWebView) -> ()
|
||||
|
||||
init(queue: NSMutableArray, articleIconSchemeHandler: ArticleIconSchemeHandler, completion: @escaping (PreloadedWebView) -> ()) {
|
||||
self.queue = queue
|
||||
self.articleIconSchemeHandler = articleIconSchemeHandler
|
||||
self.completion = completion
|
||||
}
|
||||
|
||||
func run() {
|
||||
if let webView = queue.lastObject as? PreloadedWebView {
|
||||
self.completion(webView)
|
||||
self.queue.remove(webView)
|
||||
self.operationDelegate?.operationDidComplete(self)
|
||||
return
|
||||
}
|
||||
|
||||
assertionFailure("Creating PreloadedWebView in \(#function); queue has run dry.")
|
||||
|
||||
let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler)
|
||||
webView.preload()
|
||||
self.completion(webView)
|
||||
self.operationDelegate?.operationDidComplete(self)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -15,7 +15,7 @@ enum KeyboardType: String {
|
||||
case detail = "DetailKeyboardShortcuts"
|
||||
}
|
||||
|
||||
class KeyboardManager {
|
||||
@MainActor final class KeyboardManager {
|
||||
|
||||
private(set) var _keyCommands: [UIKeyCommand]
|
||||
var keyCommands: [UIKeyCommand] {
|
||||
|
||||
@@ -15,7 +15,7 @@ protocol MasterFeedTableViewCellDelegate: AnyObject {
|
||||
func masterFeedTableViewCellDisclosureDidToggle(_ sender: MasterFeedTableViewCell, expanding: Bool)
|
||||
}
|
||||
|
||||
class MasterFeedTableViewCell : VibrantTableViewCell {
|
||||
@MainActor final class MasterFeedTableViewCell : VibrantTableViewCell {
|
||||
|
||||
weak var delegate: MasterFeedTableViewCellDelegate?
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
import RSCore
|
||||
|
||||
struct MasterFeedTableViewCellLayout {
|
||||
@MainActor struct MasterFeedTableViewCellLayout {
|
||||
|
||||
private static let indentWidth = CGFloat(integerLiteral: 15)
|
||||
private static let editingControlIndent = CGFloat(integerLiteral: 40)
|
||||
|
||||
@@ -12,7 +12,7 @@ protocol MasterFeedTableViewSectionHeaderDelegate {
|
||||
func masterFeedTableViewSectionHeaderDisclosureDidToggle(_ sender: MasterFeedTableViewSectionHeader)
|
||||
}
|
||||
|
||||
class MasterFeedTableViewSectionHeader: UITableViewHeaderFooterView {
|
||||
@MainActor class MasterFeedTableViewSectionHeader: UITableViewHeaderFooterView {
|
||||
|
||||
var delegate: MasterFeedTableViewSectionHeaderDelegate?
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
import RSCore
|
||||
|
||||
struct MasterFeedTableViewSectionHeaderLayout {
|
||||
@MainActor struct MasterFeedTableViewSectionHeaderLayout {
|
||||
|
||||
private static let labelMarginRight = CGFloat(integerLiteral: 8)
|
||||
private static let disclosureButtonSize = CGSize(width: 44, height: 44)
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class MasterFeedUnreadCountView : UIView {
|
||||
@MainActor class MasterFeedUnreadCountView : UIView {
|
||||
|
||||
var padding: UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 1.0, left: 9.0, bottom: 1.0, right: 9.0)
|
||||
@@ -35,8 +35,10 @@ class MasterFeedUnreadCountView : UIView {
|
||||
}
|
||||
|
||||
var unreadCountString: String {
|
||||
return unreadCount < 1 ? "" : "\(unreadCount)"
|
||||
return unreadCount < 1 ? "" : numberFormatter.string(from: NSNumber(value: unreadCount))!
|
||||
}
|
||||
|
||||
var numberFormatter: NumberFormatter!
|
||||
|
||||
private var contentSizeIsValid = false
|
||||
private var _contentSize = CGSize.zero
|
||||
@@ -44,11 +46,21 @@ class MasterFeedUnreadCountView : UIView {
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
self.isOpaque = false
|
||||
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = Locale.current
|
||||
formatter.numberStyle = .decimal
|
||||
numberFormatter = formatter
|
||||
}
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
self.isOpaque = false
|
||||
|
||||
let formatter = NumberFormatter()
|
||||
formatter.locale = Locale.current
|
||||
formatter.numberStyle = .decimal
|
||||
numberFormatter = formatter
|
||||
}
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user