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:
Stuart Breckenridge
2023-05-27 17:00:42 +08:00
135 changed files with 1085 additions and 903 deletions
+6 -4
View File
@@ -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
View File
@@ -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, weve had to use the Twitter API. Without free access to that API, we cant read feeds from Twitter.</p>
<p>Weve left your Twitter feeds intact. If you have any starred items from those feeds, they will remain as long as you dont 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 wouldnt 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 arent 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 alls 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: theyre part of the app bundle and theyre 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 wouldnt 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 arent 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 wouldnt 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 $ wouldnt 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 dont show a spurious error</p>
<p>NewsBlur: dont 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 hasnt 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 wouldnt 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 wouldnt 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>
+18
View File
@@ -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, weve had to use the Twitter API. Without free access to that API, we cant read feeds from Twitter.</p>
<p>Weve left your Twitter feeds intact. If you have any starred items from those feeds, they will remain as long as you dont 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 itt not yet shared,
// Which is safe, because at creation time its not yet shared,
// and it wont be mutated ever on a background thread.
public final class ArticleStatus: Hashable {
+31 -22
View File
@@ -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
View File
@@ -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 couldnt 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() {
// Its possible theres 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 }
}
}
+84 -52
View File
@@ -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
View File
@@ -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 {
+1 -1
View File
@@ -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"
+2 -5
View File
@@ -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!
+1 -1
View File
@@ -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!
+1 -1
View File
@@ -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!
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -8,7 +8,7 @@
import Foundation
class MainWindow: NSWindow {
@MainActor class MainWindow: NSWindow {
override func sendEvent(_ event: NSEvent) {
+1 -1
View File
@@ -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!
+17 -1
View File
@@ -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:
+1 -1
View File
@@ -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]()
+1 -1
View File
@@ -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()
-8
View File
@@ -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
+111 -20
View File
@@ -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;
}
+108 -17
View File
@@ -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)
+1 -1
View File
@@ -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) {
+1 -1
View File
@@ -10,7 +10,7 @@ import Foundation
import Account
import RSCore
struct OPMLExporter {
@MainActor struct OPMLExporter {
static func OPMLString(with account: Account, title: String) -> String {
+1 -1
View File
@@ -10,7 +10,7 @@ import Foundation
import RSWeb
import RSCore
struct CacheCleaner: Logging {
@MainActor struct CacheCleaner: Logging {
static func purgeIfNecessary() {
+1 -1
View File
@@ -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
}
}
+1 -1
View File
@@ -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
}
+7 -22
View File
@@ -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>
+1 -1
View File
@@ -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")!
+1 -1
View File
@@ -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)
+1 -9
View File
@@ -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))
+4 -4
View File
@@ -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 {
+1 -1
View File
@@ -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 its no longer in your list, its 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 youll always be up to date with whats happening with NetNewsWire.
Now youll always be up to date with whats happening with NetNewsWire.
+21
View File
@@ -1,8 +1,29 @@
# Mac Release Notes
## 6.1.2 build 6114 8 Apr 2023
Update default feeds to remove feeds that dont appear to be active anymore (sadly!).
## 6.1.1 build 6112 13 Mar 2023
Revised Twitter removal warning to not mention any specific month. Were 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
+11
View File
@@ -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
View File
@@ -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!
+1 -1
View File
@@ -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!
+1 -1
View File
@@ -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
+10
View File
@@ -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()
+51 -50
View File
@@ -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
-103
View File
@@ -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)
}
}
+1 -1
View File
@@ -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