Fix lint issues.

This commit is contained in:
Brent Simmons 2025-01-22 22:17:02 -08:00
parent 8f1379360c
commit 40ada2ba5a
91 changed files with 1278 additions and 1350 deletions

View File

@ -23,11 +23,11 @@ struct AppAssets {
static var accountFeedbin: RSImage! = { static var accountFeedbin: RSImage! = {
return RSImage(named: "accountFeedbin") return RSImage(named: "accountFeedbin")
}() }()
static var accountFeedly: RSImage! = { static var accountFeedly: RSImage! = {
return RSImage(named: "accountFeedly") return RSImage(named: "accountFeedly")
}() }()
static var accountFreshRSS: RSImage! = { static var accountFreshRSS: RSImage! = {
return RSImage(named: "accountFreshRSS") return RSImage(named: "accountFreshRSS")
}() }()
@ -43,7 +43,7 @@ struct AppAssets {
static var accountNewsBlur: RSImage! = { static var accountNewsBlur: RSImage! = {
return RSImage(named: "accountNewsBlur") return RSImage(named: "accountNewsBlur")
}() }()
static var accountTheOldReader: RSImage! = { static var accountTheOldReader: RSImage! = {
return RSImage(named: "accountTheOldReader") return RSImage(named: "accountTheOldReader")
}() }()
@ -78,11 +78,11 @@ struct AppAssets {
static var marsEditIcon: RSImage = { static var marsEditIcon: RSImage = {
return RSImage(named: "MarsEditIcon")! return RSImage(named: "MarsEditIcon")!
}() }()
static var microblogIcon: RSImage = { static var microblogIcon: RSImage = {
return RSImage(named: "MicroblogIcon")! return RSImage(named: "MicroblogIcon")!
}() }()
static var faviconTemplateImage: RSImage = { static var faviconTemplateImage: RSImage = {
return RSImage(named: "faviconTemplateImage")! return RSImage(named: "faviconTemplateImage")!
}() }()
@ -102,39 +102,39 @@ struct AppAssets {
static var iconDarkBackgroundColor: NSColor = { static var iconDarkBackgroundColor: NSColor = {
return NSColor(named: NSColor.Name("iconDarkBackgroundColor"))! return NSColor(named: NSColor.Name("iconDarkBackgroundColor"))!
}() }()
static var legacyArticleExtractor: RSImage! = { static var legacyArticleExtractor: RSImage! = {
return RSImage(named: "legacyArticleExtractor") return RSImage(named: "legacyArticleExtractor")
}() }()
static var legacyArticleExtractorError: RSImage! = { static var legacyArticleExtractorError: RSImage! = {
return RSImage(named: "legacyArticleExtractorError") return RSImage(named: "legacyArticleExtractorError")
}() }()
static var legacyArticleExtractorInactiveDark: RSImage! = { static var legacyArticleExtractorInactiveDark: RSImage! = {
return RSImage(named: "legacyArticleExtractorInactiveDark") return RSImage(named: "legacyArticleExtractorInactiveDark")
}() }()
static var legacyArticleExtractorInactiveLight: RSImage! = { static var legacyArticleExtractorInactiveLight: RSImage! = {
return RSImage(named: "legacyArticleExtractorInactiveLight") return RSImage(named: "legacyArticleExtractorInactiveLight")
}() }()
static var legacyArticleExtractorProgress1: RSImage! = { static var legacyArticleExtractorProgress1: RSImage! = {
return RSImage(named: "legacyArticleExtractorProgress1") return RSImage(named: "legacyArticleExtractorProgress1")
}() }()
static var legacyArticleExtractorProgress2: RSImage! = { static var legacyArticleExtractorProgress2: RSImage! = {
return RSImage(named: "legacyArticleExtractorProgress2") return RSImage(named: "legacyArticleExtractorProgress2")
}() }()
static var legacyArticleExtractorProgress3: RSImage! = { static var legacyArticleExtractorProgress3: RSImage! = {
return RSImage(named: "legacyArticleExtractorProgress3") return RSImage(named: "legacyArticleExtractorProgress3")
}() }()
static var legacyArticleExtractorProgress4: RSImage! = { static var legacyArticleExtractorProgress4: RSImage! = {
return RSImage(named: "legacyArticleExtractorProgress4") return RSImage(named: "legacyArticleExtractorProgress4")
}() }()
static var folderImage: IconImage { static var folderImage: IconImage {
let image = NSImage(systemSymbolName: "folder", accessibilityDescription: nil)! let image = NSImage(systemSymbolName: "folder", accessibilityDescription: nil)!
let preferredColor = NSColor(named: "AccentColor")! let preferredColor = NSColor(named: "AccentColor")!
@ -157,15 +157,15 @@ struct AppAssets {
static var preferencesToolbarAccountsImage: RSImage = { static var preferencesToolbarAccountsImage: RSImage = {
return NSImage(systemSymbolName: "at", accessibilityDescription: nil)! return NSImage(systemSymbolName: "at", accessibilityDescription: nil)!
}() }()
static var preferencesToolbarExtensionsImage: RSImage = { static var preferencesToolbarExtensionsImage: RSImage = {
return RSImage(named: "preferencesToolbarExtensions")! return RSImage(named: "preferencesToolbarExtensions")!
}() }()
static var preferencesToolbarGeneralImage: RSImage = { static var preferencesToolbarGeneralImage: RSImage = {
return NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)! return NSImage(systemSymbolName: "gearshape", accessibilityDescription: nil)!
}() }()
static var preferencesToolbarAdvancedImage: RSImage = { static var preferencesToolbarAdvancedImage: RSImage = {
return NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: nil)! return NSImage(systemSymbolName: "gearshape.2", accessibilityDescription: nil)!
}() }()
@ -177,15 +177,15 @@ struct AppAssets {
static var readOpenImage: RSImage = { static var readOpenImage: RSImage = {
return NSImage(systemSymbolName: "circle", accessibilityDescription: nil)! return NSImage(systemSymbolName: "circle", accessibilityDescription: nil)!
}() }()
static var refreshImage: RSImage = { static var refreshImage: RSImage = {
return NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: nil)! return NSImage(systemSymbolName: "arrow.clockwise", accessibilityDescription: nil)!
}() }()
static var searchFeedImage: IconImage = { static var searchFeedImage: IconImage = {
return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!, isSymbol: true, isBackgroundSuppressed: true) return IconImage(RSImage(named: NSImage.smartBadgeTemplateName)!, isSymbol: true, isBackgroundSuppressed: true)
}() }()
static var shareImage: RSImage = { static var shareImage: RSImage = {
return NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: nil)! return NSImage(systemSymbolName: "square.and.arrow.up", accessibilityDescription: nil)!
}() }()
@ -193,7 +193,7 @@ struct AppAssets {
static var sidebarToggleImage: RSImage = { static var sidebarToggleImage: RSImage = {
return NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: nil)! return NSImage(systemSymbolName: "sidebar.left", accessibilityDescription: nil)!
}() }()
static var starClosedImage: RSImage = { static var starClosedImage: RSImage = {
return NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)! return NSImage(systemSymbolName: "star.fill", accessibilityDescription: nil)!
}() }()
@ -212,7 +212,7 @@ struct AppAssets {
static var timelineSeparatorColor: NSColor = { static var timelineSeparatorColor: NSColor = {
return NSColor(named: "timelineSeparatorColor")! return NSColor(named: "timelineSeparatorColor")!
}() }()
static var timelineStarSelected: RSImage! = { static var timelineStarSelected: RSImage! = {
return RSImage(named: "timelineStar")?.tinted(with: .white) return RSImage(named: "timelineStar")?.tinted(with: .white)
}() }()
@ -254,11 +254,11 @@ struct AppAssets {
return RSImage(systemSymbolName: "star", accessibilityDescription: "Unstar")! return RSImage(systemSymbolName: "star", accessibilityDescription: "Unstar")!
.withSymbolConfiguration(.init(scale: .large))! .withSymbolConfiguration(.init(scale: .large))!
}() }()
static var starColor: NSColor = { static var starColor: NSColor = {
return NSColor(named: NSColor.Name("StarColor"))! return NSColor(named: NSColor.Name("StarColor"))!
}() }()
static func image(for accountType: AccountType) -> NSImage? { static func image(for accountType: AccountType) -> NSImage? {
switch accountType { switch accountType {
case .onMyMac: case .onMyMac:
@ -281,5 +281,5 @@ struct AppAssets {
return AppAssets.accountTheOldReader return AppAssets.accountTheOldReader
} }
} }
} }

View File

@ -16,7 +16,7 @@ enum FontSize: Int {
} }
final class AppDefaults { final class AppDefaults {
static let defaultThemeName = "Default" static let defaultThemeName = "Default"
static let shared = AppDefaults() static let shared = AppDefaults()
@ -49,11 +49,11 @@ final class AppDefaults {
static let showTitleOnMainWindow = "KafasisTitleMode" static let showTitleOnMainWindow = "KafasisTitleMode"
static let feedDoubleClickMarkAsRead = "GruberFeedDoubleClickMarkAsRead" static let feedDoubleClickMarkAsRead = "GruberFeedDoubleClickMarkAsRead"
static let suppressSyncOnLaunch = "DevroeSuppressSyncOnLaunch" static let suppressSyncOnLaunch = "DevroeSuppressSyncOnLaunch"
static let webInspectorEnabled = "WebInspectorEnabled" static let webInspectorEnabled = "WebInspectorEnabled"
static let webInspectorStartsAttached = "__WebInspectorPageGroupLevel1__.WebKit2InspectorStartsAttached" static let webInspectorStartsAttached = "__WebInspectorPageGroupLevel1__.WebKit2InspectorStartsAttached"
} }
private static let smallestFontSizeRawValue = FontSize.small.rawValue private static let smallestFontSizeRawValue = FontSize.small.rawValue
private static let largestFontSizeRawValue = FontSize.veryLarge.rawValue private static let largestFontSizeRawValue = FontSize.veryLarge.rawValue
@ -63,7 +63,7 @@ final class AppDefaults {
} }
return false return false
}() }()
var isFirstRun: Bool = { var isFirstRun: Bool = {
if let _ = UserDefaults.standard.object(forKey: Key.firstRunDate) as? Date { if let _ = UserDefaults.standard.object(forKey: Key.firstRunDate) as? Date {
return false return false
@ -71,16 +71,16 @@ final class AppDefaults {
firstRunDate = Date() firstRunDate = Date()
return true return true
}() }()
var windowState: [AnyHashable : Any]? { var windowState: [AnyHashable: Any]? {
get { get {
return UserDefaults.standard.object(forKey: Key.windowState) as? [AnyHashable : Any] return UserDefaults.standard.object(forKey: Key.windowState) as? [AnyHashable: Any]
} }
set { set {
UserDefaults.standard.set(newValue, forKey: Key.windowState) UserDefaults.standard.set(newValue, forKey: Key.windowState)
} }
} }
var lastImageCacheFlushDate: Date? { var lastImageCacheFlushDate: Date? {
get { get {
return AppDefaults.date(for: Key.lastImageCacheFlushDate) return AppDefaults.date(for: Key.lastImageCacheFlushDate)
@ -89,7 +89,7 @@ final class AppDefaults {
AppDefaults.setDate(for: Key.lastImageCacheFlushDate, newValue) AppDefaults.setDate(for: Key.lastImageCacheFlushDate, newValue)
} }
} }
var openInBrowserInBackground: Bool { var openInBrowserInBackground: Bool {
get { get {
return AppDefaults.bool(for: Key.openInBrowserInBackground) return AppDefaults.bool(for: Key.openInBrowserInBackground)
@ -105,8 +105,7 @@ final class AppDefaults {
if let appGroupID = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String, if let appGroupID = Bundle.main.object(forInfoDictionaryKey: "AppGroup") as? String,
let appGroupDefaults = UserDefaults(suiteName: appGroupID) { let appGroupDefaults = UserDefaults(suiteName: appGroupID) {
return appGroupDefaults return appGroupDefaults
} } else {
else {
return UserDefaults.standard return UserDefaults.standard
} }
} }
@ -155,7 +154,7 @@ final class AppDefaults {
AppDefaults.setString(for: Key.addFeedAccountID, newValue) AppDefaults.setString(for: Key.addFeedAccountID, newValue)
} }
} }
var addFeedFolderName: String? { var addFeedFolderName: String? {
get { get {
return AppDefaults.string(for: Key.addFeedFolderName) return AppDefaults.string(for: Key.addFeedFolderName)
@ -173,7 +172,7 @@ final class AppDefaults {
AppDefaults.setString(for: Key.addFolderAccountID, newValue) AppDefaults.setString(for: Key.addFolderAccountID, newValue)
} }
} }
var importOPMLAccountID: String? { var importOPMLAccountID: String? {
get { get {
return AppDefaults.string(for: Key.importOPMLAccountID) return AppDefaults.string(for: Key.importOPMLAccountID)
@ -182,7 +181,7 @@ final class AppDefaults {
AppDefaults.setString(for: Key.importOPMLAccountID, newValue) AppDefaults.setString(for: Key.importOPMLAccountID, newValue)
} }
} }
var exportOPMLAccountID: String? { var exportOPMLAccountID: String? {
get { get {
return AppDefaults.string(for: Key.exportOPMLAccountID) return AppDefaults.string(for: Key.exportOPMLAccountID)
@ -200,7 +199,7 @@ final class AppDefaults {
AppDefaults.setString(for: Key.defaultBrowserID, newValue) AppDefaults.setString(for: Key.defaultBrowserID, newValue)
} }
} }
var currentThemeName: String? { var currentThemeName: String? {
get { get {
return AppDefaults.string(for: Key.currentThemeName) return AppDefaults.string(for: Key.currentThemeName)
@ -209,7 +208,7 @@ final class AppDefaults {
AppDefaults.setString(for: Key.currentThemeName, newValue) AppDefaults.setString(for: Key.currentThemeName, newValue)
} }
} }
var showTitleOnMainWindow: Bool { var showTitleOnMainWindow: Bool {
return AppDefaults.bool(for: Key.showTitleOnMainWindow) return AppDefaults.bool(for: Key.showTitleOnMainWindow)
} }
@ -235,7 +234,7 @@ final class AppDefaults {
AppDefaults.setBool(for: Key.suppressSyncOnLaunch, newValue) AppDefaults.setBool(for: Key.suppressSyncOnLaunch, newValue)
} }
} }
var webInspectorEnabled: Bool { var webInspectorEnabled: Bool {
get { get {
return AppDefaults.bool(for: Key.webInspectorEnabled) return AppDefaults.bool(for: Key.webInspectorEnabled)
@ -244,7 +243,7 @@ final class AppDefaults {
AppDefaults.setBool(for: Key.webInspectorEnabled, newValue) AppDefaults.setBool(for: Key.webInspectorEnabled, newValue)
} }
} }
var webInspectorStartsAttached: Bool { var webInspectorStartsAttached: Bool {
get { get {
return AppDefaults.bool(for: Key.webInspectorStartsAttached) return AppDefaults.bool(for: Key.webInspectorStartsAttached)
@ -253,7 +252,7 @@ final class AppDefaults {
AppDefaults.setBool(for: Key.webInspectorStartsAttached, newValue) AppDefaults.setBool(for: Key.webInspectorStartsAttached, newValue)
} }
} }
var timelineSortDirection: ComparisonResult { var timelineSortDirection: ComparisonResult {
get { get {
return AppDefaults.sortDirection(for: Key.timelineSortDirection) return AppDefaults.sortDirection(for: Key.timelineSortDirection)
@ -262,7 +261,7 @@ final class AppDefaults {
AppDefaults.setSortDirection(for: Key.timelineSortDirection, newValue) AppDefaults.setSortDirection(for: Key.timelineSortDirection, newValue)
} }
} }
var timelineGroupByFeed: Bool { var timelineGroupByFeed: Bool {
get { get {
return AppDefaults.bool(for: Key.timelineGroupByFeed) return AppDefaults.bool(for: Key.timelineGroupByFeed)
@ -271,7 +270,7 @@ final class AppDefaults {
AppDefaults.setBool(for: Key.timelineGroupByFeed, newValue) AppDefaults.setBool(for: Key.timelineGroupByFeed, newValue)
} }
} }
var timelineShowsSeparators: Bool { var timelineShowsSeparators: Bool {
return AppDefaults.bool(for: Key.timelineShowsSeparators) return AppDefaults.bool(for: Key.timelineShowsSeparators)
} }
@ -312,7 +311,7 @@ final class AppDefaults {
let showDebugMenu = false let showDebugMenu = false
#endif #endif
let defaults: [String : Any] = [ let defaults: [String: Any] = [
Key.sidebarFontSize: FontSize.medium.rawValue, Key.sidebarFontSize: FontSize.medium.rawValue,
Key.timelineFontSize: FontSize.medium.rawValue, Key.timelineFontSize: FontSize.medium.rawValue,
Key.detailFontSize: FontSize.medium.rawValue, Key.detailFontSize: FontSize.medium.rawValue,
@ -379,19 +378,19 @@ private extension AppDefaults {
// } // }
// return FontSize(rawValue: rawFontSize)! // return FontSize(rawValue: rawFontSize)!
} }
static func setFontSize(for key: String, _ fontSize: FontSize) { static func setFontSize(for key: String, _ fontSize: FontSize) {
setInt(for: key, fontSize.rawValue) setInt(for: key, fontSize.rawValue)
} }
static func string(for key: String) -> String? { static func string(for key: String) -> String? {
return UserDefaults.standard.string(forKey: key) return UserDefaults.standard.string(forKey: key)
} }
static func setString(for key: String, _ value: String?) { static func setString(for key: String, _ value: String?) {
UserDefaults.standard.set(value, forKey: key) UserDefaults.standard.set(value, forKey: key)
} }
static func bool(for key: String) -> Bool { static func bool(for key: String) -> Bool {
return UserDefaults.standard.bool(forKey: key) return UserDefaults.standard.bool(forKey: key)
} }
@ -403,11 +402,11 @@ private extension AppDefaults {
static func int(for key: String) -> Int { static func int(for key: String) -> Int {
return UserDefaults.standard.integer(forKey: key) return UserDefaults.standard.integer(forKey: key)
} }
static func setInt(for key: String, _ x: Int) { static func setInt(for key: String, _ x: Int) {
UserDefaults.standard.set(x, forKey: key) UserDefaults.standard.set(x, forKey: key)
} }
static func date(for key: String) -> Date? { static func date(for key: String) -> Date? {
return UserDefaults.standard.object(forKey: key) as? Date return UserDefaults.standard.object(forKey: key) as? Date
} }
@ -416,7 +415,7 @@ private extension AppDefaults {
UserDefaults.standard.set(date, forKey: key) UserDefaults.standard.set(date, forKey: key)
} }
static func sortDirection(for key:String) -> ComparisonResult { static func sortDirection(for key: String) -> ComparisonResult {
let rawInt = int(for: key) let rawInt = int(for: key)
if rawInt == ComparisonResult.orderedAscending.rawValue { if rawInt == ComparisonResult.orderedAscending.rawValue {
return .orderedAscending return .orderedAscending
@ -427,8 +426,7 @@ private extension AppDefaults {
static func setSortDirection(for key: String, _ value: ComparisonResult) { static func setSortDirection(for key: String, _ value: ComparisonResult) {
if value == .orderedAscending { if value == .orderedAscending {
setInt(for: key, ComparisonResult.orderedAscending.rawValue) setInt(for: key, ComparisonResult.orderedAscending.rawValue)
} } else {
else {
setInt(for: key, ComparisonResult.orderedDescending.rawValue) setInt(for: key, ComparisonResult.orderedDescending.rawValue)
} }
} }

View File

@ -22,24 +22,23 @@ import Sparkle
var appDelegate: AppDelegate! var appDelegate: AppDelegate!
@NSApplicationMain @NSApplicationMain
class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate {
{
private struct WindowRestorationIdentifiers { private struct WindowRestorationIdentifiers {
static let mainWindow = "mainWindow" static let mainWindow = "mainWindow"
} }
var userNotificationManager: UserNotificationManager! var userNotificationManager: UserNotificationManager!
var faviconDownloader: FaviconDownloader! var faviconDownloader: FaviconDownloader!
var extensionContainersFile: ExtensionContainersFile! var extensionContainersFile: ExtensionContainersFile!
var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile! var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile!
var appName: String! var appName: String!
var refreshTimer: AccountRefreshTimer? var refreshTimer: AccountRefreshTimer?
var syncTimer: ArticleStatusSyncTimer? var syncTimer: ArticleStatusSyncTimer?
var lastRefreshInterval = AppDefaults.shared.refreshInterval var lastRefreshInterval = AppDefaults.shared.refreshInterval
var shuttingDown = false { var shuttingDown = false {
didSet { didSet {
if shuttingDown { if shuttingDown {
@ -52,7 +51,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
} }
var isShutDownSyncDone = false var isShutDownSyncDone = false
@IBOutlet var debugMenuItem: NSMenuItem! @IBOutlet var debugMenuItem: NSMenuItem!
@IBOutlet var sortByOldestArticleOnTopMenuItem: NSMenuItem! @IBOutlet var sortByOldestArticleOnTopMenuItem: NSMenuItem!
@IBOutlet var sortByNewestArticleOnTopMenuItem: NSMenuItem! @IBOutlet var sortByNewestArticleOnTopMenuItem: NSMenuItem!
@ -81,7 +80,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
} }
return bestController return bestController
} }
private var mainWindowControllers = [MainWindowController]() private var mainWindowControllers = [MainWindowController]()
private var preferencesWindowController: NSWindowController? private var preferencesWindowController: NSWindowController?
private var addFeedController: AddFeedController? private var addFeedController: AddFeedController?
@ -127,20 +126,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
addFeedController = AddFeedController(hostWindow: window) addFeedController = AddFeedController(hostWindow: window)
addFeedController?.showAddFeedSheet(urlString, name, account, folder) addFeedController?.showAddFeedSheet(urlString, name, account, folder)
} }
// MARK: - NSApplicationDelegate // MARK: - NSApplicationDelegate
func applicationWillFinishLaunching(_ notification: Notification) { func applicationWillFinishLaunching(_ notification: Notification) {
installAppleEventHandlers() installAppleEventHandlers()
CacheCleaner.purgeIfNecessary() CacheCleaner.purgeIfNecessary()
// Try to establish a cache in the Caches folder, but if it fails for some reason fall back to a temporary dir // 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 let cacheFolder: String
if let userCacheFolder = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path { if let userCacheFolder = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path {
cacheFolder = userCacheFolder cacheFolder = userCacheFolder
} } else {
else {
let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String) let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String)
cacheFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier) cacheFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier)
} }
@ -156,7 +154,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
appName = (Bundle.main.infoDictionary!["CFBundleExecutable"]! as! String) appName = (Bundle.main.infoDictionary!["CFBundleExecutable"]! as! String)
} }
func applicationDidFinishLaunching(_ note: Notification) { func applicationDidFinishLaunching(_ note: Notification) {
// Initialize Sparkle... // Initialize Sparkle...
@ -166,8 +164,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
do { do {
try self.softwareUpdater.start() try self.softwareUpdater.start()
} } catch {
catch {
NSLog("Failed to start software updater with error: \(error)") NSLog("Failed to start software updater with error: \(error)")
} }
@ -187,12 +184,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
updateSortMenuItems() updateSortMenuItems()
updateGroupByFeedMenuItem() updateGroupByFeedMenuItem()
if mainWindowController == nil { if mainWindowController == nil {
let mainWindowController = createAndShowMainWindow() let mainWindowController = createAndShowMainWindow()
mainWindowController.restoreStateFromUserDefaults() mainWindowController.restoreStateFromUserDefaults()
} }
if isFirstRun { if isFirstRun {
mainWindowController?.window?.center() mainWindowController?.window?.center()
} }
@ -213,8 +210,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
refreshTimer = AccountRefreshTimer() refreshTimer = AccountRefreshTimer()
syncTimer = ArticleStatusSyncTimer() syncTimer = ArticleStatusSyncTimer()
UNUserNotificationCenter.current().requestAuthorization(options:[.badge]) { (granted, error) in } UNUserNotificationCenter.current().requestAuthorization(options: [.badge]) { (_, _) in }
UNUserNotificationCenter.current().getNotificationSettings { (settings) in UNUserNotificationCenter.current().getNotificationSettings { (settings) in
if settings.authorizationStatus == .authorized { if settings.authorizationStatus == .authorized {
@ -241,7 +238,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
} }
} }
#endif #endif
if !AppDefaults.shared.showDebugMenu { if !AppDefaults.shared.showDebugMenu {
debugMenuItem.menu?.removeItem(debugMenuItem) debugMenuItem.menu?.removeItem(debugMenuItem)
} }
@ -250,7 +247,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
CrashReporter.check(crashReporter: self.crashReporter) CrashReporter.check(crashReporter: self.crashReporter)
} }
} }
func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool { func application(_ application: NSApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([NSUserActivityRestoring]) -> Void) -> Bool {
guard let mainWindowController = mainWindowController else { guard let mainWindowController = mainWindowController else {
return false return false
@ -276,32 +273,32 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
func applicationDidBecomeActive(_ notification: Notification) { func applicationDidBecomeActive(_ notification: Notification) {
fireOldTimers() fireOldTimers()
} }
func applicationDidResignActive(_ notification: Notification) { func applicationDidResignActive(_ notification: Notification) {
ArticleStringFormatter.emptyCaches() ArticleStringFormatter.emptyCaches()
saveState() saveState()
} }
func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) { func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String: Any]) {
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) AccountManager.shared.receiveRemoteNotification(userInfo: userInfo)
} }
func application(_ sender: NSApplication, openFile filename: String) -> Bool { func application(_ sender: NSApplication, openFile filename: String) -> Bool {
guard filename.hasSuffix(ArticleTheme.nnwThemeSuffix) else { return false } guard filename.hasSuffix(ArticleTheme.nnwThemeSuffix) else { return false }
importTheme(filename: filename) importTheme(filename: filename)
return true return true
} }
func applicationWillTerminate(_ notification: Notification) { func applicationWillTerminate(_ notification: Notification) {
shuttingDown = true shuttingDown = true
saveState() saveState()
ArticleThemeDownloader.shared.cleanUp() ArticleThemeDownloader.shared.cleanUp()
AccountManager.shared.sendArticleStatusAll() { AccountManager.shared.sendArticleStatusAll {
self.isShutDownSyncDone = true self.isShutDownSyncDone = true
} }
let timeout = Date().addingTimeInterval(2) let timeout = Date().addingTimeInterval(2)
while !isShutDownSyncDone && RunLoop.current.run(mode: .default, before: timeout) && timeout > Date() { } while !isShutDownSyncDone && RunLoop.current.run(mode: .default, before: timeout) && timeout > Date() { }
} }
@ -318,7 +315,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
return return
} }
if key == Feed.FeedSettingKey.homePageURL || key == Feed.FeedSettingKey.faviconURL { if key == Feed.FeedSettingKey.homePageURL || key == Feed.FeedSettingKey.faviconURL {
let _ = faviconDownloader.favicon(for: feed) _ = faviconDownloader.favicon(for: feed)
} }
} }
@ -332,19 +329,19 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
@objc func userDefaultsDidChange(_ note: Notification) { @objc func userDefaultsDidChange(_ note: Notification) {
updateSortMenuItems() updateSortMenuItems()
updateGroupByFeedMenuItem() updateGroupByFeedMenuItem()
if lastRefreshInterval != AppDefaults.shared.refreshInterval { if lastRefreshInterval != AppDefaults.shared.refreshInterval {
refreshTimer?.update() refreshTimer?.update()
lastRefreshInterval = AppDefaults.shared.refreshInterval lastRefreshInterval = AppDefaults.shared.refreshInterval
} }
updateDockBadge() updateDockBadge()
} }
@objc func didWakeNotification(_ note: Notification) { @objc func didWakeNotification(_ note: Notification) {
fireOldTimers() fireOldTimers()
} }
@objc func importDownloadedTheme(_ note: Notification) { @objc func importDownloadedTheme(_ note: Notification) {
guard let userInfo = note.userInfo, guard let userInfo = note.userInfo,
let url = userInfo["url"] as? URL else { let url = userInfo["url"] as? URL else {
@ -356,7 +353,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
} }
// MARK: Main Window // MARK: Main Window
func createMainWindowController() -> MainWindowController { func createMainWindowController() -> MainWindowController {
let controller: MainWindowController = windowControllerWithName("UnifiedWindow") as! MainWindowController let controller: MainWindowController = windowControllerWithName("UnifiedWindow") as! MainWindowController
@ -376,12 +373,12 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
func createAndShowMainWindow() -> MainWindowController { func createAndShowMainWindow() -> MainWindowController {
let controller = createMainWindowController() let controller = createMainWindowController()
controller.showWindow(self) controller.showWindow(self)
if let window = controller.window { if let window = controller.window {
window.restorationClass = Self.self window.restorationClass = Self.self
window.identifier = NSUserInterfaceItemIdentifier(rawValue: WindowRestorationIdentifiers.mainWindow) window.identifier = NSUserInterfaceItemIdentifier(rawValue: WindowRestorationIdentifiers.mainWindow)
} }
return controller return controller
} }
@ -399,7 +396,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
mainWindowControllers.remove(at: index) mainWindowControllers.remove(at: index)
} }
} }
// MARK: NSUserInterfaceValidations // MARK: NSUserInterfaceValidations
func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
if shuttingDown { if shuttingDown {
@ -411,23 +408,23 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
if item.action == #selector(refreshAll(_:)) { if item.action == #selector(refreshAll(_:)) {
return !AccountManager.shared.refreshInProgress && !AccountManager.shared.activeAccounts.isEmpty return !AccountManager.shared.refreshInProgress && !AccountManager.shared.activeAccounts.isEmpty
} }
if item.action == #selector(importOPMLFromFile(_:)) { if item.action == #selector(importOPMLFromFile(_:)) {
return AccountManager.shared.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) }) return AccountManager.shared.activeAccounts.contains(where: { !$0.behaviors.contains(where: { $0 == .disallowOPMLImports }) })
} }
if item.action == #selector(addAppNews(_:)) { if item.action == #selector(addAppNews(_:)) {
return !isDisplayingSheet && !AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() && !AccountManager.shared.activeAccounts.isEmpty return !isDisplayingSheet && !AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() && !AccountManager.shared.activeAccounts.isEmpty
} }
if item.action == #selector(sortByNewestArticleOnTop(_:)) || item.action == #selector(sortByOldestArticleOnTop(_:)) { if item.action == #selector(sortByNewestArticleOnTop(_:)) || item.action == #selector(sortByOldestArticleOnTop(_:)) {
return mainWindowController?.isOpen ?? false return mainWindowController?.isOpen ?? false
} }
if item.action == #selector(showAddFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) { if item.action == #selector(showAddFeedWindow(_:)) || item.action == #selector(showAddFolderWindow(_:)) {
return !isDisplayingSheet && !AccountManager.shared.activeAccounts.isEmpty return !isDisplayingSheet && !AccountManager.shared.activeAccounts.isEmpty
} }
if item.action == #selector(toggleWebInspectorEnabled(_:)) { if item.action == #selector(toggleWebInspectorEnabled(_:)) {
(item as! NSMenuItem).state = AppDefaults.shared.webInspectorEnabled ? .on : .off (item as! NSMenuItem).state = AppDefaults.shared.webInspectorEnabled ? .on : .off
} }
@ -436,15 +433,15 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
} }
// MARK: UNUserNotificationCenterDelegate // MARK: UNUserNotificationCenterDelegate
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.banner, .badge, .sound]) completionHandler([.banner, .badge, .sound])
} }
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
let userInfo = response.notification.request.content.userInfo let userInfo = response.notification.request.content.userInfo
switch response.actionIdentifier { switch response.actionIdentifier {
case "MARK_AS_READ": case "MARK_AS_READ":
handleMarkAsRead(userInfo: userInfo) handleMarkAsRead(userInfo: userInfo)
@ -455,11 +452,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
} }
completionHandler() completionHandler()
} }
// MARK: Add Feed // MARK: Add Feed
func addFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) { func addFeed(_ urlString: String?, name: String? = nil, account: Account? = nil, folder: Folder? = nil) {
createAndShowMainWindowIfNecessary() createAndShowMainWindowIfNecessary()
if mainWindowController!.isDisplayingSheet { if mainWindowController!.isDisplayingSheet {
return return
} }
@ -506,7 +503,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
@IBAction func showKeyboardShortcutsWindow(_ sender: Any?) { @IBAction func showKeyboardShortcutsWindow(_ sender: Any?) {
if keyboardShortcutsWindowController == nil { if keyboardShortcutsWindowController == nil {
keyboardShortcutsWindowController = WebViewWindowController(title: NSLocalizedString("Keyboard Shortcuts", comment: "window title")) keyboardShortcutsWindowController = WebViewWindowController(title: NSLocalizedString("Keyboard Shortcuts", comment: "window title"))
let htmlFile = Bundle(for: type(of: self)).path(forResource: "KeyboardShortcuts", ofType: "html")! let htmlFile = Bundle(for: type(of: self)).path(forResource: "KeyboardShortcuts", ofType: "html")!
keyboardShortcutsWindowController?.displayContents(of: htmlFile) keyboardShortcutsWindowController?.displayContents(of: htmlFile)
@ -517,7 +514,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
let minSize = NSSize(width: 400, height: 400) let minSize = NSSize(width: 400, height: 400)
window.setPointAndSizeAdjustingForScreen(point: point, size: size, minimumSize: minSize) window.setPointAndSizeAdjustingForScreen(point: point, size: size, minimumSize: minSize)
} }
} }
keyboardShortcutsWindowController!.showWindow(self) keyboardShortcutsWindowController!.showWindow(self)
@ -530,8 +527,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
if inspectorWindowController!.isOpen { if inspectorWindowController!.isOpen {
inspectorWindowController!.window!.performClose(self) inspectorWindowController!.window!.performClose(self)
} } else {
else {
inspectorWindowController!.objects = objectsForInspector() inspectorWindowController!.objects = objectsForInspector()
inspectorWindowController!.showWindow(self) inspectorWindowController!.showWindow(self)
} }
@ -542,11 +538,11 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
if mainWindowController!.isDisplayingSheet { if mainWindowController!.isDisplayingSheet {
return return
} }
importOPMLController = ImportOPMLWindowController() importOPMLController = ImportOPMLWindowController()
importOPMLController?.runSheetOnWindow(mainWindowController!.window!) importOPMLController?.runSheetOnWindow(mainWindowController!.window!)
} }
@IBAction func importNNW3FromFile(_ sender: Any?) { @IBAction func importNNW3FromFile(_ sender: Any?) {
createAndShowMainWindowIfNecessary() createAndShowMainWindowIfNecessary()
if mainWindowController!.isDisplayingSheet { if mainWindowController!.isDisplayingSheet {
@ -554,17 +550,17 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
} }
NNW3ImportController.askUserToImportNNW3Subscriptions(window: mainWindowController!.window!) NNW3ImportController.askUserToImportNNW3Subscriptions(window: mainWindowController!.window!)
} }
@IBAction func exportOPML(_ sender: Any?) { @IBAction func exportOPML(_ sender: Any?) {
createAndShowMainWindowIfNecessary() createAndShowMainWindowIfNecessary()
if mainWindowController!.isDisplayingSheet { if mainWindowController!.isDisplayingSheet {
return return
} }
exportOPMLController = ExportOPMLWindowController() exportOPMLController = ExportOPMLWindowController()
exportOPMLController?.runSheetOnWindow(mainWindowController!.window!) exportOPMLController?.runSheetOnWindow(mainWindowController!.window!)
} }
@IBAction func addAppNews(_ sender: Any?) { @IBAction func addAppNews(_ sender: Any?) {
if AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() { if AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() {
return return
@ -576,17 +572,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
Browser.open("https://netnewswire.com/", inBackground: false) Browser.open("https://netnewswire.com/", inBackground: false)
} }
@IBAction func openReleaseNotes(_ sender: Any?) { @IBAction func openReleaseNotes(_ sender: Any?) {
Browser.open(URL.releaseNotes.absoluteString, inBackground: false) Browser.open(URL.releaseNotes.absoluteString, inBackground: false)
} }
@IBAction func openHowToSupport(_ sender: Any?) { @IBAction func openHowToSupport(_ sender: Any?) {
Browser.open("https://github.com/brentsimmons/NetNewsWire/blob/main/Technotes/HowToSupportNetNewsWire.markdown", inBackground: false) Browser.open("https://github.com/brentsimmons/NetNewsWire/blob/main/Technotes/HowToSupportNetNewsWire.markdown", inBackground: false)
} }
@IBAction func openRepository(_ sender: Any?) { @IBAction func openRepository(_ sender: Any?) {
Browser.open("https://github.com/brentsimmons/NetNewsWire", inBackground: false) Browser.open("https://github.com/brentsimmons/NetNewsWire", inBackground: false)
@ -646,8 +641,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
AppDefaults.shared.timelineSortDirection = .orderedDescending AppDefaults.shared.timelineSortDirection = .orderedDescending
} }
@IBAction func groupByFeedToggled(_ sender: NSMenuItem) { @IBAction func groupByFeedToggled(_ sender: NSMenuItem) {
AppDefaults.shared.timelineGroupByFeed.toggle() AppDefaults.shared.timelineGroupByFeed.toggle()
} }
@ -715,7 +710,7 @@ internal extension AppDelegate {
refreshTimer?.fireOldTimer() refreshTimer?.fireOldTimer()
syncTimer?.fireOldTimer() syncTimer?.fireOldTimer()
} }
func objectsForInspector() -> [Any]? { func objectsForInspector() -> [Any]? {
guard let window = NSApplication.shared.mainWindow, let windowController = window.windowController as? MainWindowController else { guard let window = NSApplication.shared.mainWindow, let windowController = window.windowController as? MainWindowController else {
return nil return nil
@ -733,15 +728,15 @@ internal extension AppDelegate {
sortByNewestArticleOnTopMenuItem.state = sortByNewestOnTop ? .on : .off sortByNewestArticleOnTopMenuItem.state = sortByNewestOnTop ? .on : .off
sortByOldestArticleOnTopMenuItem.state = sortByNewestOnTop ? .off : .on sortByOldestArticleOnTopMenuItem.state = sortByNewestOnTop ? .off : .on
} }
func updateGroupByFeedMenuItem() { func updateGroupByFeedMenuItem() {
let groupByFeedEnabled = AppDefaults.shared.timelineGroupByFeed let groupByFeedEnabled = AppDefaults.shared.timelineGroupByFeed
groupArticlesByFeedMenuItem.state = groupByFeedEnabled ? .on : .off groupArticlesByFeedMenuItem.state = groupByFeedEnabled ? .on : .off
} }
func importTheme(filename: String) { func importTheme(filename: String) {
guard let window = mainWindowController?.window else { return } guard let window = mainWindowController?.window else { return }
do { do {
let themeURL = URL(filePath: filename) let themeURL = URL(filePath: filename)
let theme = try ArticleTheme(url: themeURL, isAppTheme: false) let theme = try ArticleTheme(url: themeURL, isAppTheme: false)
@ -750,11 +745,11 @@ internal extension AppDelegate {
let localizedMessageText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text") let localizedMessageText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text")
alert.messageText = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name, theme.creatorName) as String alert.messageText = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name, theme.creatorName) as String
var attrs = [NSAttributedString.Key : Any]() var attrs = [NSAttributedString.Key: Any]()
attrs[.font] = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) attrs[.font] = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
attrs[.foregroundColor] = NSColor.textColor attrs[.foregroundColor] = NSColor.textColor
let titleParagraphStyle = NSMutableParagraphStyle() let titleParagraphStyle = NSMutableParagraphStyle()
titleParagraphStyle.alignment = .center titleParagraphStyle.alignment = .center
attrs[.paragraphStyle] = titleParagraphStyle attrs[.paragraphStyle] = titleParagraphStyle
@ -774,10 +769,10 @@ internal extension AppDelegate {
textView.drawsBackground = false textView.drawsBackground = false
textView.textStorage?.setAttributedString(websiteText) textView.textStorage?.setAttributedString(websiteText)
alert.accessoryView = textView alert.accessoryView = textView
alert.addButton(withTitle: NSLocalizedString("Install Theme", comment: "Install Theme")) alert.addButton(withTitle: NSLocalizedString("Install Theme", comment: "Install Theme"))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme")) alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme"))
func importTheme() { func importTheme() {
do { do {
try ArticleThemesManager.shared.importTheme(filename: filename) try ArticleThemesManager.shared.importTheme(filename: filename)
@ -786,7 +781,7 @@ internal extension AppDelegate {
NSApplication.shared.presentError(error) NSApplication.shared.presentError(error)
} }
} }
alert.beginSheetModal(for: window) { result in alert.beginSheetModal(for: window) { result in
if result == NSApplication.ModalResponse.alertFirstButtonReturn { if result == NSApplication.ModalResponse.alertFirstButtonReturn {
@ -799,7 +794,7 @@ internal extension AppDelegate {
alert.addButton(withTitle: NSLocalizedString("Overwrite", comment: "Overwrite")) alert.addButton(withTitle: NSLocalizedString("Overwrite", comment: "Overwrite"))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme")) alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme"))
alert.beginSheetModal(for: window) { result in alert.beginSheetModal(for: window) { result in
if result == NSApplication.ModalResponse.alertFirstButtonReturn { if result == NSApplication.ModalResponse.alertFirstButtonReturn {
importTheme() importTheme()
@ -811,25 +806,25 @@ internal extension AppDelegate {
} }
} }
} catch { } catch {
NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error, "path": filename]) NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error, "path": filename])
} }
} }
func confirmImportSuccess(themeName: String) { func confirmImportSuccess(themeName: String) {
guard let window = mainWindowController?.window else { return } guard let window = mainWindowController?.window else { return }
let alert = NSAlert() let alert = NSAlert()
alert.alertStyle = .informational alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Theme installed", comment: "Theme installed") alert.messageText = NSLocalizedString("Theme installed", comment: "Theme installed")
let localizedInformativeText = NSLocalizedString("The theme “%@” has been installed.", comment: "Theme installed") let localizedInformativeText = NSLocalizedString("The theme “%@” has been installed.", comment: "Theme installed")
alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, themeName) as String alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, themeName) as String
alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK"))
alert.beginSheetModal(for: window) alert.beginSheetModal(for: window)
} }
@objc func themeImportError(_ note: Notification) { @objc func themeImportError(_ note: Notification) {
guard let userInfo = note.userInfo, guard let userInfo = note.userInfo,
let error = userInfo["error"] as? Error else { let error = userInfo["error"] as? Error else {
@ -856,14 +851,14 @@ internal extension AppDelegate {
} }
let localizedError = NSLocalizedString("This theme cannot be used because of data corruption in the Info.plist: %@.", comment: "Decoding key missing") let localizedError = NSLocalizedString("This theme cannot be used because of data corruption in the Info.plist: %@.", comment: "Decoding key missing")
informativeText = NSString.localizedStringWithFormat(localizedError as NSString, debugDescription) as String informativeText = NSString.localizedStringWithFormat(localizedError as NSString, debugDescription) as String
default: default:
informativeText = error.localizedDescription informativeText = error.localizedDescription
} }
} else { } else {
informativeText = error.localizedDescription informativeText = error.localizedDescription
} }
DispatchQueue.main.async { DispatchQueue.main.async {
let alert = NSAlert() let alert = NSAlert()
alert.alertStyle = .warning alert.alertStyle = .warning
@ -871,7 +866,7 @@ internal extension AppDelegate {
alert.informativeText = informativeText alert.informativeText = informativeText
alert.addButton(withTitle: NSLocalizedString("Open Theme Folder", comment: "Open Theme Folder")) alert.addButton(withTitle: NSLocalizedString("Open Theme Folder", comment: "Open Theme Folder"))
alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK"))
let button = alert.buttons.first let button = alert.buttons.first
button?.target = self button?.target = self
button?.action = #selector(self.openThemesFolder(_:)) button?.action = #selector(self.openThemesFolder(_:))
@ -890,7 +885,7 @@ internal extension AppDelegate {
NSWorkspace.shared.open(url.deletingLastPathComponent()) NSWorkspace.shared.open(url.deletingLastPathComponent())
} }
} }
} }
/* /*
@ -900,7 +895,7 @@ internal extension AppDelegate {
These would be unnecessary if the similar accessors were marked internal rather than private, These would be unnecessary if the similar accessors were marked internal rather than private,
but for now, we'll keep the stratification of visibility but for now, we'll keep the stratification of visibility
*/ */
extension AppDelegate : ScriptingAppDelegate { extension AppDelegate: ScriptingAppDelegate {
internal var scriptingMainWindowController: ScriptingMainWindowController? { internal var scriptingMainWindowController: ScriptingMainWindowController? {
return mainWindowController return mainWindowController
@ -916,28 +911,28 @@ extension AppDelegate : ScriptingAppDelegate {
} }
extension AppDelegate: NSWindowRestoration { extension AppDelegate: NSWindowRestoration {
@objc static func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier, state: NSCoder, completionHandler: @escaping (NSWindow?, Error?) -> Void) { @objc static func restoreWindow(withIdentifier identifier: NSUserInterfaceItemIdentifier, state: NSCoder, completionHandler: @escaping (NSWindow?, Error?) -> Void) {
var mainWindow: NSWindow? = nil var mainWindow: NSWindow?
if identifier.rawValue == WindowRestorationIdentifiers.mainWindow { if identifier.rawValue == WindowRestorationIdentifiers.mainWindow {
mainWindow = appDelegate.createAndShowMainWindow().window mainWindow = appDelegate.createAndShowMainWindow().window
} }
completionHandler(mainWindow, nil) completionHandler(mainWindow, nil)
} }
} }
// Handle Notification Actions // Handle Notification Actions
private extension AppDelegate { private extension AppDelegate {
func handleMarkAsRead(userInfo: [AnyHashable: Any]) { func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any],
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
return return
} }
let account = AccountManager.shared.existingAccount(with: accountID) let account = AccountManager.shared.existingAccount(with: accountID)
guard account != nil else { guard account != nil else {
os_log(.debug, "No account found from notification.") os_log(.debug, "No account found from notification.")
@ -950,9 +945,9 @@ private extension AppDelegate {
} }
account!.markArticles(article!, statusKey: .read, flag: true) { _ in } account!.markArticles(article!, statusKey: .read, flag: true) { _ in }
} }
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) { func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any],
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
return return

View File

@ -23,7 +23,6 @@ struct Browser {
return nil return nil
} }
/// Opens a URL in the default browser. /// Opens a URL in the default browser.
/// ///
/// - Parameters: /// - Parameters:
@ -34,7 +33,6 @@ struct Browser {
open(urlString, inBackground: invert ? !AppDefaults.shared.openInBrowserInBackground : AppDefaults.shared.openInBrowserInBackground) open(urlString, inBackground: invert ? !AppDefaults.shared.openInBrowserInBackground : AppDefaults.shared.openInBrowserInBackground)
} }
/// Opens a URL in the default browser. /// Opens a URL in the default browser.
/// ///
/// - Parameters: /// - Parameters:
@ -44,7 +42,7 @@ struct Browser {
/// to open in the background. /// to open in the background.
static func open(_ urlString: String, inBackground: Bool) { static func open(_ urlString: String, inBackground: Bool) {
guard let url = URL(string: urlString), let preparedURL = url.preparedForOpeningInBrowser() else { return } guard let url = URL(string: urlString), let preparedURL = url.preparedForOpeningInBrowser() else { return }
let configuration = NSWorkspace.OpenConfiguration() let configuration = NSWorkspace.OpenConfiguration()
configuration.requiresUniversalLinks = true configuration.requiresUniversalLinks = true
configuration.promptsUserIfNeeded = false configuration.promptsUserIfNeeded = false
@ -52,7 +50,7 @@ struct Browser {
configuration.activates = false configuration.activates = false
} }
NSWorkspace.shared.open(preparedURL, configuration: configuration) { (runningApplication, error) in NSWorkspace.shared.open(preparedURL, configuration: configuration) { (_, error) in
guard error != nil else { return } guard error != nil else { return }
if let defaultBrowser = defaultBrowser { if let defaultBrowser = defaultBrowser {
defaultBrowser.openURL(url, inBackground: inBackground) defaultBrowser.openURL(url, inBackground: inBackground)

View File

@ -59,4 +59,3 @@ final class CrashReportWindowController: NSWindowController {
close() close()
} }
} }

View File

@ -37,7 +37,7 @@ struct CrashReporter {
} else { } else {
runCrashReporterWindow(crashLogText) runCrashReporterWindow(crashLogText)
} }
crashReporter.purgePendingCrashReport() crashReporter.purgePendingCrashReport()
} }
@ -48,7 +48,7 @@ struct CrashReporter {
let boundary = "0xKhTmLbOuNdArY" let boundary = "0xKhTmLbOuNdArY"
let contentType = "multipart/form-data; boundary=\(boundary)" let contentType = "multipart/form-data; boundary=\(boundary)"
request.setValue(contentType, forHTTPHeaderField:HTTPRequestHeader.contentType) request.setValue(contentType, forHTTPHeaderField: HTTPRequestHeader.contentType)
let formString = "--\(boundary)\r\nContent-Disposition: form-data; name=\"crashlog\"\r\n\r\n\(crashLogText)\r\n--\(boundary)--\r\n" let formString = "--\(boundary)\r\nContent-Disposition: form-data; name=\"crashlog\"\r\n\r\n\(crashLogText)\r\n--\(boundary)--\r\n"
let formData = formString.data(using: .utf8, allowLossyConversion: true) let formData = formString.data(using: .utf8, allowLossyConversion: true)

View File

@ -13,13 +13,13 @@ import os.log
struct ErrorHandler { struct ErrorHandler {
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account") private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Account")
public static func present(_ error: Error) { public static func present(_ error: Error) {
NSApplication.shared.presentError(error) NSApplication.shared.presentError(error)
} }
public static func log(_ error: Error) { public static func log(_ error: Error) {
os_log(.error, log: self.log, "%@", error.localizedDescription) os_log(.error, log: self.log, "%@", error.localizedDescription)
} }
} }

View File

@ -12,7 +12,7 @@ final class BuiltinSmartFeedInspectorViewController: NSViewController, Inspector
@IBOutlet var nameTextField: NSTextField? @IBOutlet var nameTextField: NSTextField?
@IBOutlet weak var smartFeedImageView: NSImageView! @IBOutlet weak var smartFeedImageView: NSImageView!
private var smartFeed: PseudoFeed? { private var smartFeed: PseudoFeed? {
didSet { didSet {
updateUI() updateUI()

View File

@ -19,7 +19,7 @@ final class FeedInspectorViewController: NSViewController, Inspector {
@IBOutlet weak var urlTextField: NSTextField? @IBOutlet weak var urlTextField: NSTextField?
@IBOutlet weak var isNotifyAboutNewArticlesCheckBox: NSButton! @IBOutlet weak var isNotifyAboutNewArticlesCheckBox: NSButton!
@IBOutlet weak var isReaderViewAlwaysOnCheckBox: NSButton? @IBOutlet weak var isReaderViewAlwaysOnCheckBox: NSButton?
private var feed: Feed? { private var feed: Feed? {
didSet { didSet {
if feed != oldValue { if feed != oldValue {
@ -52,27 +52,27 @@ final class FeedInspectorViewController: NSViewController, Inspector {
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: .DidUpdateFeedPreferencesFromContextMenu, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateUI), name: .DidUpdateFeedPreferencesFromContextMenu, object: nil)
} }
override func viewDidAppear() { override func viewDidAppear() {
updateNotificationSettings() updateNotificationSettings()
} }
override func viewDidDisappear() { override func viewDidDisappear() {
renameFeedIfNecessary() renameFeedIfNecessary()
} }
// MARK: Actions // MARK: Actions
@IBAction func isNotifyAboutNewArticlesChanged(_ sender: Any) { @IBAction func isNotifyAboutNewArticlesChanged(_ sender: Any) {
guard userNotificationSettings != nil else { guard userNotificationSettings != nil else {
DispatchQueue.main.async { DispatchQueue.main.async {
self.isNotifyAboutNewArticlesCheckBox.setNextState() self.isNotifyAboutNewArticlesCheckBox.setNextState()
} }
return return
} }
UNUserNotificationCenter.current().getNotificationSettings { (settings) in UNUserNotificationCenter.current().getNotificationSettings { (settings) in
self.updateNotificationSettings() self.updateNotificationSettings()
if settings.authorizationStatus == .denied { if settings.authorizationStatus == .denied {
DispatchQueue.main.async { DispatchQueue.main.async {
self.isNotifyAboutNewArticlesCheckBox.setNextState() self.isNotifyAboutNewArticlesCheckBox.setNextState()
@ -83,7 +83,7 @@ final class FeedInspectorViewController: NSViewController, Inspector {
self.feed?.isNotifyAboutNewArticles = (self.isNotifyAboutNewArticlesCheckBox?.state ?? .off) == .on ? true : false self.feed?.isNotifyAboutNewArticles = (self.isNotifyAboutNewArticlesCheckBox?.state ?? .off) == .on ? true : false
} }
} else { } else {
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { (granted, _) in
self.updateNotificationSettings() self.updateNotificationSettings()
if granted { if granted {
DispatchQueue.main.async { DispatchQueue.main.async {
@ -99,17 +99,17 @@ final class FeedInspectorViewController: NSViewController, Inspector {
} }
} }
} }
@IBAction func isReaderViewAlwaysOnChanged(_ sender: Any) { @IBAction func isReaderViewAlwaysOnChanged(_ sender: Any) {
feed?.isArticleExtractorAlwaysOn = (isReaderViewAlwaysOnCheckBox?.state ?? .off) == .on ? true : false feed?.isArticleExtractorAlwaysOn = (isReaderViewAlwaysOnCheckBox?.state ?? .off) == .on ? true : false
} }
// MARK: Notifications // MARK: Notifications
@objc func imageDidBecomeAvailable(_ note: Notification) { @objc func imageDidBecomeAvailable(_ note: Notification) {
updateImage() updateImage()
} }
} }
extension FeedInspectorViewController: NSTextFieldDelegate { extension FeedInspectorViewController: NSTextFieldDelegate {
@ -117,7 +117,7 @@ extension FeedInspectorViewController: NSTextFieldDelegate {
func controlTextDidEndEditing(_ note: Notification) { func controlTextDidEndEditing(_ note: Notification) {
renameFeedIfNecessary() renameFeedIfNecessary()
} }
} }
private extension FeedInspectorViewController { private extension FeedInspectorViewController {
@ -130,7 +130,6 @@ private extension FeedInspectorViewController {
feed = singleFeed feed = singleFeed
} }
@objc func updateUI() { @objc func updateUI() {
updateImage() updateImage()
updateName() updateName()
@ -209,7 +208,7 @@ private extension FeedInspectorViewController {
feed.nameForDisplay != nameTextField.stringValue else { feed.nameForDisplay != nameTextField.stringValue else {
return return
} }
account.renameFeed(feed, to: nameTextField.stringValue) { [weak self] result in account.renameFeed(feed, to: nameTextField.stringValue) { [weak self] result in
if case .failure(let error) = result { if case .failure(let error) = result {
self?.presentError(error) self?.presentError(error)
@ -218,5 +217,5 @@ private extension FeedInspectorViewController {
} }
} }
} }
} }

View File

@ -14,7 +14,7 @@ final class FolderInspectorViewController: NSViewController, Inspector {
@IBOutlet var nameTextField: NSTextField? @IBOutlet var nameTextField: NSTextField?
@IBOutlet weak var folderImageView: NSImageView! @IBOutlet weak var folderImageView: NSImageView!
private var folder: Folder? { private var folder: Folder? {
didSet { didSet {
if folder != oldValue { if folder != oldValue {
@ -46,18 +46,18 @@ final class FolderInspectorViewController: NSViewController, Inspector {
override func viewDidLoad() { override func viewDidLoad() {
updateUI() updateUI()
let image = NSImage(systemSymbolName: "folder", accessibilityDescription: nil)! let image = NSImage(systemSymbolName: "folder", accessibilityDescription: nil)!
folderImageView.image = image folderImageView.image = image
folderImageView.contentTintColor = NSColor.controlAccentColor folderImageView.contentTintColor = NSColor.controlAccentColor
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
} }
override func viewDidDisappear() { override func viewDidDisappear() {
renameFolderIfNecessary() renameFolderIfNecessary()
} }
// MARK: Notifications // MARK: Notifications
@objc func displayNameDidChange(_ note: Notification) { @objc func displayNameDidChange(_ note: Notification) {
@ -73,7 +73,7 @@ extension FolderInspectorViewController: NSTextFieldDelegate {
func controlTextDidEndEditing(_ obj: Notification) { func controlTextDidEndEditing(_ obj: Notification) {
renameFolderIfNecessary() renameFolderIfNecessary()
} }
} }
private extension FolderInspectorViewController { private extension FolderInspectorViewController {
@ -104,7 +104,7 @@ private extension FolderInspectorViewController {
} }
windowTitle = folder?.nameForDisplay ?? NSLocalizedString("Folder Inspector", comment: "Folder Inspector window title") windowTitle = folder?.nameForDisplay ?? NSLocalizedString("Folder Inspector", comment: "Folder Inspector window title")
} }
func renameFolderIfNecessary() { func renameFolderIfNecessary() {
guard let folder = folder, guard let folder = folder,
let account = folder.account, let account = folder.account,
@ -112,7 +112,7 @@ private extension FolderInspectorViewController {
folder.nameForDisplay != nameTextField.stringValue else { folder.nameForDisplay != nameTextField.stringValue else {
return return
} }
account.renameFolder(folder, to: nameTextField.stringValue) { [weak self] result in account.renameFolder(folder, to: nameTextField.stringValue) { [weak self] result in
if case .failure(let error) = result { if case .failure(let error) = result {
self?.presentError(error) self?.presentError(error)
@ -121,5 +121,5 @@ private extension FolderInspectorViewController {
} }
} }
} }
} }

View File

@ -19,7 +19,6 @@ protocol Inspector: AnyObject {
typealias InspectorViewController = Inspector & NSViewController typealias InspectorViewController = Inspector & NSViewController
final class InspectorWindowController: NSWindowController { final class InspectorWindowController: NSWindowController {
class var shouldOpenAtStartup: Bool { class var shouldOpenAtStartup: Bool {
@ -28,7 +27,7 @@ final class InspectorWindowController: NSWindowController {
var objects: [Any]? { var objects: [Any]? {
didSet { didSet {
let _ = window _ = window
currentInspector = inspector(for: objects) currentInspector = inspector(for: objects)
} }
} }
@ -67,21 +66,19 @@ final class InspectorWindowController: NSWindowController {
if let savedOrigin = originFromDefaults() { if let savedOrigin = originFromDefaults() {
window?.setFlippedOriginAdjustingForScreen(savedOrigin) window?.setFlippedOriginAdjustingForScreen(savedOrigin)
} } else {
else {
window?.flippedOrigin = NSPoint(x: 256, y: 256) window?.flippedOrigin = NSPoint(x: 256, y: 256)
} }
} }
func inspector(for objects: [Any]?) -> InspectorViewController { func inspector(for objects: [Any]?) -> InspectorViewController {
var fallbackInspector: InspectorViewController? = nil var fallbackInspector: InspectorViewController?
for inspector in inspectors { for inspector in inspectors {
if inspector.isFallbackInspector { if inspector.isFallbackInspector {
fallbackInspector = inspector fallbackInspector = inspector
} } else if let objects = objects, inspector.canInspect(objects) {
else if let objects = objects, inspector.canInspect(objects) {
return inspector return inspector
} }
} }
@ -113,7 +110,7 @@ private extension InspectorWindowController {
DispatchQueue.main.async { DispatchQueue.main.async {
window.title = inspector.windowTitle window.title = inspector.windowTitle
} }
let flippedOrigin = window.flippedOrigin let flippedOrigin = window.flippedOrigin
@ -121,7 +118,7 @@ private extension InspectorWindowController {
window.contentViewController = inspector window.contentViewController = inspector
window.makeFirstResponder(nil) window.makeFirstResponder(nil)
} }
window.layoutIfNeeded() window.layoutIfNeeded()
if let flippedOrigin = flippedOrigin { if let flippedOrigin = flippedOrigin {
window.setFlippedOriginAdjustingForScreen(flippedOrigin) window.setFlippedOriginAdjustingForScreen(flippedOrigin)

View File

@ -39,8 +39,7 @@ private extension NothingInspectorViewController {
if let objects = objects, objects.count > 1 { if let objects = objects, objects.count > 1 {
nothingTextField?.isHidden = true nothingTextField?.isHidden = true
multipleTextField?.isHidden = false multipleTextField?.isHidden = false
} } else {
else {
nothingTextField?.isHidden = false nothingTextField?.isHidden = false
multipleTextField?.isHidden = true multipleTextField?.isHidden = true
} }

View File

@ -28,7 +28,7 @@ class AddFeedController: AddFeedWindowControllerDelegate {
private var addFeedWindowController: AddFeedWindowController? private var addFeedWindowController: AddFeedWindowController?
private var foundFeedURLString: String? private var foundFeedURLString: String?
private var titleFromFeed: String? private var titleFromFeed: String?
init(hostWindow: NSWindow) { init(hostWindow: NSWindow) {
self.hostWindow = hostWindow self.hostWindow = hostWindow
} }
@ -62,11 +62,11 @@ class AddFeedController: AddFeedWindowControllerDelegate {
} }
account.createFeed(url: url.absoluteString, name: title, container: container, validateFeed: true) { result in account.createFeed(url: url.absoluteString, name: title, container: container, validateFeed: true) { result in
DispatchQueue.main.async { DispatchQueue.main.async {
self.endShowingProgress() self.endShowingProgress()
} }
switch result { switch result {
case .success(let feed): case .success(let feed):
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed]) NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
@ -82,9 +82,9 @@ class AddFeedController: AddFeedWindowControllerDelegate {
} }
} }
} }
} }
beginShowingProgress() beginShowingProgress()
} }
@ -102,7 +102,7 @@ private extension AddFeedController {
} }
return nil return nil
} }
struct AccountAndFolderSpecifier { struct AccountAndFolderSpecifier {
let account: Account let account: Account
let folder: Folder? let folder: Folder?
@ -156,13 +156,12 @@ private extension AddFeedController {
// MARK: Progress // MARK: Progress
func beginShowingProgress() { func beginShowingProgress() {
runIndeterminateProgressWithMessage(NSLocalizedString("Finding feed…", comment:"Feed finder")) runIndeterminateProgressWithMessage(NSLocalizedString("Finding feed…", comment: "Feed finder"))
} }
func endShowingProgress() { func endShowingProgress() {
stopIndeterminateProgress() stopIndeterminateProgress()
hostWindow.makeKeyAndOrderFront(self) hostWindow.makeKeyAndOrderFront(self)
} }
}
}

View File

@ -16,8 +16,8 @@ protocol AddFeedWindowControllerDelegate: AnyObject {
func addFeedWindowControllerUserDidCancel(_: AddFeedWindowController) func addFeedWindowControllerUserDidCancel(_: AddFeedWindowController)
} }
//protocol AddFeedWindowController { // protocol AddFeedWindowController {
// //
// var window: NSWindow? { get } // var window: NSWindow? { get }
// func runSheetOnWindow(_ hostWindow: NSWindow) // func runSheetOnWindow(_ hostWindow: NSWindow)
//} // }

View File

@ -12,7 +12,7 @@ import RSTree
import Articles import Articles
import Account import Account
final class AddFeedWindowController : NSWindowController { final class AddFeedWindowController: NSWindowController {
@IBOutlet var urlTextField: NSTextField! @IBOutlet var urlTextField: NSTextField!
@IBOutlet var nameTextField: NSTextField! @IBOutlet var nameTextField: NSTextField!
@ -34,7 +34,7 @@ final class AddFeedWindowController : NSWindowController {
} }
return s return s
} }
var hostWindow: NSWindow! var hostWindow: NSWindow!
convenience init(urlString: String?, name: String?, account: Account?, folder: Folder?, folderTreeController: TreeController, delegate: AddFeedWindowControllerDelegate?) { convenience init(urlString: String?, name: String?, account: Account?, folder: Folder?, folderTreeController: TreeController, delegate: AddFeedWindowControllerDelegate?) {
@ -46,9 +46,9 @@ final class AddFeedWindowController : NSWindowController {
self.delegate = delegate self.delegate = delegate
self.folderTreeController = folderTreeController self.folderTreeController = folderTreeController
} }
func runSheetOnWindow(_ hostWindow: NSWindow) { func runSheetOnWindow(_ hostWindow: NSWindow) {
hostWindow.beginSheet(window!) { (returnCode: NSApplication.ModalResponse) -> Void in hostWindow.beginSheet(window!) { (_: NSApplication.ModalResponse) in
} }
} }
@ -61,7 +61,7 @@ final class AddFeedWindowController : NSWindowController {
} }
folderPopupButton.menu = FolderTreeMenu.createFolderPopupMenu(with: folderTreeController.rootNode) folderPopupButton.menu = FolderTreeMenu.createFolderPopupMenu(with: folderTreeController.rootNode)
if let account = initialAccount { if let account = initialAccount {
FolderTreeMenu.select(account: account, folder: initialFolder, in: folderPopupButton) FolderTreeMenu.select(account: account, folder: initialFolder, in: folderPopupButton)
} else if let container = AddFeedDefaultContainer.defaultContainer { } else if let container = AddFeedDefaultContainer.defaultContainer {
@ -73,41 +73,41 @@ final class AddFeedWindowController : NSWindowController {
} }
} }
} }
updateUI() updateUI()
} }
// MARK: Actions // MARK: Actions
@IBAction func cancel(_ sender: Any?) { @IBAction func cancel(_ sender: Any?) {
cancelSheet() cancelSheet()
} }
@IBAction func addFeed(_ sender: Any?) { @IBAction func addFeed(_ sender: Any?) {
let urlString = urlTextField.stringValue let urlString = urlTextField.stringValue
let normalizedURLString = urlString.normalizedURL let normalizedURLString = urlString.normalizedURL
if normalizedURLString.isEmpty { if normalizedURLString.isEmpty {
cancelSheet() cancelSheet()
return; return
} }
guard let url = URL(string: normalizedURLString) else { guard let url = URL(string: normalizedURLString) else {
cancelSheet() cancelSheet()
return return
} }
guard let container = selectedContainer() else { return } guard let container = selectedContainer() else { return }
AddFeedDefaultContainer.saveDefaultContainer(container) AddFeedDefaultContainer.saveDefaultContainer(container)
delegate?.addFeedWindowController(self, userEnteredURL: url, userEnteredTitle: userEnteredTitle, container: container) delegate?.addFeedWindowController(self, userEnteredURL: url, userEnteredTitle: userEnteredTitle, container: container)
} }
@IBAction func localShowFeedList(_ sender: Any?) { @IBAction func localShowFeedList(_ sender: Any?) {
NSApplication.shared.sendAction(NSSelectorFromString("showFeedList:"), to: nil, from: sender) NSApplication.shared.sendAction(NSSelectorFromString("showFeedList:"), to: nil, from: sender)
hostWindow.endSheet(window!, returnCode: NSApplication.ModalResponse.continue) hostWindow.endSheet(window!, returnCode: NSApplication.ModalResponse.continue)
} }
// MARK: NSTextFieldDelegate // MARK: NSTextFieldDelegate
@objc func controlTextDidEndEditing(_ obj: Notification) { @objc func controlTextDidEndEditing(_ obj: Notification) {
@ -117,7 +117,7 @@ final class AddFeedWindowController : NSWindowController {
@objc func controlTextDidChange(_ obj: Notification) { @objc func controlTextDidChange(_ obj: Notification) {
updateUI() updateUI()
} }
private func updateUI() { private func updateUI() {
addButton.isEnabled = urlTextField.stringValue.mayBeURL && selectedContainer() != nil addButton.isEnabled = urlTextField.stringValue.mayBeURL && selectedContainer() != nil
} }

View File

@ -14,34 +14,34 @@ import Account
class FolderTreeMenu { class FolderTreeMenu {
static func createFolderPopupMenu(with rootNode: Node, restrictToSpecialAccounts: Bool = false) -> NSMenu { static func createFolderPopupMenu(with rootNode: Node, restrictToSpecialAccounts: Bool = false) -> NSMenu {
let menu = NSMenu(title: "Folders") let menu = NSMenu(title: "Folders")
menu.autoenablesItems = false menu.autoenablesItems = false
for childNode in rootNode.childNodes { for childNode in rootNode.childNodes {
guard let account = childNode.representedObject as? Account else { guard let account = childNode.representedObject as? Account else {
continue continue
} }
if restrictToSpecialAccounts && !(account.type == .onMyMac || account.type == .cloudKit) { if restrictToSpecialAccounts && !(account.type == .onMyMac || account.type == .cloudKit) {
continue continue
} }
let menuItem = NSMenuItem(title: account.nameForDisplay, action: nil, keyEquivalent: "") let menuItem = NSMenuItem(title: account.nameForDisplay, action: nil, keyEquivalent: "")
menuItem.representedObject = childNode.representedObject menuItem.representedObject = childNode.representedObject
if account.behaviors.contains(.disallowFeedInRootFolder) { if account.behaviors.contains(.disallowFeedInRootFolder) {
menuItem.isEnabled = false menuItem.isEnabled = false
} }
menu.addItem(menuItem) menu.addItem(menuItem)
let childNodes = childNode.childNodes let childNodes = childNode.childNodes
addFolderItemsToMenuWithNodes(menu: menu, nodes: childNodes, indentationLevel: 1) addFolderItemsToMenuWithNodes(menu: menu, nodes: childNodes, indentationLevel: 1)
} }
return menu return menu
} }
@ -61,21 +61,21 @@ class FolderTreeMenu {
} }
private static func addFolderItemsToMenuWithNodes(menu: NSMenu, nodes: [Node], indentationLevel: Int) { private static func addFolderItemsToMenuWithNodes(menu: NSMenu, nodes: [Node], indentationLevel: Int) {
for oneNode in nodes { for oneNode in nodes {
if let nameProvider = oneNode.representedObject as? DisplayNameProvider { if let nameProvider = oneNode.representedObject as? DisplayNameProvider {
let menuItem = NSMenuItem(title: nameProvider.nameForDisplay, action: nil, keyEquivalent: "") let menuItem = NSMenuItem(title: nameProvider.nameForDisplay, action: nil, keyEquivalent: "")
menuItem.indentationLevel = indentationLevel menuItem.indentationLevel = indentationLevel
menuItem.representedObject = oneNode.representedObject menuItem.representedObject = oneNode.representedObject
menu.addItem(menuItem) menu.addItem(menuItem)
if oneNode.numberOfChildNodes > 0 { if oneNode.numberOfChildNodes > 0 {
addFolderItemsToMenuWithNodes(menu: menu, nodes: oneNode.childNodes, indentationLevel: indentationLevel + 1) addFolderItemsToMenuWithNodes(menu: menu, nodes: oneNode.childNodes, indentationLevel: indentationLevel + 1)
} }
} }
} }
} }
} }

View File

@ -10,8 +10,8 @@ import AppKit
import Articles import Articles
import Account import Account
class AddFolderWindowController : NSWindowController { class AddFolderWindowController: NSWindowController {
@IBOutlet var folderNameTextField: NSTextField! @IBOutlet var folderNameTextField: NSTextField!
@IBOutlet var accountPopupButton: NSPopUpButton! @IBOutlet var accountPopupButton: NSPopUpButton!
@IBOutlet var addFolderButton: NSButton! @IBOutlet var addFolderButton: NSButton!
@ -22,11 +22,11 @@ class AddFolderWindowController : NSWindowController {
} }
// MARK: - API // MARK: - API
func runSheetOnWindow(_ w: NSWindow) { func runSheetOnWindow(_ w: NSWindow) {
hostWindow = w hostWindow = w
hostWindow!.beginSheet(window!) { (returnCode: NSApplication.ModalResponse) -> Void in hostWindow!.beginSheet(window!) { (returnCode: NSApplication.ModalResponse) in
if returnCode == NSApplication.ModalResponse.OK { if returnCode == NSApplication.ModalResponse.OK {
self.addFolderIfNeeded() self.addFolderIfNeeded()
} }
@ -34,37 +34,37 @@ class AddFolderWindowController : NSWindowController {
} }
// MARK: - NSViewController // MARK: - NSViewController
override func windowDidLoad() { override func windowDidLoad() {
let preferredAccountID = AppDefaults.shared.addFolderAccountID let preferredAccountID = AppDefaults.shared.addFolderAccountID
accountPopupButton.removeAllItems() accountPopupButton.removeAllItems()
let menu = NSMenu() let menu = NSMenu()
accountPopupButton.menu = menu accountPopupButton.menu = menu
let accounts = AccountManager.shared let accounts = AccountManager.shared
.sortedActiveAccounts .sortedActiveAccounts
.filter { !$0.behaviors.contains(.disallowFolderManagement) } .filter { !$0.behaviors.contains(.disallowFolderManagement) }
for oneAccount in accounts { for oneAccount in accounts {
let oneMenuItem = NSMenuItem() let oneMenuItem = NSMenuItem()
oneMenuItem.title = oneAccount.nameForDisplay oneMenuItem.title = oneAccount.nameForDisplay
oneMenuItem.representedObject = oneAccount oneMenuItem.representedObject = oneAccount
menu.addItem(oneMenuItem) menu.addItem(oneMenuItem)
if oneAccount.accountID == preferredAccountID { if oneAccount.accountID == preferredAccountID {
accountPopupButton.select(oneMenuItem) accountPopupButton.select(oneMenuItem)
} }
} }
} }
// MARK: - Actions // MARK: - Actions
@IBAction func cancel(_ sender: Any?) { @IBAction func cancel(_ sender: Any?) {
hostWindow!.endSheet(window!, returnCode: .cancel) hostWindow!.endSheet(window!, returnCode: .cancel)
} }
@IBAction func addFolder(_ sender: Any?) { @IBAction func addFolder(_ sender: Any?) {
hostWindow!.endSheet(window!, returnCode: .OK) hostWindow!.endSheet(window!, returnCode: .OK)
} }

View File

@ -16,9 +16,9 @@ enum ArticleExtractorButtonState {
} }
class ArticleExtractorButton: NSButton { class ArticleExtractorButton: NSButton {
private var animatedLayer: CALayer? private var animatedLayer: CALayer?
var buttonState: ArticleExtractorButtonState = .off { var buttonState: ArticleExtractorButtonState = .off {
didSet { didSet {
if buttonState != oldValue { if buttonState != oldValue {
@ -39,7 +39,7 @@ class ArticleExtractorButton: NSButton {
} }
} }
} }
override func accessibilityLabel() -> String? { override func accessibilityLabel() -> String? {
switch buttonState { switch buttonState {
case .error: case .error:
@ -57,12 +57,12 @@ class ArticleExtractorButton: NSButton {
super.init(frame: frameRect) super.init(frame: frameRect)
commonInit() commonInit()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
private func commonInit() { private func commonInit() {
wantsLayer = true wantsLayer = true
bezelStyle = .texturedRounded bezelStyle = .texturedRounded
@ -70,7 +70,7 @@ class ArticleExtractorButton: NSButton {
imageScaling = .scaleProportionallyDown imageScaling = .scaleProportionallyDown
widthAnchor.constraint(equalTo: heightAnchor).isActive = true widthAnchor.constraint(equalTo: heightAnchor).isActive = true
} }
override func layout() { override func layout() {
super.layout() super.layout()
guard case .animated = buttonState else { guard case .animated = buttonState else {
@ -79,31 +79,31 @@ class ArticleExtractorButton: NSButton {
stripAnimatedSublayer() stripAnimatedSublayer()
addAnimatedSublayer(to: layer!) addAnimatedSublayer(to: layer!)
} }
private func stripAnimatedSublayer() { private func stripAnimatedSublayer() {
animatedLayer?.removeFromSuperlayer() animatedLayer?.removeFromSuperlayer()
} }
private func addAnimatedSublayer(to hostedLayer: CALayer) { private func addAnimatedSublayer(to hostedLayer: CALayer) {
let image1 = AppAssets.articleExtractorOff.tinted(with: NSColor.controlTextColor) let image1 = AppAssets.articleExtractorOff.tinted(with: NSColor.controlTextColor)
let image2 = AppAssets.articleExtractorOn.tinted(with: NSColor.controlTextColor) let image2 = AppAssets.articleExtractorOn.tinted(with: NSColor.controlTextColor)
let images = [image1, image2, image1] let images = [image1, image2, image1]
animatedLayer = CALayer() animatedLayer = CALayer()
let imageSize = AppAssets.articleExtractorOff.size let imageSize = AppAssets.articleExtractorOff.size
animatedLayer!.bounds = CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height) animatedLayer!.bounds = CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height)
animatedLayer!.position = CGPoint(x: bounds.midX, y: bounds.midY) animatedLayer!.position = CGPoint(x: bounds.midX, y: bounds.midY)
hostedLayer.addSublayer(animatedLayer!) hostedLayer.addSublayer(animatedLayer!)
let animation = CAKeyframeAnimation(keyPath: "contents") let animation = CAKeyframeAnimation(keyPath: "contents")
animation.calculationMode = CAAnimationCalculationMode.linear animation.calculationMode = CAAnimationCalculationMode.linear
animation.keyTimes = [0, 0.5, 1] animation.keyTimes = [0, 0.5, 1]
animation.duration = 2 animation.duration = 2
animation.values = images animation.values = images
animation.repeatCount = HUGE animation.repeatCount = HUGE
animatedLayer!.add(animation, forKey: "contents") animatedLayer!.add(animation, forKey: "contents")
} }
} }

View File

@ -38,7 +38,7 @@ final class DetailContainerView: NSView {
override func draw(_ dirtyRect: NSRect) { override func draw(_ dirtyRect: NSRect) {
NSColor.controlBackgroundColor.set() NSColor.controlBackgroundColor.set()
let r = NSIntersectionRect(dirtyRect, bounds) let r = dirtyRect.intersection(bounds)
r.fill() r.fill()
} }
} }

View File

@ -25,8 +25,7 @@ final class DetailStatusBarView: NSView {
if let link = linkForDisplay { if let link = linkForDisplay {
urlLabel.stringValue = link urlLabel.stringValue = link
self.isHidden = false self.isHidden = false
} } else {
else {
urlLabel.stringValue = "" urlLabel.stringValue = ""
self.isHidden = true self.isHidden = true
} }
@ -38,7 +37,7 @@ final class DetailStatusBarView: NSView {
override var isOpaque: Bool { override var isOpaque: Bool {
return false return false
} }
override var isFlipped: Bool { override var isFlipped: Bool {
return true return true
} }
@ -68,11 +67,8 @@ private extension DetailStatusBarView {
func updateLinkForDisplay() { func updateLinkForDisplay() {
if let mouseoverLink = mouseoverLink, !mouseoverLink.isEmpty { if let mouseoverLink = mouseoverLink, !mouseoverLink.isEmpty {
linkForDisplay = mouseoverLink.strippingHTTPOrHTTPSScheme linkForDisplay = mouseoverLink.strippingHTTPOrHTTPSScheme
} } else {
else {
linkForDisplay = nil linkForDisplay = nil
} }
} }
} }

View File

@ -82,7 +82,7 @@ final class DetailViewController: NSViewController, WKUIDelegate {
func stopMediaPlayback() { func stopMediaPlayback() {
currentWebViewController.stopMediaPlayback() currentWebViewController.stopMediaPlayback()
} }
func canScrollDown(_ callback: @escaping (Bool) -> Void) { func canScrollDown(_ callback: @escaping (Bool) -> Void) {
currentWebViewController.canScrollDown(callback) currentWebViewController.canScrollDown(callback)
} }
@ -98,22 +98,22 @@ final class DetailViewController: NSViewController, WKUIDelegate {
override func scrollPageUp(_ sender: Any?) { override func scrollPageUp(_ sender: Any?) {
currentWebViewController.scrollPageUp(sender) currentWebViewController.scrollPageUp(sender)
} }
// MARK: - Navigation // MARK: - Navigation
func focus() { func focus() {
guard let window = currentWebViewController.webView.window else { guard let window = currentWebViewController.webView.window else {
return return
} }
window.makeFirstResponderUnlessDescendantIsFirstResponder(currentWebViewController.webView) window.makeFirstResponderUnlessDescendantIsFirstResponder(currentWebViewController.webView)
} }
// MARK: State Restoration // MARK: State Restoration
func saveState(to state: inout [AnyHashable : Any]) { func saveState(to state: inout [AnyHashable: Any]) {
currentWebViewController.saveState(to: &state) currentWebViewController.saveState(to: &state)
} }
} }
// MARK: - DetailWebViewControllerDelegate // MARK: - DetailWebViewControllerDelegate
@ -158,7 +158,7 @@ private extension DetailViewController {
} }
} }
@objc func userDefaultsDidChange(_ : Notification) { @objc func userDefaultsDidChange(_: Notification) {
if AppDefaults.shared.isArticleContentJavascriptEnabled != isArticleContentJavascriptEnabled { if AppDefaults.shared.isArticleContentJavascriptEnabled != isArticleContentJavascriptEnabled {
isArticleContentJavascriptEnabled = AppDefaults.shared.isArticleContentJavascriptEnabled isArticleContentJavascriptEnabled = AppDefaults.shared.isArticleContentJavascriptEnabled
createNewWebViewsAndRestoreState() createNewWebViewsAndRestoreState()

View File

@ -13,13 +13,13 @@ import RSCore
final class DetailWebView: WKWebView { final class DetailWebView: WKWebView {
weak var keyboardDelegate: KeyboardDelegate? weak var keyboardDelegate: KeyboardDelegate?
override func accessibilityLabel() -> String? { override func accessibilityLabel() -> String? {
return NSLocalizedString("Article", comment: "Article") return NSLocalizedString("Article", comment: "Article")
} }
// MARK: - NSResponder // MARK: - NSResponder
override func keyDown(with event: NSEvent) { override func keyDown(with event: NSEvent) {
if keyboardDelegate?.keydown(event, in: self) ?? false { if keyboardDelegate?.keydown(event, in: self) ?? false {
return return
@ -55,16 +55,16 @@ final class DetailWebView: WKWebView {
evaluateJavaScript("document.body.style.overflow = 'visible';", completionHandler: nil) evaluateJavaScript("document.body.style.overflow = 'visible';", completionHandler: nil)
bigSurOffsetFix() bigSurOffsetFix()
} }
override func setFrameSize(_ newSize: NSSize) { override func setFrameSize(_ newSize: NSSize) {
super.setFrameSize(newSize) super.setFrameSize(newSize)
if (!inLiveResize) { if !inLiveResize {
bigSurOffsetFix() bigSurOffsetFix()
} }
} }
private var inBigSurOffsetFix = false private var inBigSurOffsetFix = false
private func bigSurOffsetFix() { private func bigSurOffsetFix() {
/* /*
On macOS 11, when a user exits full screen On macOS 11, when a user exits full screen
@ -77,17 +77,17 @@ final class DetailWebView: WKWebView {
guard var frame = window?.frame else { guard var frame = window?.frame else {
return return
} }
guard !inBigSurOffsetFix else { guard !inBigSurOffsetFix else {
return return
} }
inBigSurOffsetFix = true inBigSurOffsetFix = true
defer { defer {
inBigSurOffsetFix = false inBigSurOffsetFix = false
} }
frame.size = NSSize(width: window!.frame.width, height: window!.frame.height - 1) frame.size = NSSize(width: window!.frame.width, height: window!.frame.height - 1)
window!.setFrame(frame, display: false) window!.setFrame(frame, display: false)
frame.size = NSSize(width: window!.frame.width, height: window!.frame.height + 1) frame.size = NSSize(width: window!.frame.width, height: window!.frame.height + 1)
@ -128,4 +128,3 @@ private extension DetailWebView {
return false return false
} }
} }

View File

@ -34,7 +34,7 @@ final class DetailWebViewController: NSViewController {
} }
} }
} }
var article: Article? { var article: Article? {
switch state { switch state {
case .article(let article, _): case .article(let article, _):
@ -45,9 +45,9 @@ final class DetailWebViewController: NSViewController {
return nil return nil
} }
} }
private var articleTextSize = AppDefaults.shared.articleTextSize private var articleTextSize = AppDefaults.shared.articleTextSize
private var webInspectorEnabled: Bool { private var webInspectorEnabled: Bool {
get { get {
return webView.configuration.preferences._developerExtrasEnabled return webView.configuration.preferences._developerExtrasEnabled
@ -64,7 +64,7 @@ final class DetailWebViewController: NSViewController {
private var isShowingExtractedArticle: Bool { private var isShowingExtractedArticle: Bool {
switch state { switch state {
case .extracted(_, _, _): case .extracted:
return true return true
default: default:
return false return false
@ -96,7 +96,7 @@ final class DetailWebViewController: NSViewController {
// See bug #901. // See bug #901.
webView.isHidden = true webView.isHidden = true
waitingForFirstReload = true waitingForFirstReload = true
webInspectorEnabled = AppDefaults.shared.webInspectorEnabled webInspectorEnabled = AppDefaults.shared.webInspectorEnabled
NotificationCenter.default.addObserver(self, selector: #selector(webInspectorEnabledDidChange(_:)), name: .WebInspectorEnabledDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(webInspectorEnabledDidChange(_:)), name: .WebInspectorEnabledDidChange, object: nil)
@ -110,7 +110,7 @@ final class DetailWebViewController: NSViewController {
} }
// MARK: Notifications // MARK: Notifications
@objc func feedIconDidBecomeAvailable(_ note: Notification) { @objc func feedIconDidBecomeAvailable(_ note: Notification) {
reloadArticleImage() reloadArticleImage()
} }
@ -122,24 +122,24 @@ final class DetailWebViewController: NSViewController {
@objc func faviconDidBecomeAvailable(_ note: Notification) { @objc func faviconDidBecomeAvailable(_ note: Notification) {
reloadArticleImage() reloadArticleImage()
} }
@objc func userDefaultsDidChange(_ note: Notification) { @objc func userDefaultsDidChange(_ note: Notification) {
if articleTextSize != AppDefaults.shared.articleTextSize { if articleTextSize != AppDefaults.shared.articleTextSize {
articleTextSize = AppDefaults.shared.articleTextSize articleTextSize = AppDefaults.shared.articleTextSize
reloadHTMLMaintainingScrollPosition() reloadHTMLMaintainingScrollPosition()
} }
} }
@objc func currentArticleThemeDidChangeNotification(_ note: Notification) { @objc func currentArticleThemeDidChangeNotification(_ note: Notification) {
reloadHTMLMaintainingScrollPosition() reloadHTMLMaintainingScrollPosition()
} }
// MARK: Media Functions // MARK: Media Functions
func stopMediaPlayback() { func stopMediaPlayback() {
webView.evaluateJavaScript("stopMediaPlayback();") webView.evaluateJavaScript("stopMediaPlayback();")
} }
// MARK: Scrolling // MARK: Scrolling
func canScrollDown(_ completion: @escaping (Bool) -> Void) { func canScrollDown(_ completion: @escaping (Bool) -> Void) {
@ -163,12 +163,12 @@ final class DetailWebViewController: NSViewController {
} }
// MARK: State Restoration // MARK: State Restoration
func saveState(to state: inout [AnyHashable : Any]) { func saveState(to state: inout [AnyHashable: Any]) {
state[UserInfoKey.isShowingExtractedArticle] = isShowingExtractedArticle state[UserInfoKey.isShowingExtractedArticle] = isShowingExtractedArticle
state[UserInfoKey.articleWindowScrollY] = windowScrollY state[UserInfoKey.articleWindowScrollY] = windowScrollY
} }
} }
// MARK: - ArticleIconSchemeHandlerDelegate // MARK: - ArticleIconSchemeHandlerDelegate
@ -228,7 +228,7 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
decisionHandler(.allow) decisionHandler(.allow)
} }
public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
// See note in viewDidLoad() // See note in viewDidLoad()
if waitingForFirstReload { if waitingForFirstReload {
@ -250,7 +250,7 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate {
} }
// WKUIDelegate // WKUIDelegate
func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? { func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
// This method is reached when WebKit handles a JavaScript based window.open() invocation, for example. One // This method is reached when WebKit handles a JavaScript based window.open() invocation, for example. One
// example where this is used is in YouTube's embedded video player when a user clicks on the video's title // example where this is used is in YouTube's embedded video player when a user clicks on the video's title
@ -270,18 +270,18 @@ private extension DetailWebViewController {
func reloadArticleImage() { func reloadArticleImage() {
guard let article = article else { return } guard let article = article else { return }
var components = URLComponents() var components = URLComponents()
components.scheme = ArticleRenderer.imageIconScheme components.scheme = ArticleRenderer.imageIconScheme
components.path = article.articleID components.path = article.articleID
if let imageSrc = components.string { if let imageSrc = components.string {
webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")") webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")")
} }
} }
func reloadHTMLMaintainingScrollPosition() { func reloadHTMLMaintainingScrollPosition() {
fetchScrollInfo() { scrollInfo in fetchScrollInfo { scrollInfo in
self.windowScrollY = scrollInfo?.offsetY self.windowScrollY = scrollInfo?.offsetY
self.reloadHTML() self.reloadHTML()
} }
@ -289,7 +289,7 @@ private extension DetailWebViewController {
func reloadHTML() { func reloadHTML() {
delegate?.mouseDidExit(self) delegate?.mouseDidExit(self)
let theme = ArticleThemesManager.shared.currentTheme let theme = ArticleThemesManager.shared.currentTheme
let rendering: ArticleRenderer.Rendering let rendering: ArticleRenderer.Rendering
@ -305,14 +305,14 @@ private extension DetailWebViewController {
case .extracted(let article, let extractedArticle, _): case .extracted(let article, let extractedArticle, _):
rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, theme: theme) rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, theme: theme)
} }
let substitutions = [ let substitutions = [
"title": rendering.title, "title": rendering.title,
"baseURL": rendering.baseURL, "baseURL": rendering.baseURL,
"style": rendering.style, "style": rendering.style,
"body": rendering.html "body": rendering.html
] ]
let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions) let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions)
webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL)) webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL))
} }
@ -320,7 +320,7 @@ private extension DetailWebViewController {
func fetchScrollInfo(_ completion: @escaping (ScrollInfo?) -> Void) { func fetchScrollInfo(_ completion: @escaping (ScrollInfo?) -> Void) {
let javascriptString = "var x = {contentHeight: document.body.scrollHeight, offsetY: window.pageYOffset}; x" let javascriptString = "var x = {contentHeight: document.body.scrollHeight, offsetY: window.pageYOffset}; x"
webView.evaluateJavaScript(javascriptString) { (info, error) in webView.evaluateJavaScript(javascriptString) { (info, _) in
guard let info = info as? [String: Any] else { guard let info = info as? [String: Any] else {
completion(nil) completion(nil)
return return

View File

@ -10,7 +10,7 @@ import AppKit
final class IconView: NSView { final class IconView: NSView {
var iconImage: IconImage? = nil { var iconImage: IconImage? {
didSet { didSet {
if iconImage !== oldValue { if iconImage !== oldValue {
imageView.image = iconImage?.image imageView.image = iconImage?.image
@ -36,7 +36,7 @@ final class IconView: NSView {
} }
private var isDiscernable = true private var isDiscernable = true
override var isFlipped: Bool { override var isFlipped: Bool {
return true return true
} }
@ -76,7 +76,7 @@ final class IconView: NSView {
} }
override func layout() { override func layout() {
resizeSubviews(withOldSize: NSZeroSize) resizeSubviews(withOldSize: NSSize.zero)
} }
override func resizeSubviews(withOldSize oldSize: NSSize) { override func resizeSubviews(withOldSize oldSize: NSSize) {
@ -89,7 +89,7 @@ final class IconView: NSView {
let color = NSApplication.shared.effectiveAppearance.isDarkMode ? IconView.darkBackgroundColor : IconView.lightBackgroundColor let color = NSApplication.shared.effectiveAppearance.isDarkMode ? IconView.darkBackgroundColor : IconView.lightBackgroundColor
color.set() color.set()
let r = NSIntersectionRect(dirtyRect, bounds) let r = dirtyRect.intersection(bounds)
r.fill() r.fill()
} }
} }
@ -104,9 +104,9 @@ private extension IconView {
func rectForImageView() -> NSRect { func rectForImageView() -> NSRect {
guard !(iconImage?.isSymbol ?? false) else { guard !(iconImage?.isSymbol ?? false) else {
return NSMakeRect(0.0, 0.0, bounds.size.width, bounds.size.height) return NSRect(x: 0.0, y: 0.0, width: bounds.size.width, height: bounds.size.height)
} }
guard let image = iconImage?.image else { guard let image = iconImage?.image else {
return NSRect.zero return NSRect.zero
} }
@ -116,22 +116,21 @@ private extension IconView {
if imageSize.height == imageSize.width { if imageSize.height == imageSize.width {
if imageSize.height >= viewSize.height * 0.75 { if imageSize.height >= viewSize.height * 0.75 {
// Close enough to viewSize to scale up the image. // Close enough to viewSize to scale up the image.
return NSMakeRect(0.0, 0.0, viewSize.width, viewSize.height) return NSRect(x: 0.0, y: 0.0, width: viewSize.width, height: viewSize.height)
} }
let offset = floor((viewSize.height - imageSize.height) / 2.0) let offset = floor((viewSize.height - imageSize.height) / 2.0)
return NSMakeRect(offset, offset, imageSize.width, imageSize.height) return NSRect(x: offset, y: offset, width: imageSize.width, height: imageSize.height)
} } else if imageSize.height > imageSize.width {
else if imageSize.height > imageSize.width {
let factor = viewSize.height / imageSize.height let factor = viewSize.height / imageSize.height
let width = imageSize.width * factor let width = imageSize.width * factor
let originX = floor((viewSize.width - width) / 2.0) let originX = floor((viewSize.width - width) / 2.0)
return NSMakeRect(originX, 0.0, width, viewSize.height) return NSRect(x: originX, y: 0.0, width: width, height: viewSize.height)
} }
// Wider than tall: imageSize.width > imageSize.height // Wider than tall: imageSize.width > imageSize.height
let factor = viewSize.width / imageSize.width let factor = viewSize.width / imageSize.width
let height = imageSize.height * factor let height = imageSize.height * factor
let originY = floor((viewSize.height - height) / 2.0) let originY = floor((viewSize.height - height) / 2.0)
return NSMakeRect(0.0, originY, viewSize.width, height) return NSRect(x: 0.0, y: originY, width: viewSize.width, height: height)
} }
} }

View File

@ -33,4 +33,3 @@ final class MainWindowKeyboardHandler: KeyboardDelegate {
return true return true
} }
} }

View File

@ -9,7 +9,7 @@
import Foundation import Foundation
class LegacyArticleExtractorButton: NSButton { class LegacyArticleExtractorButton: NSButton {
var isError = false { var isError = false {
didSet { didSet {
if isError != oldValue { if isError != oldValue {
@ -17,7 +17,7 @@ class LegacyArticleExtractorButton: NSButton {
} }
} }
} }
var isInProgress = false { var isInProgress = false {
didSet { didSet {
if isInProgress != oldValue { if isInProgress != oldValue {
@ -25,12 +25,12 @@ class LegacyArticleExtractorButton: NSButton {
} }
} }
} }
override init(frame frameRect: NSRect) { override init(frame frameRect: NSRect) {
super.init(frame: frameRect) super.init(frame: frameRect)
wantsLayer = true wantsLayer = true
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
wantsLayer = true wantsLayer = true
@ -38,7 +38,7 @@ class LegacyArticleExtractorButton: NSButton {
override func draw(_ dirtyRect: NSRect) { override func draw(_ dirtyRect: NSRect) {
super.draw(dirtyRect) super.draw(dirtyRect)
guard let hostedLayer = self.layer else { guard let hostedLayer = self.layer else {
return return
} }
@ -52,7 +52,7 @@ class LegacyArticleExtractorButton: NSButton {
} }
let opacity: Float = isEnabled ? 1.0 : 0.5 let opacity: Float = isEnabled ? 1.0 : 0.5
switch true { switch true {
case isError: case isError:
addImageSublayer(to: hostedLayer, image: AppAssets.legacyArticleExtractorError, opacity: opacity) addImageSublayer(to: hostedLayer, image: AppAssets.legacyArticleExtractorError, opacity: opacity)
@ -70,42 +70,42 @@ class LegacyArticleExtractorButton: NSButton {
} }
} }
} }
private func makeLayerForImage(_ image: NSImage) -> CALayer { private func makeLayerForImage(_ image: NSImage) -> CALayer {
let imageLayer = CALayer() let imageLayer = CALayer()
imageLayer.bounds = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height) imageLayer.bounds = CGRect(x: 0, y: 0, width: image.size.width, height: image.size.height)
imageLayer.position = CGPoint(x: bounds.midX, y: floor(bounds.midY)) imageLayer.position = CGPoint(x: bounds.midX, y: floor(bounds.midY))
return imageLayer return imageLayer
} }
private func addImageSublayer(to hostedLayer: CALayer, image: NSImage, opacity: Float = 1.0) { private func addImageSublayer(to hostedLayer: CALayer, image: NSImage, opacity: Float = 1.0) {
let imageLayer = makeLayerForImage(image) let imageLayer = makeLayerForImage(image)
imageLayer.contents = image imageLayer.contents = image
imageLayer.opacity = opacity imageLayer.opacity = opacity
hostedLayer.addSublayer(imageLayer) hostedLayer.addSublayer(imageLayer)
} }
private func addAnimatedSublayer(to hostedLayer: CALayer) { private func addAnimatedSublayer(to hostedLayer: CALayer) {
let imageProgress1 = AppAssets.legacyArticleExtractorProgress1 let imageProgress1 = AppAssets.legacyArticleExtractorProgress1
let imageProgress2 = AppAssets.legacyArticleExtractorProgress2 let imageProgress2 = AppAssets.legacyArticleExtractorProgress2
let imageProgress3 = AppAssets.legacyArticleExtractorProgress3 let imageProgress3 = AppAssets.legacyArticleExtractorProgress3
let imageProgress4 = AppAssets.legacyArticleExtractorProgress4 let imageProgress4 = AppAssets.legacyArticleExtractorProgress4
let images = [imageProgress1, imageProgress2, imageProgress3, imageProgress4, imageProgress3, imageProgress2, imageProgress1] let images = [imageProgress1, imageProgress2, imageProgress3, imageProgress4, imageProgress3, imageProgress2, imageProgress1]
let imageLayer = CALayer() let imageLayer = CALayer()
imageLayer.bounds = CGRect(x: 0, y: 0, width: imageProgress1?.size.width ?? 0, height: imageProgress1?.size.height ?? 0) imageLayer.bounds = CGRect(x: 0, y: 0, width: imageProgress1?.size.width ?? 0, height: imageProgress1?.size.height ?? 0)
imageLayer.position = CGPoint(x: bounds.midX, y: floor(bounds.midY)) imageLayer.position = CGPoint(x: bounds.midX, y: floor(bounds.midY))
hostedLayer.addSublayer(imageLayer) hostedLayer.addSublayer(imageLayer)
let animation = CAKeyframeAnimation(keyPath: "contents") let animation = CAKeyframeAnimation(keyPath: "contents")
animation.calculationMode = CAAnimationCalculationMode.linear animation.calculationMode = CAAnimationCalculationMode.linear
animation.keyTimes = [0, 0.16, 0.32, 0.50, 0.66, 0.82, 1] animation.keyTimes = [0, 0.16, 0.32, 0.50, 0.66, 0.82, 1]
animation.duration = 2 animation.duration = 2
animation.values = images as [Any] animation.values = images as [Any]
animation.repeatCount = HUGE animation.repeatCount = HUGE
imageLayer.add(animation, forKey: "contents") imageLayer.add(animation, forKey: "contents")
} }
} }

View File

@ -16,14 +16,14 @@ enum TimelineSourceMode {
case regular, search case regular, search
} }
class MainWindowController : NSWindowController, NSUserInterfaceValidations { class MainWindowController: NSWindowController, NSUserInterfaceValidations {
@IBOutlet weak var articleThemePopUpButton: NSPopUpButton? @IBOutlet weak var articleThemePopUpButton: NSPopUpButton?
private var activityManager = ActivityManager() private var activityManager = ActivityManager()
private var isShowingExtractedArticle = false private var isShowingExtractedArticle = false
private var articleExtractor: ArticleExtractor? = nil private var articleExtractor: ArticleExtractor?
private var sharingServicePickerDelegate: NSSharingServicePickerDelegate? private var sharingServicePickerDelegate: NSSharingServicePickerDelegate?
private let windowAutosaveName = NSWindow.FrameAutosaveName("MainWindow") private let windowAutosaveName = NSWindow.FrameAutosaveName("MainWindow")
@ -36,7 +36,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
} }
return selectedObjects.first return selectedObjects.first
} }
private var shareToolbarItem: NSToolbarItem? { private var shareToolbarItem: NSToolbarItem? {
return window?.toolbar?.existingItem(withIdentifier: .share) return window?.toolbar?.existingItem(withIdentifier: .share)
} }
@ -45,17 +45,17 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
private var sidebarViewController: SidebarViewController? private var sidebarViewController: SidebarViewController?
private var timelineContainerViewController: TimelineContainerViewController? private var timelineContainerViewController: TimelineContainerViewController?
private var detailViewController: DetailViewController? private var detailViewController: DetailViewController?
private var currentSearchField: NSSearchField? = nil private var currentSearchField: NSSearchField?
private let articleThemeMenuToolbarItem = NSMenuToolbarItem(itemIdentifier: .articleThemeMenu) private let articleThemeMenuToolbarItem = NSMenuToolbarItem(itemIdentifier: .articleThemeMenu)
private var searchString: String? = nil private var searchString: String?
private var lastSentSearchString: String? = nil private var lastSentSearchString: String?
private var timelineSourceMode: TimelineSourceMode = .regular { private var timelineSourceMode: TimelineSourceMode = .regular {
didSet { didSet {
timelineContainerViewController?.showTimeline(for: timelineSourceMode) timelineContainerViewController?.showTimeline(for: timelineSourceMode)
detailViewController?.showDetail(for: timelineSourceMode) detailViewController?.showDetail(for: timelineSourceMode)
} }
} }
private var searchSmartFeed: SmartFeed? = nil private var searchSmartFeed: SmartFeed?
private var restoreArticleWindowScrollY: CGFloat? private var restoreArticleWindowScrollY: CGFloat?
// MARK: - NSWindowController // MARK: - NSWindowController
@ -97,10 +97,10 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(currentArticleThemeDidChangeNotification(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(currentArticleThemeDidChangeNotification(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil)
DispatchQueue.main.async { DispatchQueue.main.async {
self.updateWindowTitle() self.updateWindowTitle()
} }
@ -115,14 +115,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
func handle(_ response: UNNotificationResponse) { func handle(_ response: UNNotificationResponse) {
let userInfo = response.notification.request.content.userInfo let userInfo = response.notification.request.content.userInfo
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return } guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any] else { return }
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo) sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo) currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
} }
func handle(_ activity: NSUserActivity) { func handle(_ activity: NSUserActivity) {
guard let userInfo = activity.userInfo else { return } guard let userInfo = activity.userInfo else { return }
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any] else { return } guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any] else { return }
sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo) sidebarViewController?.deepLinkRevealAndSelect(for: articlePathUserInfo)
currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo) currentTimelineViewController?.goToDeepLink(for: articlePathUserInfo)
} }
@ -131,14 +131,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
AppDefaults.shared.windowState = savableState() AppDefaults.shared.windowState = savableState()
window?.saveFrame(usingName: windowAutosaveName) window?.saveFrame(usingName: windowAutosaveName)
} }
func restoreStateFromUserDefaults() { func restoreStateFromUserDefaults() {
if let state = AppDefaults.shared.windowState { if let state = AppDefaults.shared.windowState {
restoreState(from: state) restoreState(from: state)
window?.setFrameUsingName(windowAutosaveName, force: true) window?.setFrameUsingName(windowAutosaveName, force: true)
} }
} }
// MARK: - Notifications // MARK: - Notifications
@objc func refreshProgressDidChange(_ note: Notification) { @objc func refreshProgressDidChange(_ note: Notification) {
@ -148,7 +148,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
@objc func unreadCountDidChange(_ note: Notification) { @objc func unreadCountDidChange(_ note: Notification) {
updateWindowTitleIfNecessary(note.object) updateWindowTitleIfNecessary(note.object)
} }
@objc func displayNameDidChange(_ note: Notification) { @objc func displayNameDidChange(_ note: Notification) {
updateWindowTitleIfNecessary(note.object) updateWindowTitleIfNecessary(note.object)
} }
@ -162,21 +162,21 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
} }
private func updateWindowTitleIfNecessary(_ noteObject: Any?) { private func updateWindowTitleIfNecessary(_ noteObject: Any?) {
if let folder = currentFeedOrFolder as? Folder, let noteObject = noteObject as? Folder { if let folder = currentFeedOrFolder as? Folder, let noteObject = noteObject as? Folder {
if folder == noteObject { if folder == noteObject {
updateWindowTitle() updateWindowTitle()
return return
} }
} }
if let feed = currentFeedOrFolder as? Feed, let noteObject = noteObject as? Feed { if let feed = currentFeedOrFolder as? Feed, let noteObject = noteObject as? Feed {
if feed == noteObject { if feed == noteObject {
updateWindowTitle() updateWindowTitle()
return return
} }
} }
// If we don't recognize the changed object, we will test it for identity instead // If we don't recognize the changed object, we will test it for identity instead
// of equality. This works well for us if the window title is displaying a // of equality. This works well for us if the window title is displaying a
// PsuedoFeed object. // PsuedoFeed object.
@ -185,28 +185,28 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
updateWindowTitle() updateWindowTitle()
} }
} }
} }
// MARK: - Toolbar // MARK: - Toolbar
@objc func makeToolbarValidate() { @objc func makeToolbarValidate() {
window?.toolbar?.validateVisibleItems() window?.toolbar?.validateVisibleItems()
} }
// MARK: - NSUserInterfaceValidations // MARK: - NSUserInterfaceValidations
public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool {
if item.action == #selector(copyArticleURL(_:)) { if item.action == #selector(copyArticleURL(_:)) {
return canCopyArticleURL() return canCopyArticleURL()
} }
if item.action == #selector(copyExternalURL(_:)) { if item.action == #selector(copyExternalURL(_:)) {
return canCopyExternalURL() return canCopyExternalURL()
} }
if item.action == #selector(openArticleInBrowser(_:)) { if item.action == #selector(openArticleInBrowser(_:)) {
if let item = item as? NSMenuItem, item.keyEquivalentModifierMask.contains(.shift) { if let item = item as? NSMenuItem, item.keyEquivalentModifierMask.contains(.shift) {
item.title = Browser.titleForOpenInBrowserInverted item.title = Browser.titleForOpenInBrowserInverted
@ -214,11 +214,11 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
return currentLink != nil return currentLink != nil
} }
if item.action == #selector(nextUnread(_:)) { if item.action == #selector(nextUnread(_:)) {
return canGoToNextUnread(wrappingToTop: true) return canGoToNextUnread(wrappingToTop: true)
} }
if item.action == #selector(markAllAsRead(_:)) { if item.action == #selector(markAllAsRead(_:)) {
return canMarkAllAsRead() return canMarkAllAsRead()
} }
@ -242,7 +242,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
if item.action == #selector(toggleArticleExtractor(_:)) { if item.action == #selector(toggleArticleExtractor(_:)) {
return validateToggleArticleExtractor(item) return validateToggleArticleExtractor(item)
} }
if item.action == #selector(toolbarShowShareMenu(_:)) { if item.action == #selector(toolbarShowShareMenu(_:)) {
return canShowShareMenu() return canShowShareMenu()
} }
@ -283,7 +283,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
return return
} }
detailViewController.canScrollUp { (canScroll) in detailViewController.canScrollUp { (canScroll) in
if (canScroll) { if canScroll {
NSCursor.setHiddenUntilMouseMoves(true) NSCursor.setHiddenUntilMouseMoves(true)
detailViewController.scrollPageUp(sender) detailViewController.scrollPageUp(sender)
} }
@ -306,7 +306,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
@IBAction func openArticleInBrowser(_ sender: Any?) { @IBAction func openArticleInBrowser(_ sender: Any?) {
if let link = currentLink { if let link = currentLink {
Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false) Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
} }
} }
@IBAction func openInBrowser(_ sender: Any?) { @IBAction func openInBrowser(_ sender: Any?) {
@ -340,8 +340,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
// TODO: handle search mode // TODO: handle search mode
if timelineViewController.canGoToNextUnread(wrappingToTop: false) { if timelineViewController.canGoToNextUnread(wrappingToTop: false) {
goToNextUnreadInTimeline(wrappingToTop: false) goToNextUnreadInTimeline(wrappingToTop: false)
} } else if sidebarViewController.canGoToNextUnread(wrappingToTop: true) {
else if sidebarViewController.canGoToNextUnread(wrappingToTop: true) {
sidebarViewController.goToNextUnread(wrappingToTop: true) sidebarViewController.goToNextUnread(wrappingToTop: true)
// If we ended up on the same timelineViewController, we may need to wrap // If we ended up on the same timelineViewController, we may need to wrap
@ -373,7 +372,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
} }
@IBAction func toggleArticleExtractor(_ sender: Any?) { @IBAction func toggleArticleExtractor(_ sender: Any?) {
guard let currentLink = currentLink, let article = oneSelectedArticle else { guard let currentLink = currentLink, let article = oneSelectedArticle else {
return return
} }
@ -381,12 +380,12 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
defer { defer {
makeToolbarValidate() makeToolbarValidate()
} }
if articleExtractor?.state == .failedToParse { if articleExtractor?.state == .failedToParse {
startArticleExtractorForCurrentLink() startArticleExtractorForCurrentLink()
return return
} }
guard articleExtractor?.state != .processing else { guard articleExtractor?.state != .processing else {
articleExtractor?.cancel() articleExtractor?.cancel()
articleExtractor = nil articleExtractor = nil
@ -394,13 +393,13 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
detailViewController?.setState(DetailState.article(article, nil), mode: timelineSourceMode) detailViewController?.setState(DetailState.article(article, nil), mode: timelineSourceMode)
return return
} }
guard !isShowingExtractedArticle else { guard !isShowingExtractedArticle else {
isShowingExtractedArticle = false isShowingExtractedArticle = false
detailViewController?.setState(DetailState.article(article, nil), mode: timelineSourceMode) detailViewController?.setState(DetailState.article(article, nil), mode: timelineSourceMode)
return return
} }
if let articleExtractor = articleExtractor, let extractedArticle = articleExtractor.article { if let articleExtractor = articleExtractor, let extractedArticle = articleExtractor.article {
if currentLink == articleExtractor.articleLink { if currentLink == articleExtractor.articleLink {
isShowingExtractedArticle = true isShowingExtractedArticle = true
@ -410,11 +409,11 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
} else { } else {
startArticleExtractorForCurrentLink() startArticleExtractorForCurrentLink()
} }
} }
@IBAction func markAllAsReadAndGoToNextUnread(_ sender: Any?) { @IBAction func markAllAsReadAndGoToNextUnread(_ sender: Any?) {
currentTimelineViewController?.markAllAsRead() { currentTimelineViewController?.markAllAsRead {
self.nextUnread(sender) self.nextUnread(sender)
} }
} }
@ -432,7 +431,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
@IBAction func markOlderArticlesAsRead(_ sender: Any?) { @IBAction func markOlderArticlesAsRead(_ sender: Any?) {
currentTimelineViewController?.markOlderArticlesRead() currentTimelineViewController?.markOlderArticlesRead()
} }
@IBAction func markAboveArticlesAsRead(_ sender: Any?) { @IBAction func markAboveArticlesAsRead(_ sender: Any?) {
currentTimelineViewController?.markAboveArticlesRead() currentTimelineViewController?.markAboveArticlesRead()
} }
@ -452,7 +451,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
@IBAction func navigateToDetail(_ sender: Any?) { @IBAction func navigateToDetail(_ sender: Any?) {
detailViewController?.focus() detailViewController?.focus()
} }
@IBAction func goToPreviousSubscription(_ sender: Any?) { @IBAction func goToPreviousSubscription(_ sender: Any?) {
sidebarViewController?.outlineView.selectPreviousRow(sender) sidebarViewController?.outlineView.selectPreviousRow(sender)
} }
@ -504,31 +503,31 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
@IBAction func cleanUp(_ sender: Any?) { @IBAction func cleanUp(_ sender: Any?) {
timelineContainerViewController?.cleanUp() timelineContainerViewController?.cleanUp()
} }
@IBAction func toggleReadFeedsFilter(_ sender: Any?) { @IBAction func toggleReadFeedsFilter(_ sender: Any?) {
sidebarViewController?.toggleReadFilter() sidebarViewController?.toggleReadFilter()
} }
@IBAction func toggleReadArticlesFilter(_ sender: Any?) { @IBAction func toggleReadArticlesFilter(_ sender: Any?) {
timelineContainerViewController?.toggleReadFilter() timelineContainerViewController?.toggleReadFilter()
} }
@objc func selectArticleTheme(_ menuItem: NSMenuItem) { @objc func selectArticleTheme(_ menuItem: NSMenuItem) {
ArticleThemesManager.shared.currentThemeName = menuItem.title ArticleThemesManager.shared.currentThemeName = menuItem.title
} }
} }
// MARK: NSWindowDelegate // MARK: NSWindowDelegate
extension MainWindowController: NSWindowDelegate { extension MainWindowController: NSWindowDelegate {
func window(_ window: NSWindow, willEncodeRestorableState coder: NSCoder) { func window(_ window: NSWindow, willEncodeRestorableState coder: NSCoder) {
coder.encode(savableState(), forKey: UserInfoKey.windowState) coder.encode(savableState(), forKey: UserInfoKey.windowState)
} }
func window(_ window: NSWindow, didDecodeRestorableState coder: NSCoder) { func window(_ window: NSWindow, didDecodeRestorableState coder: NSCoder) {
guard let state = try? coder.decodeTopLevelObject(forKey: UserInfoKey.windowState) as? [AnyHashable : Any] else { return } guard let state = try? coder.decodeTopLevelObject(forKey: UserInfoKey.windowState) as? [AnyHashable: Any] else { return }
restoreState(from: state) restoreState(from: state)
} }
@ -536,7 +535,7 @@ extension MainWindowController: NSWindowDelegate {
detailViewController?.stopMediaPlayback() detailViewController?.stopMediaPlayback()
appDelegate.removeMainWindow(self) appDelegate.removeMainWindow(self)
} }
} }
// MARK: - SidebarDelegate // MARK: - SidebarDelegate
@ -563,11 +562,11 @@ extension MainWindowController: SidebarDelegate {
} }
return timelineViewController.unreadCount return timelineViewController.unreadCount
} }
func sidebarInvalidatedRestorationState(_: SidebarViewController) { func sidebarInvalidatedRestorationState(_: SidebarViewController) {
invalidateRestorableState() invalidateRestorableState()
} }
} }
// MARK: - TimelineContainerViewControllerDelegate // MARK: - TimelineContainerViewControllerDelegate
@ -576,12 +575,12 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) { func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode) {
activityManager.invalidateReading() activityManager.invalidateReading()
articleExtractor?.cancel() articleExtractor?.cancel()
articleExtractor = nil articleExtractor = nil
isShowingExtractedArticle = false isShowingExtractedArticle = false
makeToolbarValidate() makeToolbarValidate()
let detailState: DetailState let detailState: DetailState
if let articles = articles { if let articles = articles {
if articles.count == 1 { if articles.count == 1 {
@ -606,11 +605,11 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed) { func timelineRequestedFeedSelection(_: TimelineContainerViewController, feed: Feed) {
sidebarViewController?.selectFeed(feed) sidebarViewController?.selectFeed(feed)
} }
func timelineInvalidatedRestorationState(_: TimelineContainerViewController) { func timelineInvalidatedRestorationState(_: TimelineContainerViewController) {
invalidateRestorableState() invalidateRestorableState()
} }
} }
// MARK: - NSSearchFieldDelegate // MARK: - NSSearchFieldDelegate
@ -683,11 +682,11 @@ extension MainWindowController: NSSearchFieldDelegate {
// MARK: - ArticleExtractorDelegate // MARK: - ArticleExtractorDelegate
extension MainWindowController: ArticleExtractorDelegate { extension MainWindowController: ArticleExtractorDelegate {
func articleExtractionDidFail(with: Error) { func articleExtractionDidFail(with: Error) {
makeToolbarValidate() makeToolbarValidate()
} }
func articleExtractionDidComplete(extractedArticle: ExtractedArticle) { func articleExtractionDidComplete(extractedArticle: ExtractedArticle) {
if let article = oneSelectedArticle, articleExtractor?.state != .cancelled { if let article = oneSelectedArticle, articleExtractor?.state != .cancelled {
isShowingExtractedArticle = true isShowingExtractedArticle = true
@ -697,7 +696,7 @@ extension MainWindowController: ArticleExtractorDelegate {
makeToolbarValidate() makeToolbarValidate()
} }
} }
} }
// MARK: - Scripting Access // MARK: - Scripting Access
@ -710,7 +709,7 @@ extension MainWindowController: ArticleExtractorDelegate {
but for now, we'll keep the stratification of visibility but for now, we'll keep the stratification of visibility
*/ */
extension MainWindowController : ScriptingMainWindowController { extension MainWindowController: ScriptingMainWindowController {
internal var scriptingCurrentArticle: Article? { internal var scriptingCurrentArticle: Article? {
return self.oneSelectedArticle return self.oneSelectedArticle
@ -827,7 +826,7 @@ extension MainWindowController: NSToolbarDelegate {
return nil return nil
} }
func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] { func toolbarAllowedItemIdentifiers(_ toolbar: NSToolbar) -> [NSToolbarItem.Identifier] {
[ [
NSToolbarItem.Identifier.toggleSidebar, NSToolbarItem.Identifier.toggleSidebar,
@ -929,7 +928,7 @@ private extension MainWindowController {
var detailSplitViewItem: NSSplitViewItem? { var detailSplitViewItem: NSSplitViewItem? {
return splitViewController?.splitViewItems[2] return splitViewController?.splitViewItems[2]
} }
var selectedArticles: [Article]? { var selectedArticles: [Article]? {
return currentTimelineViewController?.selectedArticles return currentTimelineViewController?.selectedArticles
} }
@ -946,9 +945,9 @@ private extension MainWindowController {
} }
// MARK: - State Restoration // MARK: - State Restoration
func savableState() -> [AnyHashable : Any] { func savableState() -> [AnyHashable: Any] {
var state = [AnyHashable : Any]() var state = [AnyHashable: Any]()
state[UserInfoKey.windowFullScreenState] = window?.styleMask.contains(.fullScreen) ?? false state[UserInfoKey.windowFullScreenState] = window?.styleMask.contains(.fullScreen) ?? false
saveSplitViewState(to: &state) saveSplitViewState(to: &state)
sidebarViewController?.saveState(to: &state) sidebarViewController?.saveState(to: &state)
@ -957,56 +956,56 @@ private extension MainWindowController {
return state return state
} }
func restoreState(from state: [AnyHashable : Any]) { func restoreState(from state: [AnyHashable: Any]) {
if let fullScreen = state[UserInfoKey.windowFullScreenState] as? Bool, fullScreen { if let fullScreen = state[UserInfoKey.windowFullScreenState] as? Bool, fullScreen {
window?.toggleFullScreen(self) window?.toggleFullScreen(self)
} }
restoreSplitViewState(from: state) restoreSplitViewState(from: state)
sidebarViewController?.restoreState(from: state) sidebarViewController?.restoreState(from: state)
let articleWindowScrollY = state[UserInfoKey.articleWindowScrollY] as? CGFloat let articleWindowScrollY = state[UserInfoKey.articleWindowScrollY] as? CGFloat
restoreArticleWindowScrollY = articleWindowScrollY restoreArticleWindowScrollY = articleWindowScrollY
timelineContainerViewController?.restoreState(from: state) timelineContainerViewController?.restoreState(from: state)
let isShowingExtractedArticle = state[UserInfoKey.isShowingExtractedArticle] as? Bool ?? false let isShowingExtractedArticle = state[UserInfoKey.isShowingExtractedArticle] as? Bool ?? false
if isShowingExtractedArticle { if isShowingExtractedArticle {
restoreArticleWindowScrollY = articleWindowScrollY restoreArticleWindowScrollY = articleWindowScrollY
startArticleExtractorForCurrentLink() startArticleExtractorForCurrentLink()
} }
} }
// MARK: - Command Validation // MARK: - Command Validation
func canCopyArticleURL() -> Bool { func canCopyArticleURL() -> Bool {
return currentLink != nil return currentLink != nil
} }
func canCopyExternalURL() -> Bool { func canCopyExternalURL() -> Bool {
return oneSelectedArticle?.externalLink != nil && oneSelectedArticle?.externalLink != currentLink return oneSelectedArticle?.externalLink != nil && oneSelectedArticle?.externalLink != currentLink
} }
func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool { func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool {
guard let timelineViewController = currentTimelineViewController, let sidebarViewController = sidebarViewController else { guard let timelineViewController = currentTimelineViewController, let sidebarViewController = sidebarViewController else {
return false return false
} }
// TODO: handle search mode // TODO: handle search mode
return timelineViewController.canGoToNextUnread(wrappingToTop: wrapping) || sidebarViewController.canGoToNextUnread(wrappingToTop: wrapping) return timelineViewController.canGoToNextUnread(wrappingToTop: wrapping) || sidebarViewController.canGoToNextUnread(wrappingToTop: wrapping)
} }
func canMarkAllAsRead() -> Bool { func canMarkAllAsRead() -> Bool {
return currentTimelineViewController?.canMarkAllAsRead() ?? false return currentTimelineViewController?.canMarkAllAsRead() ?? false
} }
func validateToggleRead(_ item: NSValidatedUserInterfaceItem) -> Bool { func validateToggleRead(_ item: NSValidatedUserInterfaceItem) -> Bool {
let validationStatus = currentTimelineViewController?.markReadCommandStatus() ?? .canDoNothing let validationStatus = currentTimelineViewController?.markReadCommandStatus() ?? .canDoNothing
let markingRead: Bool let markingRead: Bool
let result: Bool let result: Bool
switch validationStatus { switch validationStatus {
case .canMark: case .canMark:
markingRead = true markingRead = true
@ -1018,21 +1017,21 @@ private extension MainWindowController {
markingRead = true markingRead = true
result = false result = false
} }
let commandName = markingRead ? NSLocalizedString("Mark as Read", comment: "Command") : NSLocalizedString("Mark as Unread", comment: "Command") let commandName = markingRead ? NSLocalizedString("Mark as Read", comment: "Command") : NSLocalizedString("Mark as Unread", comment: "Command")
if let toolbarItem = item as? NSToolbarItem { if let toolbarItem = item as? NSToolbarItem {
toolbarItem.toolTip = commandName toolbarItem.toolTip = commandName
} }
if let menuItem = item as? NSMenuItem { if let menuItem = item as? NSMenuItem {
menuItem.title = commandName menuItem.title = commandName
} }
if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton { if let toolbarItem = item as? NSToolbarItem, let button = toolbarItem.view as? NSButton {
button.image = markingRead ? AppAssets.readClosedImage : AppAssets.readOpenImage button.image = markingRead ? AppAssets.readClosedImage : AppAssets.readOpenImage
} }
return result return result
} }
@ -1076,7 +1075,7 @@ private extension MainWindowController {
func canMarkBelowArticlesAsRead() -> Bool { func canMarkBelowArticlesAsRead() -> Bool {
return currentTimelineViewController?.canMarkBelowArticlesAsRead() ?? false return currentTimelineViewController?.canMarkBelowArticlesAsRead() ?? false
} }
func canShowShareMenu() -> Bool { func canShowShareMenu() -> Bool {
guard let selectedArticles = selectedArticles else { guard let selectedArticles = selectedArticles else {
@ -1119,7 +1118,7 @@ private extension MainWindowController {
return result return result
} }
func validateCleanUp(_ item: NSValidatedUserInterfaceItem) -> Bool { func validateCleanUp(_ item: NSValidatedUserInterfaceItem) -> Bool {
return timelineContainerViewController?.isCleanUpAvailable ?? false return timelineContainerViewController?.isCleanUpAvailable ?? false
} }
@ -1159,7 +1158,7 @@ private extension MainWindowController {
button.image = AppAssets.filterInactive button.image = AppAssets.filterInactive
} }
} }
return true return true
} }
@ -1192,7 +1191,7 @@ private extension MainWindowController {
window?.subtitle = "" window?.subtitle = ""
return return
} }
func setSubtitle(_ count: Int) { func setSubtitle(_ count: Int) {
let localizedLabel = NSLocalizedString("%d unread", comment: "Unread") let localizedLabel = NSLocalizedString("%d unread", comment: "Unread")
let formattedLabel = NSString.localizedStringWithFormat(localizedLabel as NSString, count) let formattedLabel = NSString.localizedStringWithFormat(localizedLabel as NSString, count)
@ -1235,16 +1234,16 @@ private extension MainWindowController {
} }
} }
func saveSplitViewState(to state: inout [AnyHashable : Any]) { func saveSplitViewState(to state: inout [AnyHashable: Any]) {
guard let splitView = splitViewController?.splitView else { guard let splitView = splitViewController?.splitView else {
return return
} }
let widths = splitView.arrangedSubviews.map{ Int(floor($0.frame.width)) } let widths = splitView.arrangedSubviews.map { Int(floor($0.frame.width)) }
state[MainWindowController.mainWindowWidthsStateKey] = widths state[MainWindowController.mainWindowWidthsStateKey] = widths
} }
func restoreSplitViewState(from state: [AnyHashable : Any]) { func restoreSplitViewState(from state: [AnyHashable: Any]) {
guard let splitView = splitViewController?.splitView, guard let splitView = splitViewController?.splitView,
let widths = state[MainWindowController.mainWindowWidthsStateKey] as? [Int], let widths = state[MainWindowController.mainWindowWidthsStateKey] as? [Int],
widths.count == 3, widths.count == 3,
@ -1269,13 +1268,13 @@ private extension MainWindowController {
func buildToolbarButton(_ itemIdentifier: NSToolbarItem.Identifier, _ title: String, _ image: NSImage, _ selector: String) -> NSToolbarItem { func buildToolbarButton(_ itemIdentifier: NSToolbarItem.Identifier, _ title: String, _ image: NSImage, _ selector: String) -> NSToolbarItem {
let toolbarItem = RSToolbarItem(itemIdentifier: itemIdentifier) let toolbarItem = RSToolbarItem(itemIdentifier: itemIdentifier)
toolbarItem.autovalidates = true toolbarItem.autovalidates = true
let button = NSButton() let button = NSButton()
button.bezelStyle = .texturedRounded button.bezelStyle = .texturedRounded
button.image = image button.image = image
button.imageScaling = .scaleProportionallyDown button.imageScaling = .scaleProportionallyDown
button.action = Selector((selector)) button.action = Selector((selector))
toolbarItem.view = button toolbarItem.view = button
toolbarItem.toolTip = title toolbarItem.toolTip = title
toolbarItem.label = title toolbarItem.label = title
@ -1284,23 +1283,23 @@ private extension MainWindowController {
func buildNewSidebarItemMenu() -> NSMenu { func buildNewSidebarItemMenu() -> NSMenu {
let menu = NSMenu() let menu = NSMenu()
let newFeedItem = NSMenuItem() let newFeedItem = NSMenuItem()
newFeedItem.title = NSLocalizedString("New Feed…", comment: "New Feed") newFeedItem.title = NSLocalizedString("New Feed…", comment: "New Feed")
newFeedItem.action = Selector(("showAddFeedWindow:")) newFeedItem.action = Selector(("showAddFeedWindow:"))
menu.addItem(newFeedItem) menu.addItem(newFeedItem)
let newFolderFeedItem = NSMenuItem() let newFolderFeedItem = NSMenuItem()
newFolderFeedItem.title = NSLocalizedString("New Folder…", comment: "New Folder") newFolderFeedItem.title = NSLocalizedString("New Folder…", comment: "New Folder")
newFolderFeedItem.action = Selector(("showAddFolderWindow:")) newFolderFeedItem.action = Selector(("showAddFolderWindow:"))
menu.addItem(newFolderFeedItem) menu.addItem(newFolderFeedItem)
return menu return menu
} }
func updateArticleThemeMenu() { func updateArticleThemeMenu() {
let articleThemeMenu = NSMenu() let articleThemeMenu = NSMenu()
let defaultThemeItem = NSMenuItem() let defaultThemeItem = NSMenuItem()
defaultThemeItem.title = ArticleTheme.defaultTheme.name defaultThemeItem.title = ArticleTheme.defaultTheme.name
defaultThemeItem.action = #selector(selectArticleTheme(_:)) defaultThemeItem.action = #selector(selectArticleTheme(_:))
@ -1322,4 +1321,3 @@ private extension MainWindowController {
} }
} }

View File

@ -141,4 +141,3 @@ extension NNW3Feed: OPMLRepresentable {
return s return s
} }
} }

View File

@ -74,7 +74,7 @@ private extension NNW3ImportController {
panel.accessoryView = accessoryViewController.view panel.accessoryView = accessoryViewController.view
panel.isAccessoryViewDisclosed = true panel.isAccessoryViewDisclosed = true
panel.title = NSLocalizedString("Choose a Subscriptions.plist file:", comment: "NNW3 Import") panel.title = NSLocalizedString("Choose a Subscriptions.plist file:", comment: "NNW3 Import")
panel.beginSheetModal(for: window) { modalResult in panel.beginSheetModal(for: window) { modalResult in
guard modalResult == .OK, let subscriptionsPlistURL = panel.url else { guard modalResult == .OK, let subscriptionsPlistURL = panel.url else {
return return

View File

@ -22,7 +22,7 @@ final class NNW3OpenPanelAccessoryViewController: NSViewController {
} }
// MARK: - NSViewController // MARK: - NSViewController
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
preconditionFailure("NNW3OpenPanelAccessoryViewController.init(coder) not implemented by design.") preconditionFailure("NNW3OpenPanelAccessoryViewController.init(coder) not implemented by design.")
} }

View File

@ -14,24 +14,24 @@ class ExportOPMLWindowController: NSWindowController {
@IBOutlet weak var accountPopUpButton: NSPopUpButton! @IBOutlet weak var accountPopUpButton: NSPopUpButton!
private weak var hostWindow: NSWindow? private weak var hostWindow: NSWindow?
convenience init() { convenience init() {
self.init(windowNibName: NSNib.Name("ExportOPMLSheet")) self.init(windowNibName: NSNib.Name("ExportOPMLSheet"))
} }
override func windowDidLoad() { override func windowDidLoad() {
accountPopUpButton.removeAllItems() accountPopUpButton.removeAllItems()
let menu = NSMenu() let menu = NSMenu()
accountPopUpButton.menu = menu accountPopUpButton.menu = menu
for oneAccount in AccountManager.shared.sortedAccounts { for oneAccount in AccountManager.shared.sortedAccounts {
let oneMenuItem = NSMenuItem() let oneMenuItem = NSMenuItem()
oneMenuItem.title = oneAccount.nameForDisplay oneMenuItem.title = oneAccount.nameForDisplay
oneMenuItem.representedObject = oneAccount oneMenuItem.representedObject = oneAccount
menu.addItem(oneMenuItem) menu.addItem(oneMenuItem)
if oneAccount.accountID == AppDefaults.shared.exportOPMLAccountID { if oneAccount.accountID == AppDefaults.shared.exportOPMLAccountID {
accountPopUpButton.select(oneMenuItem) accountPopUpButton.select(oneMenuItem)
} }
@ -40,26 +40,26 @@ class ExportOPMLWindowController: NSWindowController {
} }
// MARK: API // MARK: API
func runSheetOnWindow(_ hostWindow: NSWindow) { func runSheetOnWindow(_ hostWindow: NSWindow) {
self.hostWindow = hostWindow self.hostWindow = hostWindow
if AccountManager.shared.accounts.count == 1 { if AccountManager.shared.accounts.count == 1 {
let account = AccountManager.shared.accounts.first! let account = AccountManager.shared.accounts.first!
exportOPML(account: account) exportOPML(account: account)
} else { } else {
hostWindow.beginSheet(window!) hostWindow.beginSheet(window!)
} }
} }
// MARK: Actions // MARK: Actions
@IBAction func cancel(_ sender: Any) { @IBAction func cancel(_ sender: Any) {
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
} }
@IBAction func exportOPML(_ sender: Any) { @IBAction func exportOPML(_ sender: Any) {
guard let menuItem = accountPopUpButton.selectedItem else { guard let menuItem = accountPopUpButton.selectedItem else {
@ -70,11 +70,11 @@ class ExportOPMLWindowController: NSWindowController {
AppDefaults.shared.exportOPMLAccountID = account.accountID AppDefaults.shared.exportOPMLAccountID = account.accountID
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK) hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
exportOPML(account: account) exportOPML(account: account)
} }
func exportOPML(account: Account) { func exportOPML(account: Account) {
let panel = NSSavePanel() let panel = NSSavePanel()
panel.allowedContentTypes = [UTType.opml] panel.allowedContentTypes = [UTType.opml]
panel.allowsOtherFileTypes = false panel.allowsOtherFileTypes = false
@ -83,10 +83,10 @@ class ExportOPMLWindowController: NSWindowController {
panel.nameFieldLabel = NSLocalizedString("Export to:", comment: "Export OPML") panel.nameFieldLabel = NSLocalizedString("Export to:", comment: "Export OPML")
panel.message = NSLocalizedString("Choose a location for the exported OPML file.", comment: "Export OPML") panel.message = NSLocalizedString("Choose a location for the exported OPML file.", comment: "Export OPML")
panel.isExtensionHidden = false panel.isExtensionHidden = false
let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces) let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces)
panel.nameFieldStringValue = "Subscriptions-\(accountName).opml" panel.nameFieldStringValue = "Subscriptions-\(accountName).opml"
panel.beginSheetModal(for: hostWindow!) { result in panel.beginSheetModal(for: hostWindow!) { result in
if result == NSApplication.ModalResponse.OK, let url = panel.url { if result == NSApplication.ModalResponse.OK, let url = panel.url {
DispatchQueue.main.async { DispatchQueue.main.async {
@ -94,14 +94,13 @@ class ExportOPMLWindowController: NSWindowController {
let opmlString = OPMLExporter.OPMLString(with: account, title: filename) let opmlString = OPMLExporter.OPMLString(with: account, title: filename)
do { do {
try opmlString.write(to: url, atomically: true, encoding: String.Encoding.utf8) try opmlString.write(to: url, atomically: true, encoding: String.Encoding.utf8)
} } catch let error as NSError {
catch let error as NSError {
NSApplication.shared.presentError(error) NSApplication.shared.presentError(error)
} }
} }
} }
} }
} }
} }

View File

@ -14,71 +14,71 @@ class ImportOPMLWindowController: NSWindowController {
@IBOutlet weak var accountPopUpButton: NSPopUpButton! @IBOutlet weak var accountPopUpButton: NSPopUpButton!
private weak var hostWindow: NSWindow? private weak var hostWindow: NSWindow?
convenience init() { convenience init() {
self.init(windowNibName: NSNib.Name("ImportOPMLSheet")) self.init(windowNibName: NSNib.Name("ImportOPMLSheet"))
} }
override func windowDidLoad() { override func windowDidLoad() {
accountPopUpButton.removeAllItems() accountPopUpButton.removeAllItems()
let menu = NSMenu() let menu = NSMenu()
accountPopUpButton.menu = menu accountPopUpButton.menu = menu
for oneAccount in AccountManager.shared.sortedActiveAccounts { for oneAccount in AccountManager.shared.sortedActiveAccounts {
if oneAccount.behaviors.contains(.disallowOPMLImports) { if oneAccount.behaviors.contains(.disallowOPMLImports) {
continue continue
} }
let oneMenuItem = NSMenuItem() let oneMenuItem = NSMenuItem()
oneMenuItem.title = oneAccount.nameForDisplay oneMenuItem.title = oneAccount.nameForDisplay
oneMenuItem.representedObject = oneAccount oneMenuItem.representedObject = oneAccount
menu.addItem(oneMenuItem) menu.addItem(oneMenuItem)
if oneAccount.accountID == AppDefaults.shared.importOPMLAccountID { if oneAccount.accountID == AppDefaults.shared.importOPMLAccountID {
accountPopUpButton.select(oneMenuItem) accountPopUpButton.select(oneMenuItem)
} }
} }
} }
// MARK: API // MARK: API
func runSheetOnWindow(_ hostWindow: NSWindow) { func runSheetOnWindow(_ hostWindow: NSWindow) {
self.hostWindow = hostWindow self.hostWindow = hostWindow
if AccountManager.shared.activeAccounts.count == 1 { if AccountManager.shared.activeAccounts.count == 1 {
let account = AccountManager.shared.activeAccounts.first! let account = AccountManager.shared.activeAccounts.first!
importOPML(account: account) importOPML(account: account)
} else { } else {
hostWindow.beginSheet(window!) hostWindow.beginSheet(window!)
} }
} }
// MARK: Actions // MARK: Actions
@IBAction func cancel(_ sender: Any) { @IBAction func cancel(_ sender: Any) {
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
} }
@IBAction func importOPML(_ sender: Any) { @IBAction func importOPML(_ sender: Any) {
guard let menuItem = accountPopUpButton.selectedItem else { guard let menuItem = accountPopUpButton.selectedItem else {
return return
} }
let account = menuItem.representedObject as! Account let account = menuItem.representedObject as! Account
AppDefaults.shared.importOPMLAccountID = account.accountID AppDefaults.shared.importOPMLAccountID = account.accountID
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK) hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
importOPML(account: account) importOPML(account: account)
} }
func importOPML(account: Account) { func importOPML(account: Account) {
let panel = NSOpenPanel() let panel = NSOpenPanel()
panel.canDownloadUbiquitousContents = true panel.canDownloadUbiquitousContents = true
panel.canResolveUbiquitousConflicts = true panel.canResolveUbiquitousConflicts = true
@ -88,7 +88,7 @@ class ImportOPMLWindowController: NSWindowController {
panel.resolvesAliases = true panel.resolvesAliases = true
panel.allowedContentTypes = [UTType.opml, UTType.xml] panel.allowedContentTypes = [UTType.opml, UTType.xml]
panel.allowsOtherFileTypes = false panel.allowsOtherFileTypes = false
panel.beginSheetModal(for: hostWindow!) { modalResult in panel.beginSheetModal(for: hostWindow!) { modalResult in
if modalResult == NSApplication.ModalResponse.OK, let url = panel.url { if modalResult == NSApplication.ModalResponse.OK, let url = panel.url {
account.importOPML(url) { result in account.importOPML(url) { result in
@ -101,8 +101,7 @@ class ImportOPMLWindowController: NSWindowController {
} }
} }
} }
} }
} }

View File

@ -11,7 +11,7 @@ import AppKit
@objc final class SharingServiceDelegate: NSObject, NSSharingServiceDelegate { @objc final class SharingServiceDelegate: NSObject, NSSharingServiceDelegate {
weak var window: NSWindow? weak var window: NSWindow?
init(_ window: NSWindow?) { init(_ window: NSWindow?) {
self.window = window self.window = window
} }
@ -24,9 +24,9 @@ import AppKit
} }
.joined(separator: ", ") .joined(separator: ", ")
} }
func sharingService(_ sharingService: NSSharingService, sourceWindowForShareItems items: [Any], sharingContentScope: UnsafeMutablePointer<NSSharingService.SharingContentScope>) -> NSWindow? { func sharingService(_ sharingService: NSSharingService, sourceWindowForShareItems items: [Any], sharingContentScope: UnsafeMutablePointer<NSSharingService.SharingContentScope>) -> NSWindow? {
return window return window
} }
} }

View File

@ -10,13 +10,13 @@ import AppKit
import RSCore import RSCore
@objc final class SharingServicePickerDelegate: NSObject, NSSharingServicePickerDelegate { @objc final class SharingServicePickerDelegate: NSObject, NSSharingServicePickerDelegate {
private let sharingServiceDelegate: SharingServiceDelegate private let sharingServiceDelegate: SharingServiceDelegate
init(_ window: NSWindow?) { init(_ window: NSWindow?) {
sharingServiceDelegate = SharingServiceDelegate(window) sharingServiceDelegate = SharingServiceDelegate(window)
} }
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] { func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] {
let filteredServices = proposedServices.filter { $0.menuItemTitle != "NetNewsWire" } let filteredServices = proposedServices.filter { $0.menuItemTitle != "NetNewsWire" }
return filteredServices + SharingServicePickerDelegate.customSharingServices(for: items) return filteredServices + SharingServicePickerDelegate.customSharingServices(for: items)
@ -34,7 +34,7 @@ import RSCore
guard let object = items.first else { guard let object = items.first else {
return nil return nil
} }
guard sendToCommand.canSendObject(object, selectedText: nil) else { guard sendToCommand.canSendObject(object, selectedText: nil) else {
return nil return nil
} }

View File

@ -11,7 +11,7 @@ import RSCore
import Account import Account
import RSTree import RSTree
class SidebarCell : NSTableCellView { class SidebarCell: NSTableCellView {
var iconImage: IconImage? { var iconImage: IconImage? {
didSet { didSet {
@ -73,14 +73,14 @@ class SidebarCell : NSTableCellView {
}() }()
private let faviconImageView = IconView() private let faviconImageView = IconView()
private let unreadCountView = UnreadCountView(frame: NSZeroRect) private let unreadCountView = UnreadCountView(frame: NSRect.zero)
override var backgroundStyle: NSView.BackgroundStyle { override var backgroundStyle: NSView.BackgroundStyle {
didSet { didSet {
updateFaviconImage() updateFaviconImage()
} }
} }
override var isFlipped: Bool { override var isFlipped: Bool {
return true return true
} }
@ -89,8 +89,8 @@ class SidebarCell : NSTableCellView {
super.init(frame: frameRect) super.init(frame: frameRect)
commonInit() commonInit()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
@ -99,7 +99,7 @@ class SidebarCell : NSTableCellView {
if let cellAppearance = cellAppearance { if let cellAppearance = cellAppearance {
titleView.font = cellAppearance.textFieldFont titleView.font = cellAppearance.textFieldFont
} }
resizeSubviews(withOldSize: NSZeroSize) resizeSubviews(withOldSize: NSSize.zero)
} }
override func resizeSubviews(withOldSize oldSize: NSSize) { override func resizeSubviews(withOldSize oldSize: NSSize) {
@ -138,10 +138,10 @@ private extension SidebarCell {
titleView.setFrame(ifNotEqualTo: layout.titleRect) titleView.setFrame(ifNotEqualTo: layout.titleRect)
unreadCountView.setFrame(ifNotEqualTo: layout.unreadCountRect) unreadCountView.setFrame(ifNotEqualTo: layout.unreadCountRect)
} }
func updateFaviconImage() { func updateFaviconImage() {
var updatedIconImage = iconImage var updatedIconImage = iconImage
if let iconImage = iconImage, iconImage.isSymbol { if let iconImage = iconImage, iconImage.isSymbol {
if backgroundStyle != .normal { if backgroundStyle != .normal {
let image = iconImage.image.tinted(with: .white) let image = iconImage.image.tinted(with: .white)
@ -163,6 +163,5 @@ private extension SidebarCell {
faviconImageView.iconImage = nil faviconImageView.iconImage = nil
} }
} }
}
}

View File

@ -28,8 +28,7 @@ struct SidebarCellAppearance: Equatable {
imageSize = CGSize(width: 19, height: 19) imageSize = CGSize(width: 19, height: 19)
textFieldFontSize = 13 textFieldFontSize = 13
} }
self.textFieldFont = NSFont.systemFont(ofSize: textFieldFontSize) self.textFieldFont = NSFont.systemFont(ofSize: textFieldFontSize)
} }
} }

View File

@ -16,7 +16,7 @@ struct SidebarCellLayout {
let faviconRect: CGRect let faviconRect: CGRect
let titleRect: CGRect let titleRect: CGRect
let unreadCountRect: CGRect let unreadCountRect: CGRect
init(appearance: SidebarCellAppearance, cellSize: NSSize, shouldShowImage: Bool, textField: NSTextField, unreadCountView: UnreadCountView) { init(appearance: SidebarCellAppearance, cellSize: NSSize, shouldShowImage: Bool, textField: NSTextField, unreadCountView: UnreadCountView) {
let bounds = NSRect(x: 0.0, y: 0.0, width: floor(cellSize.width), height: floor(cellSize.height)) let bounds = NSRect(x: 0.0, y: 0.0, width: floor(cellSize.width), height: floor(cellSize.height))
@ -32,7 +32,7 @@ struct SidebarCellLayout {
var rTextField = NSRect(x: 0.0, y: 0.0, width: textFieldSize.width, height: textFieldSize.height) var rTextField = NSRect(x: 0.0, y: 0.0, width: textFieldSize.width, height: textFieldSize.height)
if shouldShowImage { if shouldShowImage {
rTextField.origin.x = NSMaxX(rFavicon) + appearance.imageMarginRight rTextField.origin.x = rFavicon.maxX + appearance.imageMarginRight
} }
rTextField = rTextField.centeredVertically(in: bounds) rTextField = rTextField.centeredVertically(in: bounds)
@ -42,17 +42,17 @@ struct SidebarCellLayout {
var rUnread = NSRect.zero var rUnread = NSRect.zero
if !unreadCountIsHidden { if !unreadCountIsHidden {
rUnread.size = unreadCountSize rUnread.size = unreadCountSize
rUnread.origin.x = NSMaxX(bounds) - unreadCountSize.width rUnread.origin.x = bounds.maxX - unreadCountSize.width
rUnread = rUnread.centeredVertically(in: bounds) rUnread = rUnread.centeredVertically(in: bounds)
let textFieldMaxX = NSMinX(rUnread) - appearance.unreadCountMarginLeft let textFieldMaxX = rUnread.minX - appearance.unreadCountMarginLeft
if NSMaxX(rTextField) > textFieldMaxX { if rTextField.maxX > textFieldMaxX {
rTextField.size.width = textFieldMaxX - NSMinX(rTextField) rTextField.size.width = textFieldMaxX - rTextField.minX
} }
} }
self.unreadCountRect = rUnread self.unreadCountRect = rUnread
if NSMaxX(rTextField) > NSMaxX(bounds) { if rTextField.maxX > bounds.maxX {
rTextField.size.width = NSMaxX(bounds) - NSMinX(rTextField) rTextField.size.width = bounds.maxX - rTextField.minX
} }
self.titleRect = rTextField self.titleRect = rTextField
} }

View File

@ -23,7 +23,7 @@ import RSCore
super.init() super.init()
} }
func keydown(_ event: NSEvent, in view: NSView) -> Bool { func keydown(_ event: NSEvent, in view: NSView) -> Bool {
if MainWindowKeyboardHandler.shared.keydown(event, in: view) { if MainWindowKeyboardHandler.shared.keydown(event, in: view) {
@ -39,4 +39,3 @@ import RSCore
return true return true
} }
} }

View File

@ -60,11 +60,11 @@ struct PasteboardFeed: Hashable {
let feedID = dictionary[Key.feedID] let feedID = dictionary[Key.feedID]
let editedName = dictionary[Key.editedName] let editedName = dictionary[Key.editedName]
var accountType: AccountType? = nil var accountType: AccountType?
if let accountTypeString = dictionary[Key.accountType], let accountTypeInt = Int(accountTypeString) { if let accountTypeString = dictionary[Key.accountType], let accountTypeInt = Int(accountTypeString) {
accountType = AccountType(rawValue: accountTypeInt) accountType = AccountType(rawValue: accountTypeInt)
} }
self.init(url: url, feedID: feedID, homePageURL: homePageURL, name: name, editedName: editedName, accountID: accountID, accountType: accountType) self.init(url: url, feedID: feedID, homePageURL: homePageURL, name: name, editedName: editedName, accountID: accountID, accountType: accountType)
} }
@ -72,8 +72,7 @@ struct PasteboardFeed: Hashable {
var pasteboardType: NSPasteboard.PasteboardType? var pasteboardType: NSPasteboard.PasteboardType?
if pasteboardItem.types.contains(FeedPasteboardWriter.feedUTIInternalType) { if pasteboardItem.types.contains(FeedPasteboardWriter.feedUTIInternalType) {
pasteboardType = FeedPasteboardWriter.feedUTIInternalType pasteboardType = FeedPasteboardWriter.feedUTIInternalType
} } else if pasteboardItem.types.contains(FeedPasteboardWriter.feedUTIType) {
else if pasteboardItem.types.contains(FeedPasteboardWriter.feedUTIType) {
pasteboardType = FeedPasteboardWriter.feedUTIType pasteboardType = FeedPasteboardWriter.feedUTIType
} }
if let foundType = pasteboardType { if let foundType = pasteboardType {
@ -87,8 +86,7 @@ struct PasteboardFeed: Hashable {
// Check for URL or a string that may be a URL. // Check for URL or a string that may be a URL.
if pasteboardItem.types.contains(.URL) { if pasteboardItem.types.contains(.URL) {
pasteboardType = .URL pasteboardType = .URL
} } else if pasteboardItem.types.contains(.string) {
else if pasteboardItem.types.contains(.string) {
pasteboardType = .string pasteboardType = .string
} }
if let foundType = pasteboardType { if let foundType = pasteboardType {
@ -161,7 +159,6 @@ extension Feed: @retroactive PasteboardWriterOwner {
static let feedUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.feed" static let feedUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.feed"
static let feedUTIInternalType = NSPasteboard.PasteboardType(rawValue: feedUTIInternal) static let feedUTIInternalType = NSPasteboard.PasteboardType(rawValue: feedUTIInternal)
init(feed: Feed) { init(feed: Feed) {
self.feed = feed self.feed = feed
} }

View File

@ -13,7 +13,7 @@ import RSCore
typealias PasteboardFolderDictionary = [String: String] typealias PasteboardFolderDictionary = [String: String]
struct PasteboardFolder: Hashable { struct PasteboardFolder: Hashable {
private struct Key { private struct Key {
static let name = "name" static let name = "name"
// Internal // Internal
@ -21,30 +21,29 @@ struct PasteboardFolder: Hashable {
static let accountID = "accountID" static let accountID = "accountID"
} }
let name: String let name: String
let folderID: String? let folderID: String?
let accountID: String? let accountID: String?
init(name: String, folderID: String?, accountID: String?) { init(name: String, folderID: String?, accountID: String?) {
self.name = name self.name = name
self.folderID = folderID self.folderID = folderID
self.accountID = accountID self.accountID = accountID
} }
// MARK: - Reading // MARK: - Reading
init?(dictionary: PasteboardFolderDictionary) { init?(dictionary: PasteboardFolderDictionary) {
guard let name = dictionary[Key.name] else { guard let name = dictionary[Key.name] else {
return nil return nil
} }
let folderID = dictionary[Key.folderID] let folderID = dictionary[Key.folderID]
let accountID = dictionary[Key.accountID] let accountID = dictionary[Key.accountID]
self.init(name: name, folderID: folderID, accountID: accountID) self.init(name: name, folderID: folderID, accountID: accountID)
} }
init?(pasteboardItem: NSPasteboardItem) { init?(pasteboardItem: NSPasteboardItem) {
var pasteboardType: NSPasteboard.PasteboardType? var pasteboardType: NSPasteboard.PasteboardType?
if pasteboardItem.types.contains(FolderPasteboardWriter.folderUTIInternalType) { if pasteboardItem.types.contains(FolderPasteboardWriter.folderUTIInternalType) {
@ -57,10 +56,10 @@ struct PasteboardFolder: Hashable {
return return
} }
} }
return nil return nil
} }
static func pasteboardFolders(with pasteboard: NSPasteboard) -> Set<PasteboardFolder>? { static func pasteboardFolders(with pasteboard: NSPasteboard) -> Set<PasteboardFolder>? {
guard let items = pasteboard.pasteboardItems else { guard let items = pasteboard.pasteboardItems else {
return nil return nil
@ -68,9 +67,9 @@ struct PasteboardFolder: Hashable {
let folders = items.compactMap { PasteboardFolder(pasteboardItem: $0) } let folders = items.compactMap { PasteboardFolder(pasteboardItem: $0) }
return folders.isEmpty ? nil : Set(folders) return folders.isEmpty ? nil : Set(folders)
} }
// MARK: - Writing // MARK: - Writing
func internalDictionary() -> PasteboardFolderDictionary { func internalDictionary() -> PasteboardFolderDictionary {
var d = PasteboardFeedDictionary() var d = PasteboardFeedDictionary()
d[PasteboardFolder.Key.name] = name d[PasteboardFolder.Key.name] = name
@ -130,7 +129,7 @@ private extension FolderPasteboardWriter {
var pasteboardFolder: PasteboardFolder { var pasteboardFolder: PasteboardFolder {
return PasteboardFolder(name: folder.name ?? "", folderID: String(folder.folderID), accountID: folder.account?.accountID) return PasteboardFolder(name: folder.name ?? "", folderID: String(folder.folderID), accountID: folder.account?.accountID)
} }
var internalDictionary: PasteboardFeedDictionary { var internalDictionary: PasteboardFeedDictionary {
return pasteboardFolder.internalDictionary() return pasteboardFolder.internalDictionary()
} }

View File

@ -18,7 +18,7 @@ final class RenameWindowController: NSWindowController {
@IBOutlet var renamePrompt: NSTextField! @IBOutlet var renamePrompt: NSTextField!
@IBOutlet var newTitleTextField: NSTextField! @IBOutlet var newTitleTextField: NSTextField!
@IBOutlet var renameButton: NSButton! @IBOutlet var renameButton: NSButton!
private var originalTitle: String? private var originalTitle: String?
private var representedObject: Any? private var representedObject: Any?
private var delegate: RenameWindowControllerDelegate? private var delegate: RenameWindowControllerDelegate?

View File

@ -11,12 +11,12 @@ import RSTree
import Account import Account
enum SidebarDeleteItemsAlert { enum SidebarDeleteItemsAlert {
/// Builds a delete confirmation dialog for the supplied nodes /// Builds a delete confirmation dialog for the supplied nodes
static func build(_ nodes: [Node]) -> NSAlert { static func build(_ nodes: [Node]) -> NSAlert {
let alert = NSAlert() let alert = NSAlert()
alert.alertStyle = .warning alert.alertStyle = .warning
if nodes.count == 1 { if nodes.count == 1 {
if let folder = nodes.first?.representedObject as? Folder { if let folder = nodes.first?.representedObject as? Folder {
alert.messageText = NSLocalizedString("Delete Folder", comment: "Delete Folder") alert.messageText = NSLocalizedString("Delete Folder", comment: "Delete Folder")
@ -32,11 +32,11 @@ enum SidebarDeleteItemsAlert {
let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the %d selected items?", comment: "Items delete text") let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the %d selected items?", comment: "Items delete text")
alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, nodes.count) as String alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, nodes.count) as String
} }
alert.addButton(withTitle: NSLocalizedString("Delete", comment: "Delete Account")) alert.addButton(withTitle: NSLocalizedString("Delete", comment: "Delete Account"))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Delete Account")) alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Delete Account"))
return alert return alert
} }
} }

View File

@ -16,7 +16,7 @@ import Account
let treeController: TreeController let treeController: TreeController
static let dragOperationNone = NSDragOperation(rawValue: 0) static let dragOperationNone = NSDragOperation(rawValue: 0)
private var draggedNodes: Set<Node>? = nil private var draggedNodes: Set<Node>?
init(treeController: TreeController) { init(treeController: TreeController) {
self.treeController = treeController self.treeController = treeController
@ -56,7 +56,7 @@ import Account
func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation { func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard) let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)
let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard) let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard)
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) { if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
return SidebarOutlineDataSource.dragOperationNone return SidebarOutlineDataSource.dragOperationNone
} }
let parentNode = nodeForItem(item) let parentNode = nodeForItem(item)
@ -68,7 +68,7 @@ import Account
return validateLocalFoldersDrop(outlineView, draggedFolders, parentNode, index) return validateLocalFoldersDrop(outlineView, draggedFolders, parentNode, index)
} }
} }
if let draggedFeeds = draggedFeeds { if let draggedFeeds = draggedFeeds {
let contentsType = draggedFeedContentsType(draggedFeeds) let contentsType = draggedFeedContentsType(draggedFeeds)
@ -88,11 +88,11 @@ import Account
return SidebarOutlineDataSource.dragOperationNone return SidebarOutlineDataSource.dragOperationNone
} }
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool { func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard) let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)
let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard) let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard)
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) { if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
return false return false
} }
let parentNode = nodeForItem(item) let parentNode = nodeForItem(item)
@ -100,7 +100,7 @@ import Account
if let draggedFolders = draggedFolders { if let draggedFolders = draggedFolders {
return acceptLocalFoldersDrop(outlineView, draggedFolders, parentNode, index) return acceptLocalFoldersDrop(outlineView, draggedFolders, parentNode, index)
} }
if let draggedFeeds = draggedFeeds { if let draggedFeeds = draggedFeeds {
let contentsType = draggedFeedContentsType(draggedFeeds) let contentsType = draggedFeedContentsType(draggedFeeds)
@ -116,7 +116,7 @@ import Account
return false return false
} }
} }
return false return false
} }
} }
@ -159,8 +159,7 @@ private extension SidebarOutlineDataSource {
for feed in draggedFeeds { for feed in draggedFeeds {
if feed.isLocalFeed { if feed.isLocalFeed {
hasLocalFeed = true hasLocalFeed = true
} } else {
else {
hasNonLocalFeed = true hasNonLocalFeed = true
} }
if hasLocalFeed && hasNonLocalFeed { if hasLocalFeed && hasNonLocalFeed {
@ -228,7 +227,7 @@ private extension SidebarOutlineDataSource {
} }
return localDragOperation(parentNode: parentNode) return localDragOperation(parentNode: parentNode)
} }
func localDragOperation(parentNode: Node) -> NSDragOperation { func localDragOperation(parentNode: Node) -> NSDragOperation {
guard let firstDraggedNode = draggedNodes?.first else { return .move } guard let firstDraggedNode = draggedNodes?.first else { return .move }
if sameAccount(firstDraggedNode, parentNode) { if sameAccount(firstDraggedNode, parentNode) {
@ -275,7 +274,7 @@ private extension SidebarOutlineDataSource {
} }
return false return false
} }
func validateLocalFolderDrop(_ outlineView: NSOutlineView, _ draggedFolder: PasteboardFolder, _ parentNode: Node, _ index: Int) -> NSDragOperation { func validateLocalFolderDrop(_ outlineView: NSOutlineView, _ draggedFolder: PasteboardFolder, _ parentNode: Node, _ index: Int) -> NSDragOperation {
guard let dropAccount = parentNode.representedObject as? Account, dropAccount.accountID != draggedFolder.accountID else { guard let dropAccount = parentNode.representedObject as? Account, dropAccount.accountID != draggedFolder.accountID else {
return SidebarOutlineDataSource.dragOperationNone return SidebarOutlineDataSource.dragOperationNone
@ -289,7 +288,7 @@ private extension SidebarOutlineDataSource {
} }
return localDragOperation(parentNode: parentNode) return localDragOperation(parentNode: parentNode)
} }
func validateLocalFoldersDrop(_ outlineView: NSOutlineView, _ draggedFolders: Set<PasteboardFolder>, _ parentNode: Node, _ index: Int) -> NSDragOperation { func validateLocalFoldersDrop(_ outlineView: NSOutlineView, _ draggedFolders: Set<PasteboardFolder>, _ parentNode: Node, _ index: Int) -> NSDragOperation {
guard let dropAccount = parentNode.representedObject as? Account else { guard let dropAccount = parentNode.representedObject as? Account else {
return SidebarOutlineDataSource.dragOperationNone return SidebarOutlineDataSource.dragOperationNone
@ -307,12 +306,12 @@ private extension SidebarOutlineDataSource {
} }
return localDragOperation(parentNode: parentNode) return localDragOperation(parentNode: parentNode)
} }
func copyFeedInAccount(node: Node, to parentNode: Node) { func copyFeedInAccount(node: Node, to parentNode: Node) {
guard let feed = node.representedObject as? Feed, let destination = parentNode.representedObject as? Container else { guard let feed = node.representedObject as? Feed, let destination = parentNode.representedObject as? Container else {
return return
} }
destination.account?.addFeed(feed, to: destination) { result in destination.account?.addFeed(feed, to: destination) { result in
switch result { switch result {
case .success: case .success:
@ -348,7 +347,7 @@ private extension SidebarOutlineDataSource {
let destinationContainer = parentNode.representedObject as? Container else { let destinationContainer = parentNode.representedObject as? Container else {
return return
} }
if let existingFeed = destinationAccount.existingFeed(withURL: feed.url) { if let existingFeed = destinationAccount.existingFeed(withURL: feed.url) {
destinationAccount.addFeed(existingFeed, to: destinationContainer) { result in destinationAccount.addFeed(existingFeed, to: destinationContainer) { result in
switch result { switch result {
@ -386,7 +385,7 @@ private extension SidebarOutlineDataSource {
copyFeedBetweenAccounts(node: node, to: parentNode) copyFeedBetweenAccounts(node: node, to: parentNode)
} }
} }
return true return true
} }
@ -463,13 +462,13 @@ private extension SidebarOutlineDataSource {
guard let draggedNodes = draggedNodes else { guard let draggedNodes = draggedNodes else {
return false return false
} }
for node in draggedNodes { for node in draggedNodes {
if !sameAccount(node, parentNode) { if !sameAccount(node, parentNode) {
copyFolderBetweenAccounts(node: node, to: parentNode) copyFolderBetweenAccounts(node: node, to: parentNode)
} }
} }
return true return true
} }
@ -486,7 +485,7 @@ private extension SidebarOutlineDataSource {
let folder = parentNode.representedObject as? Folder let folder = parentNode.representedObject as? Folder
appDelegate.addFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: folder) appDelegate.addFeed(draggedFeed.url, name: draggedFeed.editedName ?? draggedFeed.name, account: account, folder: folder)
} }
return true return true
} }
@ -505,7 +504,7 @@ private extension SidebarOutlineDataSource {
} }
return false return false
} }
func sameAccount(_ node: Node, _ parentNode: Node) -> Bool { func sameAccount(_ node: Node, _ parentNode: Node) -> Bool {
if let accountID = nodeAccountID(node), let parentAccountID = nodeAccountID(parentNode) { if let accountID = nodeAccountID(node), let parentAccountID = nodeAccountID(parentNode) {
if accountID == parentAccountID { if accountID == parentAccountID {
@ -514,7 +513,7 @@ private extension SidebarOutlineDataSource {
} }
return false return false
} }
func nodeAccount(_ node: Node) -> Account? { func nodeAccount(_ node: Node) -> Account? {
if let account = node.representedObject as? Account { if let account = node.representedObject as? Account {
return account return account
@ -527,11 +526,11 @@ private extension SidebarOutlineDataSource {
} }
} }
func nodeAccountID(_ node: Node) -> String? { func nodeAccountID(_ node: Node) -> String? {
return nodeAccount(node)?.accountID return nodeAccount(node)?.accountID
} }
func nodeHasChildRepresentingAnyDraggedFeed(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool { func nodeHasChildRepresentingAnyDraggedFeed(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
for node in parentNode.childNodes { for node in parentNode.childNodes {
if nodeRepresentsAnyDraggedFeed(node, draggedFeeds) { if nodeRepresentsAnyDraggedFeed(node, draggedFeeds) {
@ -544,7 +543,7 @@ private extension SidebarOutlineDataSource {
func violatesAccountSpecificBehavior(_ dropTargetNode: Node, _ draggedFeed: PasteboardFeed) -> Bool { func violatesAccountSpecificBehavior(_ dropTargetNode: Node, _ draggedFeed: PasteboardFeed) -> Bool {
return violatesAccountSpecificBehavior(dropTargetNode, Set([draggedFeed])) return violatesAccountSpecificBehavior(dropTargetNode, Set([draggedFeed]))
} }
func violatesAccountSpecificBehavior(_ dropTargetNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool { func violatesAccountSpecificBehavior(_ dropTargetNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
if violatesDisallowFeedInRootFolder(dropTargetNode) { if violatesDisallowFeedInRootFolder(dropTargetNode) {
return true return true
@ -553,23 +552,23 @@ private extension SidebarOutlineDataSource {
if violatesDisallowFeedCopyInRootFolder(dropTargetNode, draggedFeeds) { if violatesDisallowFeedCopyInRootFolder(dropTargetNode, draggedFeeds) {
return true return true
} }
if violatesDisallowFeedInMultipleFolders(dropTargetNode, draggedFeeds) { if violatesDisallowFeedInMultipleFolders(dropTargetNode, draggedFeeds) {
return true return true
} }
return false return false
} }
func violatesDisallowFeedInRootFolder(_ dropTargetNode: Node) -> Bool { func violatesDisallowFeedInRootFolder(_ dropTargetNode: Node) -> Bool {
guard let parentAccount = nodeAccount(dropTargetNode), parentAccount.behaviors.contains(.disallowFeedInRootFolder) else { guard let parentAccount = nodeAccount(dropTargetNode), parentAccount.behaviors.contains(.disallowFeedInRootFolder) else {
return false return false
} }
if dropTargetNode.representedObject is Account { if dropTargetNode.representedObject is Account {
return true return true
} }
return false return false
} }
@ -577,17 +576,17 @@ private extension SidebarOutlineDataSource {
guard let dropTargetAccount = nodeAccount(dropTargetNode), dropTargetAccount.behaviors.contains(.disallowFeedCopyInRootFolder) else { guard let dropTargetAccount = nodeAccount(dropTargetNode), dropTargetAccount.behaviors.contains(.disallowFeedCopyInRootFolder) else {
return false return false
} }
for draggedFeed in draggedFeeds { for draggedFeed in draggedFeeds {
if dropTargetAccount.accountID != draggedFeed.accountID { if dropTargetAccount.accountID != draggedFeed.accountID {
return false return false
} }
} }
if dropTargetNode.representedObject is Account && (NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false) { if dropTargetNode.representedObject is Account && (NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false) {
return true return true
} }
return false return false
} }
@ -595,7 +594,7 @@ private extension SidebarOutlineDataSource {
guard let dropTargetAccount = nodeAccount(dropTargetNode), dropTargetAccount.behaviors.contains(.disallowFeedInMultipleFolders) else { guard let dropTargetAccount = nodeAccount(dropTargetNode), dropTargetAccount.behaviors.contains(.disallowFeedInMultipleFolders) else {
return false return false
} }
for draggedFeed in draggedFeeds { for draggedFeed in draggedFeeds {
if dropTargetAccount.accountID == draggedFeed.accountID { if dropTargetAccount.accountID == draggedFeed.accountID {
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false { if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
@ -607,7 +606,7 @@ private extension SidebarOutlineDataSource {
} }
} }
} }
return false return false
} }
@ -627,7 +626,7 @@ private extension SidebarOutlineDataSource {
let draggedFolderNode = Node(representedObject: draggedFolderWrapper, parent: nil) let draggedFolderNode = Node(representedObject: draggedFolderWrapper, parent: nil)
draggedFolderNode.canHaveChildNodes = true draggedFolderNode.canHaveChildNodes = true
let nodes = parentNode.childNodes + [draggedFolderNode] let nodes = parentNode.childNodes + [draggedFolderNode]
// Revisit if the tree controller can ever be sorted in some other way. // Revisit if the tree controller can ever be sorted in some other way.
let sortedNodes = nodes.sortedAlphabeticallyWithFoldersAtEnd() let sortedNodes = nodes.sortedAlphabeticallyWithFoldersAtEnd()
let index = sortedNodes.firstIndex(of: draggedFolderNode)! let index = sortedNodes.firstIndex(of: draggedFolderNode)!
@ -648,12 +647,12 @@ final class PasteboardFeedObjectWrapper: DisplayNameProvider {
} }
final class PasteboardFolderObjectWrapper: DisplayNameProvider { final class PasteboardFolderObjectWrapper: DisplayNameProvider {
var nameForDisplay: String { var nameForDisplay: String {
return pasteboardFolder.name return pasteboardFolder.name
} }
let pasteboardFolder: PasteboardFolder let pasteboardFolder: PasteboardFolder
init(pasteboardFolder: PasteboardFolder) { init(pasteboardFolder: PasteboardFolder) {
self.pasteboardFolder = pasteboardFolder self.pasteboardFolder = pasteboardFolder
} }

View File

@ -10,7 +10,7 @@ import AppKit
import RSCore import RSCore
import RSTree import RSTree
class SidebarOutlineView : NSOutlineView { class SidebarOutlineView: NSOutlineView {
@IBOutlet var keyboardDelegate: KeyboardDelegate! @IBOutlet var keyboardDelegate: KeyboardDelegate!
@ -40,15 +40,15 @@ class SidebarOutlineView : NSOutlineView {
// MARK: NSView // MARK: NSView
override func viewWillStartLiveResize() { override func viewWillStartLiveResize() {
if let scrollView = self.enclosingScrollView { if let scrollView = self.enclosingScrollView {
scrollView.hasVerticalScroller = false scrollView.hasVerticalScroller = false
} }
super.viewWillStartLiveResize() super.viewWillStartLiveResize()
} }
override func viewDidEndLiveResize() { override func viewDidEndLiveResize() {
if let scrollView = self.enclosingScrollView { if let scrollView = self.enclosingScrollView {
scrollView.hasVerticalScroller = true scrollView.hasVerticalScroller = true
} }

View File

@ -32,8 +32,8 @@ final class SidebarStatusBarView: NSView {
let progressLabelFontSize = progressLabel.font?.pointSize ?? 13.0 let progressLabelFontSize = progressLabel.font?.pointSize ?? 13.0
progressLabel.font = NSFont.monospacedDigitSystemFont(ofSize: progressLabelFontSize, weight: NSFont.Weight.regular) progressLabel.font = NSFont.monospacedDigitSystemFont(ofSize: progressLabelFontSize, weight: NSFont.Weight.regular)
progressLabel.stringValue = "" progressLabel.stringValue = ""
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .combinedRefreshProgressDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .combinedRefreshProgressDidChange, object: nil)
} }

View File

@ -68,7 +68,7 @@ extension SidebarViewController {
guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [Any] else { guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [Any] else {
return return
} }
let articles = unreadArticles(for: objects) let articles = unreadArticles(for: objects)
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else { guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else {
return return
@ -80,7 +80,7 @@ extension SidebarViewController {
guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [AnyObject] else { guard let menuItem = sender as? NSMenuItem, let objects = menuItem.representedObject as? [AnyObject] else {
return return
} }
let nodes = objects.compactMap { treeController.nodeInTreeRepresentingObject($0) } let nodes = objects.compactMap { treeController.nodeInTreeRepresentingObject($0) }
let alert = SidebarDeleteItemsAlert.build(nodes) let alert = SidebarDeleteItemsAlert.build(nodes)
@ -103,7 +103,7 @@ extension SidebarViewController {
} }
window.beginSheet(renameSheet) window.beginSheet(renameSheet)
} }
@objc func toggleNotificationsFromContextMenu(_ sender: Any?) { @objc func toggleNotificationsFromContextMenu(_ sender: Any?) {
guard let item = sender as? NSMenuItem, guard let item = sender as? NSMenuItem,
let feed = item.representedObject as? Feed else { let feed = item.representedObject as? Feed else {
@ -119,7 +119,7 @@ extension SidebarViewController {
NotificationCenter.default.post(Notification(name: .DidUpdateFeedPreferencesFromContextMenu)) NotificationCenter.default.post(Notification(name: .DidUpdateFeedPreferencesFromContextMenu))
} }
} else { } else {
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { (granted, error) in UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { (granted, _) in
if granted { if granted {
DispatchQueue.main.async { DispatchQueue.main.async {
if feed.isNotifyAboutNewArticles == nil { feed.isNotifyAboutNewArticles = false } if feed.isNotifyAboutNewArticles == nil { feed.isNotifyAboutNewArticles = false }
@ -134,7 +134,7 @@ extension SidebarViewController {
} }
} }
} }
@objc func toggleArticleExtractorFromContextMenu(_ sender: Any?) { @objc func toggleArticleExtractorFromContextMenu(_ sender: Any?) {
guard let item = sender as? NSMenuItem, guard let item = sender as? NSMenuItem,
let feed = item.representedObject as? Feed else { let feed = item.representedObject as? Feed else {
@ -144,7 +144,7 @@ extension SidebarViewController {
feed.isArticleExtractorAlwaysOn?.toggle() feed.isArticleExtractorAlwaysOn?.toggle()
NotificationCenter.default.post(Notification(name: .DidUpdateFeedPreferencesFromContextMenu)) NotificationCenter.default.post(Notification(name: .DidUpdateFeedPreferencesFromContextMenu))
} }
func showNotificationsNotEnabledAlert() { func showNotificationsNotEnabledAlert() {
DispatchQueue.main.async { DispatchQueue.main.async {
let alert = NSAlert() let alert = NSAlert()
@ -163,7 +163,7 @@ extension SidebarViewController {
} }
} }
} }
} }
extension SidebarViewController: RenameWindowControllerDelegate { extension SidebarViewController: RenameWindowControllerDelegate {
@ -229,9 +229,9 @@ private extension SidebarViewController {
menu.addItem(item) menu.addItem(item)
} }
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
let notificationText = feed.notificationDisplayName.capitalized let notificationText = feed.notificationDisplayName.capitalized
let notificationMenuItem = menuItem(notificationText, #selector(toggleNotificationsFromContextMenu(_:)), feed) let notificationMenuItem = menuItem(notificationText, #selector(toggleNotificationsFromContextMenu(_:)), feed)
if feed.isNotifyAboutNewArticles == nil || feed.isNotifyAboutNewArticles! == false { if feed.isNotifyAboutNewArticles == nil || feed.isNotifyAboutNewArticles! == false {
notificationMenuItem.state = .off notificationMenuItem.state = .off
@ -251,7 +251,7 @@ private extension SidebarViewController {
menu.addItem(articleExtractorMenuItem) menu.addItem(articleExtractorMenuItem)
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
menu.addItem(renameMenuItem(feed)) menu.addItem(renameMenuItem(feed))
menu.addItem(deleteMenuItem([feed])) menu.addItem(deleteMenuItem([feed]))
@ -362,4 +362,3 @@ private extension SidebarViewController {
return articles return articles
} }
} }

View File

@ -23,9 +23,9 @@ protocol SidebarDelegate: AnyObject {
} }
@objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner { @objc class SidebarViewController: NSViewController, NSOutlineViewDelegate, NSMenuDelegate, UndoableCommandRunner {
@IBOutlet weak var outlineView: NSOutlineView! @IBOutlet weak var outlineView: NSOutlineView!
weak var delegate: SidebarDelegate? weak var delegate: SidebarDelegate?
private let rebuildTreeAndRestoreSelectionQueue = CoalescingQueue(name: "Rebuild Tree Queue", interval: 1.0) private let rebuildTreeAndRestoreSelectionQueue = CoalescingQueue(name: "Rebuild Tree Queue", interval: 1.0)
@ -36,7 +36,7 @@ protocol SidebarDelegate: AnyObject {
lazy var dataSource: SidebarOutlineDataSource = { lazy var dataSource: SidebarOutlineDataSource = {
return SidebarOutlineDataSource(treeController: treeController) return SidebarOutlineDataSource(treeController: treeController)
}() }()
var isReadFiltered: Bool { var isReadFiltered: Bool {
get { get {
return treeControllerDelegate.isReadFiltered return treeControllerDelegate.isReadFiltered
@ -89,19 +89,19 @@ protocol SidebarDelegate: AnyObject {
} }
} }
expandNodes() expandNodes()
} }
// MARK: State Restoration // MARK: State Restoration
func saveState(to state: inout [AnyHashable : Any]) { func saveState(to state: inout [AnyHashable: Any]) {
state[UserInfoKey.readFeedsFilterState] = isReadFiltered state[UserInfoKey.readFeedsFilterState] = isReadFiltered
state[UserInfoKey.containerExpandedWindowState] = expandedTable.map { $0.userInfo } state[UserInfoKey.containerExpandedWindowState] = expandedTable.map { $0.userInfo }
state[UserInfoKey.selectedFeedsState] = selectedFeeds.compactMap { $0.sidebarItemID?.userInfo } state[UserInfoKey.selectedFeedsState] = selectedFeeds.compactMap { $0.sidebarItemID?.userInfo }
} }
func restoreState(from state: [AnyHashable : Any]) { func restoreState(from state: [AnyHashable: Any]) {
if let containerExpandedWindowState = state[UserInfoKey.containerExpandedWindowState] as? [[AnyHashable: AnyHashable]] { if let containerExpandedWindowState = state[UserInfoKey.containerExpandedWindowState] as? [[AnyHashable: AnyHashable]] {
let containerIdentifiers = containerExpandedWindowState.compactMap( { ContainerIdentifier(userInfo: $0) }) let containerIdentifiers = containerExpandedWindowState.compactMap( { ContainerIdentifier(userInfo: $0) })
expandedTable = Set(containerIdentifiers) expandedTable = Set(containerIdentifiers)
@ -117,7 +117,7 @@ protocol SidebarDelegate: AnyObject {
} }
rebuildTreeAndReloadDataIfNeeded() rebuildTreeAndReloadDataIfNeeded()
var selectIndexes = IndexSet() var selectIndexes = IndexSet()
func selectFeedsVisitor(node: Node) { func selectFeedsVisitor(node: Node) {
@ -131,12 +131,12 @@ protocol SidebarDelegate: AnyObject {
treeController.visitNodes(selectFeedsVisitor(node:)) treeController.visitNodes(selectFeedsVisitor(node:))
outlineView.selectRowIndexes(selectIndexes, byExtendingSelection: false) outlineView.selectRowIndexes(selectIndexes, byExtendingSelection: false)
focus() focus()
if let readFeedsFilterState = state[UserInfoKey.readFeedsFilterState] as? Bool { if let readFeedsFilterState = state[UserInfoKey.readFeedsFilterState] as? Bool {
isReadFiltered = readFeedsFilterState isReadFiltered = readFeedsFilterState
} }
} }
// MARK: - Notifications // MARK: - Notifications
@objc func unreadCountDidInitialize(_ notification: Notification) { @objc func unreadCountDidInitialize(_ notification: Notification) {
@ -152,7 +152,7 @@ protocol SidebarDelegate: AnyObject {
guard let representedObject = note.object else { guard let representedObject = note.object else {
return return
} }
if let timelineViewController = representedObject as? TimelineViewController { if let timelineViewController = representedObject as? TimelineViewController {
configureUnreadCountForCellsForRepresentedObjects(timelineViewController.representedObjects) configureUnreadCountForCellsForRepresentedObjects(timelineViewController.representedObjects)
} else { } else {
@ -175,7 +175,7 @@ protocol SidebarDelegate: AnyObject {
@objc func accountsDidChange(_ notification: Notification) { @objc func accountsDidChange(_ notification: Notification) {
rebuildTreeAndRestoreSelection() rebuildTreeAndRestoreSelection()
} }
@objc func accountStateDidChange(_ notification: Notification) { @objc func accountStateDidChange(_ notification: Notification) {
rebuildTreeAndRestoreSelection() rebuildTreeAndRestoreSelection()
} }
@ -183,7 +183,7 @@ protocol SidebarDelegate: AnyObject {
@objc func batchUpdateDidPerform(_ notification: Notification) { @objc func batchUpdateDidPerform(_ notification: Notification) {
rebuildTreeAndRestoreSelection() rebuildTreeAndRestoreSelection()
} }
@objc func userDidAddFeed(_ notification: Notification) { @objc func userDidAddFeed(_ notification: Notification) {
guard let feed = notification.userInfo?[UserInfoKey.feed] else { guard let feed = notification.userInfo?[UserInfoKey.feed] else {
return return
@ -199,7 +199,7 @@ protocol SidebarDelegate: AnyObject {
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else { return } guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else { return }
configureCellsForRepresentedObject(feed) configureCellsForRepresentedObject(feed)
} }
@objc func feedSettingDidChange(_ note: Notification) { @objc func feedSettingDidChange(_ note: Notification) {
guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.FeedSettingUserInfoKey] as? String else { guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.FeedSettingUserInfoKey] as? String else {
return return
@ -228,18 +228,18 @@ protocol SidebarDelegate: AnyObject {
self.restoreSelection(to: savedSelection, sendNotificationIfChanged: true) self.restoreSelection(to: savedSelection, sendNotificationIfChanged: true)
} }
} }
// MARK: - Actions // MARK: - Actions
@IBAction func delete(_ sender: AnyObject?) { @IBAction func delete(_ sender: AnyObject?) {
let availableSelectedNodes = selectedNodes.filter { !($0.representedObject is PseudoFeed) } let availableSelectedNodes = selectedNodes.filter { !($0.representedObject is PseudoFeed) }
if availableSelectedNodes.isEmpty { if availableSelectedNodes.isEmpty {
return return
} }
let alert = SidebarDeleteItemsAlert.build(availableSelectedNodes) let alert = SidebarDeleteItemsAlert.build(availableSelectedNodes)
alert.beginSheetModal(for: view.window!) { [weak self] result in alert.beginSheetModal(for: view.window!) { [weak self] result in
if result == NSApplication.ModalResponse.alertFirstButtonReturn { if result == NSApplication.ModalResponse.alertFirstButtonReturn {
guard let self = self else { return } guard let self = self else { return }
@ -252,7 +252,7 @@ protocol SidebarDelegate: AnyObject {
} }
} }
} }
@IBAction func doubleClickedSidebar(_ sender: Any?) { @IBAction func doubleClickedSidebar(_ sender: Any?) {
guard outlineView.clickedRow == outlineView.selectedRow else { guard outlineView.clickedRow == outlineView.selectedRow else {
return return
@ -300,20 +300,20 @@ protocol SidebarDelegate: AnyObject {
} }
// MARK: - Navigation // MARK: - Navigation
func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool { func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool {
if let _ = nextSelectableRowWithUnreadArticle(wrappingToTop: wrapping) { if let _ = nextSelectableRowWithUnreadArticle(wrappingToTop: wrapping) {
return true return true
} }
return false return false
} }
func goToNextUnread(wrappingToTop wrapping: Bool = false) { func goToNextUnread(wrappingToTop wrapping: Bool = false) {
guard let row = nextSelectableRowWithUnreadArticle(wrappingToTop: wrapping) else { guard let row = nextSelectableRowWithUnreadArticle(wrappingToTop: wrapping) else {
assertionFailure("goToNextUnread called before checking if there is a next unread.") assertionFailure("goToNextUnread called before checking if there is a next unread.")
return return
} }
NSCursor.setHiddenUntilMouseMoves(true) NSCursor.setHiddenUntilMouseMoves(true)
outlineView.selectRowIndexes(IndexSet([row]), byExtendingSelection: false) outlineView.selectRowIndexes(IndexSet([row]), byExtendingSelection: false)
outlineView.scrollTo(row: row) outlineView.scrollTo(row: row)
@ -339,13 +339,13 @@ protocol SidebarDelegate: AnyObject {
// If the clickedRow is part of the selected rows, then do a contextual menu for all the selected rows. // If the clickedRow is part of the selected rows, then do a contextual menu for all the selected rows.
return contextualMenuForSelectedObjects() return contextualMenuForSelectedObjects()
} }
let object = node.representedObject let object = node.representedObject
return menu(for: [object]) return menu(for: [object])
} }
// MARK: - NSMenuDelegate // MARK: - NSMenuDelegate
public func menuNeedsUpdate(_ menu: NSMenu) { public func menuNeedsUpdate(_ menu: NSMenu) {
menu.removeAllItems() menu.removeAllItems()
guard let contextualMenu = contextualMenuForClickedRows() else { guard let contextualMenu = contextualMenuForClickedRows() else {
@ -354,9 +354,8 @@ protocol SidebarDelegate: AnyObject {
menu.takeItems(from: contextualMenu) menu.takeItems(from: contextualMenu)
} }
// MARK: - NSOutlineViewDelegate // MARK: - NSOutlineViewDelegate
func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? { func outlineView(_ outlineView: NSOutlineView, viewFor tableColumn: NSTableColumn?, item: Any) -> NSView? {
let node = item as! Node let node = item as! Node
@ -398,7 +397,7 @@ protocol SidebarDelegate: AnyObject {
func outlineViewSelectionDidChange(_ notification: Notification) { func outlineViewSelectionDidChange(_ notification: Notification) {
selectionDidChange(selectedObjects.isEmpty ? nil : selectedObjects) selectionDidChange(selectedObjects.isEmpty ? nil : selectedObjects)
} }
func outlineViewItemDidExpand(_ notification: Notification) { func outlineViewItemDidExpand(_ notification: Notification) {
guard let node = notification.userInfo?["NSObject"] as? Node, guard let node = notification.userInfo?["NSObject"] as? Node,
let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else { let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else {
@ -409,7 +408,7 @@ protocol SidebarDelegate: AnyObject {
delegate?.sidebarInvalidatedRestorationState(self) delegate?.sidebarInvalidatedRestorationState(self)
} }
} }
func outlineViewItemDidCollapse(_ notification: Notification) { func outlineViewItemDidCollapse(_ notification: Notification) {
guard let node = notification.userInfo?["NSObject"] as? Node, guard let node = notification.userInfo?["NSObject"] as? Node,
let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else { let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID else {
@ -420,43 +419,43 @@ protocol SidebarDelegate: AnyObject {
delegate?.sidebarInvalidatedRestorationState(self) delegate?.sidebarInvalidatedRestorationState(self)
} }
} }
//MARK: - Node Manipulation // MARK: - Node Manipulation
func deleteNodes(_ nodes: [Node]) { func deleteNodes(_ nodes: [Node]) {
let nodesToDelete = treeController.normalizedSelectedNodes(nodes) let nodesToDelete = treeController.normalizedSelectedNodes(nodes)
guard let undoManager = undoManager, let deleteCommand = DeleteCommand(nodesToDelete: nodesToDelete, treeController: treeController, undoManager: undoManager, errorHandler: ErrorHandler.present) else { guard let undoManager = undoManager, let deleteCommand = DeleteCommand(nodesToDelete: nodesToDelete, treeController: treeController, undoManager: undoManager, errorHandler: ErrorHandler.present) else {
return return
} }
animatingChanges = true animatingChanges = true
outlineView.beginUpdates() outlineView.beginUpdates()
let indexSetsGroupedByParent = Node.indexSetsGroupedByParent(nodesToDelete) let indexSetsGroupedByParent = Node.indexSetsGroupedByParent(nodesToDelete)
for (parent, indexSet) in indexSetsGroupedByParent { for (parent, indexSet) in indexSetsGroupedByParent {
outlineView.removeItems(at: indexSet, inParent: parent.isRoot ? nil : parent, withAnimation: [.slideDown]) outlineView.removeItems(at: indexSet, inParent: parent.isRoot ? nil : parent, withAnimation: [.slideDown])
} }
outlineView.endUpdates() outlineView.endUpdates()
runCommand(deleteCommand) runCommand(deleteCommand)
animatingChanges = false animatingChanges = false
} }
// MARK: - API // MARK: - API
func selectFeed(_ feed: SidebarItem) { func selectFeed(_ feed: SidebarItem) {
if isReadFiltered, let feedID = feed.sidebarItemID { if isReadFiltered, let feedID = feed.sidebarItemID {
self.treeControllerDelegate.addFilterException(feedID) self.treeControllerDelegate.addFilterException(feedID)
if let feed = feed as? Feed, let account = feed.account { if let feed = feed as? Feed, let account = feed.account {
let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(feed) }) let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(feed) })
if let parentFolderFeedID = parentFolder?.sidebarItemID { if let parentFolderFeedID = parentFolder?.sidebarItemID {
self.treeControllerDelegate.addFilterException(parentFolderFeedID) self.treeControllerDelegate.addFilterException(parentFolderFeedID)
} }
} }
addTreeControllerToFilterExceptions() addTreeControllerToFilterExceptions()
rebuildTreeAndRestoreSelection() rebuildTreeAndRestoreSelection()
} }
@ -464,7 +463,7 @@ protocol SidebarDelegate: AnyObject {
revealAndSelectRepresentedObject(feed as AnyObject) revealAndSelectRepresentedObject(feed as AnyObject)
} }
func deepLinkRevealAndSelect(for userInfo: [AnyHashable : Any]) { func deepLinkRevealAndSelect(for userInfo: [AnyHashable: Any]) {
guard let accountNode = findAccountNode(userInfo), guard let accountNode = findAccountNode(userInfo),
let feedNode = findFeedNode(userInfo, beginningAt: accountNode), let feedNode = findFeedNode(userInfo, beginningAt: accountNode),
let feed = feedNode.representedObject as? SidebarItem else { let feed = feedNode.representedObject as? SidebarItem else {
@ -482,7 +481,7 @@ protocol SidebarDelegate: AnyObject {
delegate?.sidebarInvalidatedRestorationState(self) delegate?.sidebarInvalidatedRestorationState(self)
rebuildTreeAndRestoreSelection() rebuildTreeAndRestoreSelection()
} }
} }
// MARK: - NSUserInterfaceValidations // MARK: - NSUserInterfaceValidations
@ -497,21 +496,21 @@ extension SidebarViewController: NSUserInterfaceValidations {
} }
} }
//MARK: - Private // MARK: - Private
private extension SidebarViewController { private extension SidebarViewController {
var accountNodes: [Account] { var accountNodes: [Account] {
return treeController.rootNode.childNodes.compactMap { $0.representedObject as? Account } return treeController.rootNode.childNodes.compactMap { $0.representedObject as? Account }
} }
var selectedNodes: [Node] { var selectedNodes: [Node] {
if let nodes = outlineView.selectedItems as? [Node] { if let nodes = outlineView.selectedItems as? [Node] {
return nodes return nodes
} }
return [Node]() return [Node]()
} }
var selectedFeeds: [SidebarItem] { var selectedFeeds: [SidebarItem] {
selectedNodes.compactMap { $0.representedObject as? SidebarItem } selectedNodes.compactMap { $0.representedObject as? SidebarItem }
} }
@ -529,13 +528,13 @@ private extension SidebarViewController {
} }
return node.representedObject as? Feed return node.representedObject as? Feed
} }
func addAllSelectedToFilterExceptions() { func addAllSelectedToFilterExceptions() {
for feed in selectedFeeds { for feed in selectedFeeds {
addToFilterExceptionsIfNecessary(feed) addToFilterExceptionsIfNecessary(feed)
} }
} }
func addToFilterExceptionsIfNecessary(_ feed: SidebarItem?) { func addToFilterExceptionsIfNecessary(_ feed: SidebarItem?) {
if isReadFiltered, let feedID = feed?.sidebarItemID { if isReadFiltered, let feedID = feed?.sidebarItemID {
if feed is PseudoFeed { if feed is PseudoFeed {
@ -552,29 +551,28 @@ private extension SidebarViewController {
} }
} }
} }
func addParentFolderToFilterExceptions(_ feed: SidebarItem) { func addParentFolderToFilterExceptions(_ feed: SidebarItem) {
guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject), guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject),
let folder = node.parent?.representedObject as? Folder, let folder = node.parent?.representedObject as? Folder,
let folderFeedID = folder.sidebarItemID else { let folderFeedID = folder.sidebarItemID else {
return return
} }
treeControllerDelegate.addFilterException(folderFeedID) treeControllerDelegate.addFilterException(folderFeedID)
} }
func queueRebuildTreeAndRestoreSelection() { func queueRebuildTreeAndRestoreSelection() {
rebuildTreeAndRestoreSelectionQueue.add(self, #selector(rebuildTreeAndRestoreSelection)) rebuildTreeAndRestoreSelectionQueue.add(self, #selector(rebuildTreeAndRestoreSelection))
} }
@objc func rebuildTreeAndRestoreSelection() { @objc func rebuildTreeAndRestoreSelection() {
let savedAccounts = accountNodes let savedAccounts = accountNodes
let savedSelection = selectedNodes let savedSelection = selectedNodes
rebuildTreeAndReloadDataIfNeeded() rebuildTreeAndReloadDataIfNeeded()
restoreSelection(to: savedSelection, sendNotificationIfChanged: true) restoreSelection(to: savedSelection, sendNotificationIfChanged: true)
// Automatically expand any new or newly active accounts // Automatically expand any new or newly active accounts
for account in AccountManager.shared.activeAccounts { for account in AccountManager.shared.activeAccounts {
if !savedAccounts.contains(account) { if !savedAccounts.contains(account) {
@ -583,7 +581,7 @@ private extension SidebarViewController {
} }
} }
} }
func rebuildTreeAndReloadDataIfNeeded() { func rebuildTreeAndReloadDataIfNeeded() {
if !animatingChanges && !BatchUpdate.shared.isPerforming { if !animatingChanges && !BatchUpdate.shared.isPerforming {
addAllSelectedToFilterExceptions() addAllSelectedToFilterExceptions()
@ -593,11 +591,11 @@ private extension SidebarViewController {
expandNodes() expandNodes()
} }
} }
func expandNodes() { func expandNodes() {
treeController.visitNodes(expandNodesVisitor(node:)) treeController.visitNodes(expandNodesVisitor(node:))
} }
func expandNodesVisitor(node: Node) { func expandNodesVisitor(node: Node) {
if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID { if let containerID = (node.representedObject as? ContainerIdentifiable)?.containerID {
if expandedTable.contains(containerID) { if expandedTable.contains(containerID) {
@ -607,7 +605,7 @@ private extension SidebarViewController {
} }
} }
} }
func addTreeControllerToFilterExceptions() { func addTreeControllerToFilterExceptions() {
treeController.visitNodes(addTreeControllerToFilterExceptionsVisitor(node:)) treeController.visitNodes(addTreeControllerToFilterExceptionsVisitor(node:))
} }
@ -654,13 +652,13 @@ private extension SidebarViewController {
if row < 0 || row >= outlineView.numberOfRows { if row < 0 || row >= outlineView.numberOfRows {
return nil return nil
} }
if let node = outlineView.item(atRow: row) as? Node { if let node = outlineView.item(atRow: row) as? Node {
return node return node
} }
return nil return nil
} }
func rowHasAtLeastOneUnreadArticle(_ row: Int) -> Bool { func rowHasAtLeastOneUnreadArticle(_ row: Int) -> Bool {
if let oneNode = nodeForRow(row) { if let oneNode = nodeForRow(row) {
if let unreadCountProvider = oneNode.representedObject as? UnreadCountProvider { if let unreadCountProvider = oneNode.representedObject as? UnreadCountProvider {
@ -716,15 +714,15 @@ private extension SidebarViewController {
return row return row
} }
} }
return nil return nil
} }
func findAccountNode(_ userInfo: [AnyHashable : Any]?) -> Node? { func findAccountNode(_ userInfo: [AnyHashable: Any]?) -> Node? {
guard let accountID = userInfo?[ArticlePathKey.accountID] as? String else { guard let accountID = userInfo?[ArticlePathKey.accountID] as? String else {
return nil return nil
} }
if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) { if let node = treeController.rootNode.descendantNode(where: { ($0.representedObject as? Account)?.accountID == accountID }) {
return node return node
} }
@ -739,8 +737,8 @@ private extension SidebarViewController {
return nil return nil
} }
func findFeedNode(_ userInfo: [AnyHashable : Any]?, beginningAt startingNode: Node) -> Node? { func findFeedNode(_ userInfo: [AnyHashable: Any]?, beginningAt startingNode: Node) -> Node? {
guard let feedID = userInfo?[ArticlePathKey.feedID] as? String else { guard let feedID = userInfo?[ArticlePathKey.feedID] as? String else {
return nil return nil
} }
@ -749,7 +747,7 @@ private extension SidebarViewController {
} }
return nil return nil
} }
func configure(_ cell: SidebarCell, _ node: Node) { func configure(_ cell: SidebarCell, _ node: Node) {
cell.cellAppearance = SidebarCellAppearance(rowSizeStyle: outlineView.effectiveRowSizeStyle) cell.cellAppearance = SidebarCellAppearance(rowSizeStyle: outlineView.effectiveRowSizeStyle)
cell.name = nameFor(node) cell.name = nameFor(node)
@ -819,7 +817,7 @@ private extension SidebarViewController {
} }
func applyToAvailableCells(_ completion: (SidebarCell, Node) -> Void) { func applyToAvailableCells(_ completion: (SidebarCell, Node) -> Void) {
outlineView.enumerateAvailableRowViews { (rowView: NSTableRowView, row: Int) -> Void in outlineView.enumerateAvailableRowViews { (rowView: NSTableRowView, row: Int) in
guard let cell = cellForRowView(rowView), let node = nodeForRow(row) else { guard let cell = cellForRowView(rowView), let node = nodeForRow(row) else {
return return
} }
@ -852,7 +850,7 @@ private extension SidebarViewController {
func revealAndSelectRepresentedObject(_ representedObject: AnyObject) -> Bool { func revealAndSelectRepresentedObject(_ representedObject: AnyObject) -> Bool {
return outlineView.revealAndSelectRepresentedObject(representedObject, treeController) return outlineView.revealAndSelectRepresentedObject(representedObject, treeController)
} }
} }
private extension Node { private extension Node {

View File

@ -8,7 +8,7 @@
import AppKit import AppKit
class UnreadCountView : NSView { class UnreadCountView: NSView {
struct Appearance { struct Appearance {
static let padding = NSEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0) static let padding = NSEdgeInsets(top: 1.0, left: 7.0, bottom: 1.0, right: 7.0)
@ -31,11 +31,11 @@ class UnreadCountView : NSView {
} }
private var intrinsicContentSizeIsValid = false private var intrinsicContentSizeIsValid = false
private var _intrinsicContentSize = NSZeroSize private var _intrinsicContentSize = NSSize.zero
override var intrinsicContentSize: NSSize { override var intrinsicContentSize: NSSize {
if !intrinsicContentSizeIsValid { if !intrinsicContentSizeIsValid {
var size = NSZeroSize var size = NSSize.zero
if unreadCount > 0 { if unreadCount > 0 {
size = textSize() size = textSize()
size.width += (Appearance.padding.left + Appearance.padding.right) size.width += (Appearance.padding.left + Appearance.padding.right)
@ -46,11 +46,11 @@ class UnreadCountView : NSView {
} }
return _intrinsicContentSize return _intrinsicContentSize
} }
override var isFlipped: Bool { override var isFlipped: Bool {
return true return true
} }
override func invalidateIntrinsicContentSize() { override func invalidateIntrinsicContentSize() {
intrinsicContentSizeIsValid = false intrinsicContentSizeIsValid = false
} }
@ -59,7 +59,7 @@ class UnreadCountView : NSView {
private func textSize() -> NSSize { private func textSize() -> NSSize {
if unreadCount < 1 { if unreadCount < 1 {
return NSZeroSize return NSSize.zero
} }
if let cachedSize = UnreadCountView.textSizeCache[unreadCount] { if let cachedSize = UnreadCountView.textSizeCache[unreadCount] {
@ -76,9 +76,9 @@ class UnreadCountView : NSView {
private func textRect() -> NSRect { private func textRect() -> NSRect {
let size = textSize() let size = textSize()
var r = NSZeroRect var r = NSRect.zero
r.size = size r.size = size
r.origin.x = (NSMaxX(bounds) - Appearance.padding.right) - r.size.width r.origin.x = (bounds.maxX - Appearance.padding.right) - r.size.width
r.origin.y = Appearance.padding.top r.origin.y = Appearance.padding.top
return r return r
} }
@ -93,4 +93,3 @@ class UnreadCountView : NSView {
} }
} }
} }

View File

@ -78,11 +78,9 @@ private extension ArticlePasteboardWriter {
} }
if let text = article.contentText { if let text = article.contentText {
s += "\(text)\n\n" s += "\(text)\n\n"
} } else if let summary = article.summary {
else if let summary = article.summary {
s += "\(summary)\n\n" s += "\(summary)\n\n"
} } else if let html = article.contentHTML {
else if let html = article.contentHTML {
let convertedHTML = html.convertingToPlainText() let convertedHTML = html.convertingToPlainText()
s += "\(convertedHTML)\n\n" s += "\(convertedHTML)\n\n"
} }
@ -184,7 +182,6 @@ private extension ArticlePasteboardWriter {
guard let authors = article.authors, !authors.isEmpty else { guard let authors = article.authors, !authors.isEmpty else {
return nil return nil
} }
return authors.map{ authorDictionary($0) } return authors.map { authorDictionary($0) }
} }
} }

View File

@ -30,7 +30,7 @@ final class MultilineTextFieldSizer {
private let numberOfLines: Int private let numberOfLines: Int
private let font: NSFont private let font: NSFont
private let textField:NSTextField private let textField: NSTextField
private let singleLineHeightEstimate: Int private let singleLineHeightEstimate: Int
private let doubleLineHeightEstimate: Int private let doubleLineHeightEstimate: Int
private var cache = [String: WidthHeightCache]() // Each string has a cache. private var cache = [String: WidthHeightCache]() // Each string has a cache.
@ -56,7 +56,7 @@ final class MultilineTextFieldSizer {
guard attributedString.length > 0 else { guard attributedString.length > 0 else {
return TextFieldSizeInfo(size: NSSize.zero, numberOfLinesUsed: 0) return TextFieldSizeInfo(size: NSSize.zero, numberOfLinesUsed: 0)
} }
// Assumes the same font family/size for the whole string // Assumes the same font family/size for the whole string
let font = attributedString.attribute(.font, at: 0, effectiveRange: nil) as! NSFont let font = attributedString.attribute(.font, at: 0, effectiveRange: nil) as! NSFont
@ -227,8 +227,7 @@ private extension MultilineTextFieldSizer {
if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) { if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) {
smallNeighbor = (oneWidth, oneHeight) smallNeighbor = (oneWidth, oneHeight)
} } else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) {
else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) {
largeNeighbor = (oneWidth, oneHeight) largeNeighbor = (oneWidth, oneHeight)
} }

View File

@ -38,7 +38,7 @@ final class SingleLineTextFieldSizer {
var calculatedSize = textField.fittingSize var calculatedSize = textField.fittingSize
calculatedSize.height = ceil(calculatedSize.height) calculatedSize.height = ceil(calculatedSize.height)
calculatedSize.width = ceil(calculatedSize.width) calculatedSize.width = ceil(calculatedSize.width)
cache[text] = calculatedSize cache[text] = calculatedSize
return calculatedSize return calculatedSize
} }

View File

@ -13,7 +13,7 @@ struct TimelineCellAppearance: Equatable {
let showIcon: Bool let showIcon: Bool
let cellPadding: NSEdgeInsets let cellPadding: NSEdgeInsets
let feedNameFont: NSFont let feedNameFont: NSFont
let dateFont: NSFont let dateFont: NSFont
@ -22,7 +22,7 @@ struct TimelineCellAppearance: Equatable {
let titleFont: NSFont let titleFont: NSFont
let titleBottomMargin: CGFloat = 1.0 let titleBottomMargin: CGFloat = 1.0
let titleNumberOfLines = 3 let titleNumberOfLines = 3
let textFont: NSFont let textFont: NSFont
let textOnlyFont: NSFont let textOnlyFont: NSFont
@ -55,7 +55,7 @@ struct TimelineCellAppearance: Equatable {
self.textOnlyFont = NSFont.systemFont(ofSize: largeItemFontSize) self.textOnlyFont = NSFont.systemFont(ofSize: largeItemFontSize)
self.showIcon = showIcon self.showIcon = showIcon
cellPadding = NSEdgeInsets(top: 8.0, left: 4.0, bottom: 10.0, right: 4.0) cellPadding = NSEdgeInsets(top: 8.0, left: 4.0, bottom: 10.0, right: 4.0)
let margin = self.cellPadding.left + self.unreadCircleDimension + self.unreadCircleMarginRight let margin = self.cellPadding.left + self.unreadCircleDimension + self.unreadCircleMarginRight

View File

@ -10,9 +10,9 @@ import AppKit
import Articles import Articles
struct TimelineCellData { struct TimelineCellData {
private static let noText = NSLocalizedString("(No Text)", comment: "No Text") private static let noText = NSLocalizedString("(No Text)", comment: "No Text")
let title: String let title: String
let attributedTitle: NSAttributedString let attributedTitle: NSAttributedString
let text: String let text: String
@ -37,7 +37,7 @@ struct TimelineCellData {
} else { } else {
self.text = truncatedSummary self.text = truncatedSummary
} }
self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished) self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished)
if let feedName = feedName { if let feedName = feedName {
@ -45,7 +45,7 @@ struct TimelineCellData {
} else { } else {
self.feedName = "" self.feedName = ""
} }
if let byline = byline { if let byline = byline {
self.byline = byline self.byline = byline
} else { } else {
@ -57,12 +57,12 @@ struct TimelineCellData {
self.showIcon = showIcon self.showIcon = showIcon
self.iconImage = iconImage self.iconImage = iconImage
self.featuredImage = featuredImage self.featuredImage = featuredImage
self.read = article.status.read self.read = article.status.read
self.starred = article.status.starred self.starred = article.status.starred
} }
init() { //Empty init() { // Empty
self.title = "" self.title = ""
self.text = "" self.text = ""
self.dateString = "" self.dateString = ""

View File

@ -10,7 +10,7 @@ import AppKit
import RSCore import RSCore
struct TimelineCellLayout { struct TimelineCellLayout {
let width: CGFloat let width: CGFloat
let height: CGFloat let height: CGFloat
let feedNameRect: NSRect let feedNameRect: NSRect
@ -24,9 +24,9 @@ struct TimelineCellLayout {
let iconImageRect: NSRect let iconImageRect: NSRect
let separatorRect: NSRect let separatorRect: NSRect
let paddingBottom: CGFloat let paddingBottom: CGFloat
init(width: CGFloat, height: CGFloat, feedNameRect: NSRect, dateRect: NSRect, titleRect: NSRect, numberOfLinesForTitle: Int, summaryRect: NSRect, textRect: NSRect, unreadIndicatorRect: NSRect, starRect: NSRect, iconImageRect: NSRect, separatorRect: NSRect, paddingBottom: CGFloat) { init(width: CGFloat, height: CGFloat, feedNameRect: NSRect, dateRect: NSRect, titleRect: NSRect, numberOfLinesForTitle: Int, summaryRect: NSRect, textRect: NSRect, unreadIndicatorRect: NSRect, starRect: NSRect, iconImageRect: NSRect, separatorRect: NSRect, paddingBottom: CGFloat) {
self.width = width self.width = width
self.feedNameRect = feedNameRect self.feedNameRect = feedNameRect
self.dateRect = dateRect self.dateRect = dateRect
@ -42,8 +42,7 @@ struct TimelineCellLayout {
if height > 0.1 { if height > 0.1 {
self.height = height self.height = height
} } else {
else {
self.height = [feedNameRect, dateRect, titleRect, summaryRect, textRect, unreadIndicatorRect, iconImageRect].maxY() + paddingBottom self.height = [feedNameRect, dateRect, titleRect, summaryRect, textRect, unreadIndicatorRect, iconImageRect].maxY() + paddingBottom
} }
} }
@ -62,8 +61,7 @@ struct TimelineCellLayout {
var lastTextRect = titleRect var lastTextRect = titleRect
if numberOfLinesForTitle == 0 { if numberOfLinesForTitle == 0 {
lastTextRect = textRect lastTextRect = textRect
} } else if numberOfLinesForTitle < appearance.titleNumberOfLines {
else if numberOfLinesForTitle < appearance.titleNumberOfLines {
if summaryRect.height > 0.1 { if summaryRect.height > 0.1 {
lastTextRect = summaryRect lastTextRect = summaryRect
} }
@ -114,7 +112,7 @@ private extension TimelineCellLayout {
r.size.height = 0 r.size.height = 0
return (r, 0) return (r, 0)
} }
let attributedTitle = cellData.attributedTitle.adding(font: appearance.titleFont) let attributedTitle = cellData.attributedTitle.adding(font: appearance.titleFont)
let sizeInfo = MultilineTextFieldSizer.size(for: attributedTitle, numberOfLines: appearance.titleNumberOfLines, width: Int(textBoxRect.width)) let sizeInfo = MultilineTextFieldSizer.size(for: attributedTitle, numberOfLines: appearance.titleNumberOfLines, width: Int(textBoxRect.width))
r.size.height = sizeInfo.size.height r.size.height = sizeInfo.size.height
@ -124,15 +122,15 @@ private extension TimelineCellLayout {
return (r, sizeInfo.numberOfLinesUsed) return (r, sizeInfo.numberOfLinesUsed)
} }
static func rectForSummary(_ textBoxRect: NSRect, _ titleRect: NSRect, _ titleNumberOfLines: Int, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect { static func rectForSummary(_ textBoxRect: NSRect, _ titleRect: NSRect, _ titleNumberOfLines: Int, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
if titleNumberOfLines >= appearance.titleNumberOfLines || cellData.text.isEmpty { if titleNumberOfLines >= appearance.titleNumberOfLines || cellData.text.isEmpty {
return NSRect.zero return NSRect.zero
} }
var r = textBoxRect var r = textBoxRect
r.origin.y = NSMaxY(titleRect) r.origin.y = titleRect.maxY
let summaryNumberOfLines = appearance.titleNumberOfLines - titleNumberOfLines let summaryNumberOfLines = appearance.titleNumberOfLines - titleNumberOfLines
let sizeInfo = MultilineTextFieldSizer.size(for: cellData.text, font: appearance.textOnlyFont, numberOfLines: summaryNumberOfLines, width: Int(textBoxRect.width)) let sizeInfo = MultilineTextFieldSizer.size(for: cellData.text, font: appearance.textOnlyFont, numberOfLines: summaryNumberOfLines, width: Int(textBoxRect.width))
r.size.height = sizeInfo.size.height r.size.height = sizeInfo.size.height
if sizeInfo.numberOfLinesUsed < 1 { if sizeInfo.numberOfLinesUsed < 1 {
@ -160,10 +158,10 @@ private extension TimelineCellLayout {
static func rectForDate(_ textBoxRect: NSRect, _ rectAbove: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect { static func rectForDate(_ textBoxRect: NSRect, _ rectAbove: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
let textFieldSize = SingleLineTextFieldSizer.size(for: cellData.dateString, font: appearance.dateFont) let textFieldSize = SingleLineTextFieldSizer.size(for: cellData.dateString, font: appearance.dateFont)
var r = NSZeroRect var r = NSRect.zero
r.size = textFieldSize r.size = textFieldSize
r.origin.y = NSMaxY(rectAbove) + appearance.titleBottomMargin r.origin.y = rectAbove.maxY + appearance.titleBottomMargin
r.size.width = textFieldSize.width r.size.width = textFieldSize.width
r.origin.x = textBoxRect.maxX - textFieldSize.width r.origin.x = textBoxRect.maxX - textFieldSize.width
@ -173,22 +171,22 @@ private extension TimelineCellLayout {
static func rectForFeedName(_ textBoxRect: NSRect, _ dateRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect { static func rectForFeedName(_ textBoxRect: NSRect, _ dateRect: NSRect, _ appearance: TimelineCellAppearance, _ cellData: TimelineCellData) -> NSRect {
if cellData.showFeedName == .none { if cellData.showFeedName == .none {
return NSZeroRect return NSRect.zero
} }
let textFieldSize = SingleLineTextFieldSizer.size(for: cellData.feedName, font: appearance.feedNameFont) let textFieldSize = SingleLineTextFieldSizer.size(for: cellData.feedName, font: appearance.feedNameFont)
var r = NSZeroRect var r = NSRect.zero
r.size = textFieldSize r.size = textFieldSize
r.origin.y = dateRect.minY r.origin.y = dateRect.minY
r.origin.x = textBoxRect.origin.x r.origin.x = textBoxRect.origin.x
r.size.width = (textBoxRect.maxX - (dateRect.size.width + appearance.dateMarginLeft)) - textBoxRect.origin.x r.size.width = (textBoxRect.maxX - (dateRect.size.width + appearance.dateMarginLeft)) - textBoxRect.origin.x
return r return r
} }
static func rectForUnreadIndicator(_ appearance: TimelineCellAppearance, _ titleRect: NSRect) -> NSRect { static func rectForUnreadIndicator(_ appearance: TimelineCellAppearance, _ titleRect: NSRect) -> NSRect {
var r = NSZeroRect var r = NSRect.zero
r.size = NSSize(width: appearance.unreadCircleDimension, height: appearance.unreadCircleDimension) r.size = NSSize(width: appearance.unreadCircleDimension, height: appearance.unreadCircleDimension)
r.origin.x = appearance.cellPadding.left r.origin.x = appearance.cellPadding.left
r.origin.y = titleRect.minY + 6 r.origin.y = titleRect.minY + 6
@ -220,7 +218,7 @@ private extension TimelineCellLayout {
return r return r
} }
static func rectForSeparator(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ alignmentRect: NSRect, _ width: CGFloat, _ height: CGFloat) -> NSRect { static func rectForSeparator(_ cellData: TimelineCellData, _ appearance: TimelineCellAppearance, _ alignmentRect: NSRect, _ width: CGFloat, _ height: CGFloat) -> NSRect {
return NSRect(x: alignmentRect.minX, y: height - 1, width: width - alignmentRect.minX, height: 1) return NSRect(x: alignmentRect.minX, y: height - 1, width: width - alignmentRect.minX, height: 1)
} }
@ -237,4 +235,3 @@ private extension Array where Element == NSRect {
return y return y
} }
} }

View File

@ -14,7 +14,7 @@ class TimelineTableCellView: NSTableCellView {
private let titleView = TimelineTableCellView.multiLineTextField() private let titleView = TimelineTableCellView.multiLineTextField()
private let summaryView = TimelineTableCellView.multiLineTextField() private let summaryView = TimelineTableCellView.multiLineTextField()
private let textView = TimelineTableCellView.multiLineTextField() private let textView = TimelineTableCellView.multiLineTextField()
private let unreadIndicatorView = UnreadIndicatorView(frame: NSZeroRect) private let unreadIndicatorView = UnreadIndicatorView(frame: NSRect.zero)
private let dateView = TimelineTableCellView.singleLineTextField() private let dateView = TimelineTableCellView.singleLineTextField()
private let feedNameView = TimelineTableCellView.singleLineTextField() private let feedNameView = TimelineTableCellView.singleLineTextField()
@ -35,13 +35,13 @@ class TimelineTableCellView: NSTableCellView {
} }
} }
} }
var cellData: TimelineCellData! { var cellData: TimelineCellData! {
didSet { didSet {
updateSubviews() updateSubviews()
} }
} }
var isEmphasized: Bool = false { var isEmphasized: Bool = false {
didSet { didSet {
unreadIndicatorView.isEmphasized = isEmphasized unreadIndicatorView.isEmphasized = isEmphasized
@ -55,7 +55,7 @@ class TimelineTableCellView: NSTableCellView {
updateStarView() updateStarView()
} }
} }
override var isFlipped: Bool { override var isFlipped: Bool {
return true return true
} }
@ -64,8 +64,8 @@ class TimelineTableCellView: NSTableCellView {
super.init(frame: frameRect) super.init(frame: frameRect)
commonInit() commonInit()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
@ -73,29 +73,29 @@ class TimelineTableCellView: NSTableCellView {
convenience init() { convenience init() {
self.init(frame: NSRect.zero) self.init(frame: NSRect.zero)
} }
override func setFrameSize(_ newSize: NSSize) { override func setFrameSize(_ newSize: NSSize) {
if newSize == self.frame.size { if newSize == self.frame.size {
return return
} }
super.setFrameSize(newSize) super.setFrameSize(newSize)
needsLayout = true needsLayout = true
} }
override func viewDidMoveToSuperview() { override func viewDidMoveToSuperview() {
updateSubviews() updateSubviews()
} }
override func layout() { override func layout() {
resizeSubviews(withOldSize: NSZeroSize) resizeSubviews(withOldSize: NSSize.zero)
} }
override func resizeSubviews(withOldSize oldSize: NSSize) { override func resizeSubviews(withOldSize oldSize: NSSize) {
let layoutRects = updatedLayoutRects() let layoutRects = updatedLayoutRects()
setFrame(for: titleView, rect: layoutRects.titleRect) setFrame(for: titleView, rect: layoutRects.titleRect)
@ -144,13 +144,12 @@ private extension TimelineTableCellView {
imageView.imageScaling = scaling imageView.imageScaling = scaling
return imageView return imageView
} }
func setFrame(for textField: NSTextField, rect: NSRect) { func setFrame(for textField: NSTextField, rect: NSRect) {
if Int(floor(rect.height)) == 0 || Int(floor(rect.width)) == 0 { if Int(floor(rect.height)) == 0 || Int(floor(rect.width)) == 0 {
hideView(textField) hideView(textField)
} } else {
else {
showView(textField) showView(textField)
textField.setFrame(ifNotEqualTo: rect) textField.setFrame(ifNotEqualTo: rect)
} }

View File

@ -11,7 +11,7 @@ import AppKit
class UnreadIndicatorView: NSView { class UnreadIndicatorView: NSView {
static let unreadCircleDimension: CGFloat = 8.0 static let unreadCircleDimension: CGFloat = 8.0
var isEmphasized = false { var isEmphasized = false {
didSet { didSet {
if isEmphasized != oldValue { if isEmphasized != oldValue {
@ -19,7 +19,7 @@ class UnreadIndicatorView: NSView {
} }
} }
} }
var isSelected = false { var isSelected = false {
didSet { didSet {
if isSelected != oldValue { if isSelected != oldValue {
@ -41,5 +41,5 @@ class UnreadIndicatorView: NSView {
} }
UnreadIndicatorView.bezierPath.fill() UnreadIndicatorView.bezierPath.fill()
} }
} }

View File

@ -34,4 +34,3 @@ final class TimelineContainerView: NSView {
} }
} }
} }

View File

@ -23,7 +23,7 @@ final class TimelineContainerViewController: NSViewController {
@IBOutlet weak var newestToOldestMenuItem: NSMenuItem! @IBOutlet weak var newestToOldestMenuItem: NSMenuItem!
@IBOutlet weak var oldestToNewestMenuItem: NSMenuItem! @IBOutlet weak var oldestToNewestMenuItem: NSMenuItem!
@IBOutlet weak var groupByFeedMenuItem: NSMenuItem! @IBOutlet weak var groupByFeedMenuItem: NSMenuItem!
@IBOutlet weak var readFilteredButton: NSButton! @IBOutlet weak var readFilteredButton: NSButton!
@IBOutlet var containerView: TimelineContainerView! @IBOutlet var containerView: TimelineContainerView!
@ -49,7 +49,7 @@ final class TimelineContainerViewController: NSViewController {
guard let currentTimelineViewController = currentTimelineViewController, mode(for: currentTimelineViewController) == .regular else { return false } guard let currentTimelineViewController = currentTimelineViewController, mode(for: currentTimelineViewController) == .regular else { return false }
return regularTimelineViewController.isCleanUpAvailable return regularTimelineViewController.isCleanUpAvailable
} }
lazy var regularTimelineViewController = { lazy var regularTimelineViewController = {
return TimelineViewController(delegate: self) return TimelineViewController(delegate: self)
}() }()
@ -63,21 +63,21 @@ final class TimelineContainerViewController: NSViewController {
super.viewDidLoad() super.viewDidLoad()
setRepresentedObjects(nil, mode: .regular) setRepresentedObjects(nil, mode: .regular)
showTimeline(for: .regular) showTimeline(for: .regular)
makeMenuItemTitleLarger(newestToOldestMenuItem) makeMenuItemTitleLarger(newestToOldestMenuItem)
makeMenuItemTitleLarger(oldestToNewestMenuItem) makeMenuItemTitleLarger(oldestToNewestMenuItem)
makeMenuItemTitleLarger(groupByFeedMenuItem) makeMenuItemTitleLarger(groupByFeedMenuItem)
updateViewOptionsPopUpButton() updateViewOptionsPopUpButton()
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
} }
// MARK: - Notifications // MARK: - Notifications
@objc func userDefaultsDidChange(_ note: Notification) { @objc func userDefaultsDidChange(_ note: Notification) {
updateViewOptionsPopUpButton() updateViewOptionsPopUpButton()
} }
// MARK: - API // MARK: - API
func setRepresentedObjects(_ objects: [AnyObject]?, mode: TimelineSourceMode) { func setRepresentedObjects(_ objects: [AnyObject]?, mode: TimelineSourceMode) {
@ -107,29 +107,29 @@ final class TimelineContainerViewController: NSViewController {
return false return false
} }
for object in representedObjects { for object in representedObjects {
guard let _ = currentObjects.firstIndex(where: { $0 === object } ) else { guard let _ = currentObjects.firstIndex(where: { $0 === object }) else {
return false return false
} }
} }
return true return true
} }
func cleanUp() { func cleanUp() {
regularTimelineViewController.cleanUp() regularTimelineViewController.cleanUp()
} }
func toggleReadFilter() { func toggleReadFilter() {
regularTimelineViewController.toggleReadFilter() regularTimelineViewController.toggleReadFilter()
updateReadFilterButton() updateReadFilterButton()
} }
// MARK: State Restoration // MARK: State Restoration
func saveState(to state: inout [AnyHashable : Any]) { func saveState(to state: inout [AnyHashable: Any]) {
regularTimelineViewController.saveState(to: &state) regularTimelineViewController.saveState(to: &state)
} }
func restoreState(from state: [AnyHashable : Any]) { func restoreState(from state: [AnyHashable: Any]) {
regularTimelineViewController.restoreState(from: state) regularTimelineViewController.restoreState(from: state)
updateReadFilterButton() updateReadFilterButton()
} }
@ -144,11 +144,11 @@ extension TimelineContainerViewController: TimelineDelegate {
func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed) { func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed) {
delegate?.timelineRequestedFeedSelection(self, feed: feed) delegate?.timelineRequestedFeedSelection(self, feed: feed)
} }
func timelineInvalidatedRestorationState(_: TimelineViewController) { func timelineInvalidatedRestorationState(_: TimelineViewController) {
delegate?.timelineInvalidatedRestorationState(self) delegate?.timelineInvalidatedRestorationState(self)
} }
} }
private extension TimelineContainerViewController { private extension TimelineContainerViewController {
@ -157,7 +157,7 @@ private extension TimelineContainerViewController {
menuItem.attributedTitle = NSAttributedString(string: menuItem.title, menuItem.attributedTitle = NSAttributedString(string: menuItem.title,
attributes: [NSAttributedString.Key.font: NSFont.controlContentFont(ofSize: NSFont.systemFontSize)]) attributes: [NSAttributedString.Key.font: NSFont.controlContentFont(ofSize: NSFont.systemFontSize)])
} }
func timelineViewController(for mode: TimelineSourceMode) -> TimelineViewController { func timelineViewController(for mode: TimelineSourceMode) -> TimelineViewController {
switch mode { switch mode {
case .regular: case .regular:
@ -170,14 +170,13 @@ private extension TimelineContainerViewController {
func mode(for timelineViewController: TimelineViewController) -> TimelineSourceMode { func mode(for timelineViewController: TimelineViewController) -> TimelineSourceMode {
if timelineViewController === regularTimelineViewController { if timelineViewController === regularTimelineViewController {
return .regular return .regular
} } else if timelineViewController === searchTimelineViewController {
else if timelineViewController === searchTimelineViewController {
return .search return .search
} }
assertionFailure("Expected timelineViewController to match either regular or search timelineViewController, but it doesnt.") assertionFailure("Expected timelineViewController to match either regular or search timelineViewController, but it doesnt.")
return .regular // Should never get here. return .regular // Should never get here.
} }
func updateViewOptionsPopUpButton() { func updateViewOptionsPopUpButton() {
if AppDefaults.shared.timelineSortDirection == .orderedAscending { if AppDefaults.shared.timelineSortDirection == .orderedAscending {
newestToOldestMenuItem.state = .off newestToOldestMenuItem.state = .off
@ -188,32 +187,32 @@ private extension TimelineContainerViewController {
oldestToNewestMenuItem.state = .off oldestToNewestMenuItem.state = .off
viewOptionsPopUpButton.setTitle(newestToOldestMenuItem.title) viewOptionsPopUpButton.setTitle(newestToOldestMenuItem.title)
} }
if AppDefaults.shared.timelineGroupByFeed == true { if AppDefaults.shared.timelineGroupByFeed == true {
groupByFeedMenuItem.state = .on groupByFeedMenuItem.state = .on
} else { } else {
groupByFeedMenuItem.state = .off groupByFeedMenuItem.state = .off
} }
} }
func updateReadFilterButton() { func updateReadFilterButton() {
guard currentTimelineViewController == regularTimelineViewController else { guard currentTimelineViewController == regularTimelineViewController else {
readFilteredButton.isHidden = true readFilteredButton.isHidden = true
return return
} }
guard let isReadFiltered = regularTimelineViewController.isReadFiltered else { guard let isReadFiltered = regularTimelineViewController.isReadFiltered else {
readFilteredButton.isHidden = true readFilteredButton.isHidden = true
return return
} }
readFilteredButton.isHidden = false readFilteredButton.isHidden = false
if isReadFiltered { if isReadFiltered {
readFilteredButton.image = AppAssets.filterActive readFilteredButton.image = AppAssets.filterActive
} else { } else {
readFilteredButton.image = AppAssets.filterInactive readFilteredButton.image = AppAssets.filterInactive
} }
} }
} }

View File

@ -8,10 +8,10 @@
import AppKit import AppKit
class TimelineTableRowView : NSTableRowView { class TimelineTableRowView: NSTableRowView {
private var separator: NSView? private var separator: NSView?
override var isOpaque: Bool { override var isOpaque: Bool {
return true return true
} }
@ -21,14 +21,14 @@ class TimelineTableRowView : NSTableRowView {
cellView?.isEmphasized = isEmphasized cellView?.isEmphasized = isEmphasized
} }
} }
override var isSelected: Bool { override var isSelected: Bool {
didSet { didSet {
cellView?.isSelected = isSelected cellView?.isSelected = isSelected
separator?.isHidden = isSelected separator?.isHidden = isSelected
} }
} }
init() { init() {
super.init(frame: NSRect.zero) super.init(frame: NSRect.zero)
} }
@ -36,7 +36,7 @@ class TimelineTableRowView : NSTableRowView {
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
} }
private var cellView: TimelineTableCellView? { private var cellView: TimelineTableCellView? {
for oneSubview in subviews { for oneSubview in subviews {
if let foundView = oneSubview as? TimelineTableCellView { if let foundView = oneSubview as? TimelineTableCellView {
@ -51,7 +51,7 @@ class TimelineTableRowView : NSTableRowView {
addSeparatorView() addSeparatorView()
} }
} }
private func addSeparatorView() { private func addSeparatorView() {
guard let cellView = cellView, separator == nil else { return } guard let cellView = cellView, separator == nil else { return }
separator = NSView() separator = NSView()

View File

@ -10,15 +10,15 @@ import AppKit
import RSCore import RSCore
class TimelineTableView: NSTableView { class TimelineTableView: NSTableView {
weak var keyboardDelegate: KeyboardDelegate? weak var keyboardDelegate: KeyboardDelegate?
override func accessibilityLabel() -> String? { override func accessibilityLabel() -> String? {
return NSLocalizedString("Timeline", comment: "Timeline") return NSLocalizedString("Timeline", comment: "Timeline")
} }
// MARK: - NSResponder // MARK: - NSResponder
override func keyDown(with event: NSEvent) { override func keyDown(with event: NSEvent) {
if keyboardDelegate?.keydown(event, in: self) ?? false { if keyboardDelegate?.keydown(event, in: self) ?? false {
return return
@ -31,14 +31,14 @@ class TimelineTableView: NSTableView {
override var isOpaque: Bool { override var isOpaque: Bool {
return true return true
} }
override func viewWillStartLiveResize() { override func viewWillStartLiveResize() {
if let scrollView = self.enclosingScrollView { if let scrollView = self.enclosingScrollView {
scrollView.hasVerticalScroller = false scrollView.hasVerticalScroller = false
} }
super.viewWillStartLiveResize() super.viewWillStartLiveResize()
} }
override func viewDidEndLiveResize() { override func viewDidEndLiveResize() {
if let scrollView = self.enclosingScrollView { if let scrollView = self.enclosingScrollView {
scrollView.hasVerticalScroller = true scrollView.hasVerticalScroller = true

View File

@ -70,19 +70,19 @@ extension TimelineViewController {
} }
delegate?.timelineRequestedFeedSelection(self, feed: feed) delegate?.timelineRequestedFeedSelection(self, feed: feed)
} }
@objc func markAllInFeedAsRead(_ sender: Any?) { @objc func markAllInFeedAsRead(_ sender: Any?) {
guard let menuItem = sender as? NSMenuItem, let feedArticles = menuItem.representedObject as? ArticleArray else { guard let menuItem = sender as? NSMenuItem, let feedArticles = menuItem.representedObject as? ArticleArray else {
return return
} }
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: feedArticles, markingRead: true, undoManager: undoManager) else { guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: feedArticles, markingRead: true, undoManager: undoManager) else {
return return
} }
runCommand(markReadCommand) runCommand(markReadCommand)
} }
@objc func openInBrowserFromContextualMenu(_ sender: Any?) { @objc func openInBrowserFromContextualMenu(_ sender: Any?) {
guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else { guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
@ -90,7 +90,7 @@ extension TimelineViewController {
} }
Browser.open(urlString, inBackground: false) Browser.open(urlString, inBackground: false)
} }
@objc func copyURLFromContextualMenu(_ sender: Any?) { @objc func copyURLFromContextualMenu(_ sender: Any?) {
guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else { guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else {
return return
@ -106,7 +106,6 @@ extension TimelineViewController {
} }
} }
private extension TimelineViewController { private extension TimelineViewController {
func markArticles(_ articles: [Article], read: Bool) { func markArticles(_ articles: [Article], read: Bool) {
@ -162,7 +161,7 @@ private extension TimelineViewController {
} }
menu.addSeparatorIfNeeded() menu.addSeparatorIfNeeded()
if articles.count == 1, let feed = articles.first!.feed { if articles.count == 1, let feed = articles.first!.feed {
if !(representedObjects?.contains(where: { $0 as? Feed == feed }) ?? false) { if !(representedObjects?.contains(where: { $0 as? Feed == feed }) ?? false) {
menu.addItem(selectFeedInSidebarMenuItem(feed)) menu.addItem(selectFeedInSidebarMenuItem(feed))
@ -171,13 +170,13 @@ private extension TimelineViewController {
menu.addItem(markAllMenuItem) menu.addItem(markAllMenuItem)
} }
} }
if articles.count == 1, let link = articles.first!.preferredLink { if articles.count == 1, let link = articles.first!.preferredLink {
menu.addSeparatorIfNeeded() menu.addSeparatorIfNeeded()
menu.addItem(openInBrowserMenuItem(link)) menu.addItem(openInBrowserMenuItem(link))
menu.addSeparatorIfNeeded() menu.addSeparatorIfNeeded()
menu.addItem(copyArticleURLMenuItem(link)) menu.addItem(copyArticleURLMenuItem(link))
if let externalLink = articles.first?.externalLink, externalLink != link { if let externalLink = articles.first?.externalLink, externalLink != link {
menu.addItem(copyExternalURLMenuItem(externalLink)) menu.addItem(copyExternalURLMenuItem(externalLink))
} }
@ -241,11 +240,11 @@ private extension TimelineViewController {
} }
func markAboveReadMenuItem(_ articles: [Article]) -> NSMenuItem { func markAboveReadMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("Mark Above as Read", comment: "Command"), #selector(markAboveArticlesReadFromContextualMenu(_:)), articles) return menuItem(NSLocalizedString("Mark Above as Read", comment: "Command"), #selector(markAboveArticlesReadFromContextualMenu(_:)), articles)
} }
func markBelowReadMenuItem(_ articles: [Article]) -> NSMenuItem { func markBelowReadMenuItem(_ articles: [Article]) -> NSMenuItem {
return menuItem(NSLocalizedString("Mark Below as Read", comment: "Command"), #selector(markBelowArticlesReadFromContextualMenu(_:)), articles) return menuItem(NSLocalizedString("Mark Below as Read", comment: "Command"), #selector(markBelowArticlesReadFromContextualMenu(_:)), articles)
} }
func selectFeedInSidebarMenuItem(_ feed: Feed) -> NSMenuItem { func selectFeedInSidebarMenuItem(_ feed: Feed) -> NSMenuItem {
@ -265,24 +264,23 @@ private extension TimelineViewController {
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
let menuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String let menuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
return menuItem(menuText, #selector(markAllInFeedAsRead(_:)), articles) return menuItem(menuText, #selector(markAllInFeedAsRead(_:)), articles)
} }
func openInBrowserMenuItem(_ urlString: String) -> NSMenuItem { func openInBrowserMenuItem(_ urlString: String) -> NSMenuItem {
return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlString) return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlString)
} }
func copyArticleURLMenuItem(_ urlString: String) -> NSMenuItem { func copyArticleURLMenuItem(_ urlString: String) -> NSMenuItem {
return menuItem(NSLocalizedString("Copy Article URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString) return menuItem(NSLocalizedString("Copy Article URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString)
} }
func copyExternalURLMenuItem(_ urlString: String) -> NSMenuItem { func copyExternalURLMenuItem(_ urlString: String) -> NSMenuItem {
return menuItem(NSLocalizedString("Copy External URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString) return menuItem(NSLocalizedString("Copy External URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString)
} }
func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem { func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem {
let item = NSMenuItem(title: title, action: action, keyEquivalent: "") let item = NSMenuItem(title: title, action: action, keyEquivalent: "")

View File

@ -12,7 +12,7 @@ import Articles
import Account import Account
import os.log import os.log
protocol TimelineDelegate: AnyObject { protocol TimelineDelegate: AnyObject {
func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?) func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed) func timelineRequestedFeedSelection(_: TimelineViewController, feed: Feed)
func timelineInvalidatedRestorationState(_: TimelineViewController) func timelineInvalidatedRestorationState(_: TimelineViewController)
@ -42,10 +42,10 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
return timelineFeed.defaultReadFilterType == .read return timelineFeed.defaultReadFilterType == .read
} }
} }
var isCleanUpAvailable: Bool { var isCleanUpAvailable: Bool {
let isEligibleForCleanUp: Bool? let isEligibleForCleanUp: Bool?
if representedObjects?.count == 1, let timelineFeed = representedObjects?.first as? SidebarItem, timelineFeed.defaultReadFilterType == .alwaysRead { if representedObjects?.count == 1, let timelineFeed = representedObjects?.first as? SidebarItem, timelineFeed.defaultReadFilterType == .alwaysRead {
isEligibleForCleanUp = true isEligibleForCleanUp = true
} else { } else {
@ -53,14 +53,14 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
guard isEligibleForCleanUp ?? false else { return false } guard isEligibleForCleanUp ?? false else { return false }
let readSelectedCount = selectedArticles.filter({ $0.status.read }).count let readSelectedCount = selectedArticles.filter({ $0.status.read }).count
let readArticleCount = articles.count - unreadCount let readArticleCount = articles.count - unreadCount
let availableToCleanCount = readArticleCount - readSelectedCount let availableToCleanCount = readArticleCount - readSelectedCount
return availableToCleanCount > 0 return availableToCleanCount > 0
} }
var representedObjects: [AnyObject]? { var representedObjects: [AnyObject]? {
didSet { didSet {
if !representedObjectArraysAreEqual(oldValue, representedObjects) { if !representedObjectArraysAreEqual(oldValue, representedObjects) {
@ -196,7 +196,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
self.init(nibName: "TimelineTableView", bundle: nil) self.init(nibName: "TimelineTableView", bundle: nil)
self.delegate = delegate self.delegate = delegate
} }
override func viewDidLoad() { override func viewDidLoad() {
cellAppearance = TimelineCellAppearance(showIcon: false, fontSize: fontSize) cellAppearance = TimelineCellAppearance(showIcon: false, fontSize: fontSize)
cellAppearanceWithIcon = TimelineCellAppearance(showIcon: true, fontSize: fontSize) cellAppearanceWithIcon = TimelineCellAppearance(showIcon: true, fontSize: fontSize)
@ -208,7 +208,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
tableView.setDraggingSourceOperationMask(.copy, forLocal: false) tableView.setDraggingSourceOperationMask(.copy, forLocal: false)
tableView.keyboardDelegate = keyboardDelegate tableView.keyboardDelegate = keyboardDelegate
tableView.style = .inset tableView.style = .inset
if !didRegisterForNotifications { if !didRegisterForNotifications {
NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(statusesDidChange(_:)), name: .StatusesDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil)
@ -223,13 +223,13 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
didRegisterForNotifications = true didRegisterForNotifications = true
} }
} }
override func viewDidAppear() { override func viewDidAppear() {
sharingServiceDelegate = SharingServiceDelegate(self.view.window) sharingServiceDelegate = SharingServiceDelegate(self.view.window)
} }
// MARK: - API // MARK: - API
func markAllAsRead(completion: (() -> Void)? = nil) { func markAllAsRead(completion: (() -> Void)? = nil) {
guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager, completion: completion) else { guard let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager, completion: completion) else {
return return
@ -254,7 +254,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
return representedObjects.first! === object return representedObjects.first! === object
} }
func cleanUp() { func cleanUp() {
fetchAndReplacePreservingSelection() fetchAndReplacePreservingSelection()
} }
@ -265,19 +265,19 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
delegate?.timelineInvalidatedRestorationState(self) delegate?.timelineInvalidatedRestorationState(self)
fetchAndReplacePreservingSelection() fetchAndReplacePreservingSelection()
} }
// MARK: State Restoration // MARK: State Restoration
func saveState(to state: inout [AnyHashable : Any]) { func saveState(to state: inout [AnyHashable: Any]) {
state[UserInfoKey.readArticlesFilterStateKeys] = readFilterEnabledTable.keys.compactMap { $0.userInfo } state[UserInfoKey.readArticlesFilterStateKeys] = readFilterEnabledTable.keys.compactMap { $0.userInfo }
state[UserInfoKey.readArticlesFilterStateValues] = readFilterEnabledTable.values.compactMap( { $0 }) state[UserInfoKey.readArticlesFilterStateValues] = readFilterEnabledTable.values.compactMap( { $0 })
if selectedArticles.count == 1 { if selectedArticles.count == 1 {
state[UserInfoKey.articlePath] = selectedArticles.first!.pathUserInfo state[UserInfoKey.articlePath] = selectedArticles.first!.pathUserInfo
} }
} }
func restoreState(from state: [AnyHashable : Any]) { func restoreState(from state: [AnyHashable: Any]) {
guard let readArticlesFilterStateKeys = state[UserInfoKey.readArticlesFilterStateKeys] as? [[AnyHashable: AnyHashable]], guard let readArticlesFilterStateKeys = state[UserInfoKey.readArticlesFilterStateKeys] as? [[AnyHashable: AnyHashable]],
let readArticlesFilterStateValues = state[UserInfoKey.readArticlesFilterStateValues] as? [Bool] else { let readArticlesFilterStateValues = state[UserInfoKey.readArticlesFilterStateValues] as? [Bool] else {
return return
@ -288,15 +288,15 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
readFilterEnabledTable[feedIdentifier] = readArticlesFilterStateValues[i] readFilterEnabledTable[feedIdentifier] = readArticlesFilterStateValues[i]
} }
} }
if let articlePathUserInfo = state[UserInfoKey.articlePath] as? [AnyHashable : Any], if let articlePathUserInfo = state[UserInfoKey.articlePath] as? [AnyHashable: Any],
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
let account = AccountManager.shared.existingAccount(with: accountID), let account = AccountManager.shared.existingAccount(with: accountID),
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String { let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String {
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID) exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: articleID)
fetchAndReplaceArticlesSync() fetchAndReplaceArticlesSync()
if let selectedIndex = articles.firstIndex(where: { $0.articleID == articleID }) { if let selectedIndex = articles.firstIndex(where: { $0.articleID == articleID }) {
tableView.selectRow(selectedIndex) tableView.selectRow(selectedIndex)
DispatchQueue.main.async { DispatchQueue.main.async {
@ -306,13 +306,13 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
} else { } else {
fetchAndReplaceArticlesSync() fetchAndReplaceArticlesSync()
} }
} }
// MARK: - Actions // MARK: - Actions
@objc func openArticleInBrowser(_ sender: Any?) { @objc func openArticleInBrowser(_ sender: Any?) {
@ -320,7 +320,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false) Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false)
} }
} }
@IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) { @IBAction func toggleStatusOfSelectedArticles(_ sender: Any?) {
guard !selectedArticles.isEmpty else { guard !selectedArticles.isEmpty else {
return return
@ -331,8 +331,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
if markAsRead { if markAsRead {
markSelectedArticlesAsRead(sender) markSelectedArticlesAsRead(sender)
} } else {
else {
markSelectedArticlesAsUnread(sender) markSelectedArticlesAsUnread(sender)
} }
} }
@ -343,7 +342,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
runCommand(markReadCommand) runCommand(markReadCommand)
} }
@IBAction func markSelectedArticlesAsUnread(_ sender: Any?) { @IBAction func markSelectedArticlesAsUnread(_ sender: Any?) {
guard let undoManager = undoManager, let markUnreadCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: false, undoManager: undoManager) else { guard let undoManager = undoManager, let markUnreadCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: false, undoManager: undoManager) else {
return return
@ -359,36 +358,36 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
guard let lastSelectedRow = tableView.selectedRowIndexes.last else { guard let lastSelectedRow = tableView.selectedRowIndexes.last else {
return return
} }
let nextRowIndex = lastSelectedRow - 1 let nextRowIndex = lastSelectedRow - 1
if nextRowIndex <= 0 { if nextRowIndex <= 0 {
tableView.scrollTo(row: 0, extraHeight: 0) tableView.scrollTo(row: 0, extraHeight: 0)
} }
tableView.selectRow(nextRowIndex) tableView.selectRow(nextRowIndex)
let followingRowIndex = nextRowIndex - 1 let followingRowIndex = nextRowIndex - 1
if followingRowIndex < 0 { if followingRowIndex < 0 {
return return
} }
tableView.scrollToRowIfNotVisible(followingRowIndex) tableView.scrollToRowIfNotVisible(followingRowIndex)
} }
@IBAction func selectNextDown(_ sender: Any?) { @IBAction func selectNextDown(_ sender: Any?) {
guard let firstSelectedRow = tableView.selectedRowIndexes.first else { guard let firstSelectedRow = tableView.selectedRowIndexes.first else {
return return
} }
let tableMaxIndex = tableView.numberOfRows - 1 let tableMaxIndex = tableView.numberOfRows - 1
let nextRowIndex = firstSelectedRow + 1 let nextRowIndex = firstSelectedRow + 1
if nextRowIndex >= tableMaxIndex { if nextRowIndex >= tableMaxIndex {
tableView.scrollTo(row: tableMaxIndex, extraHeight: 0) tableView.scrollTo(row: tableMaxIndex, extraHeight: 0)
} }
tableView.selectRow(nextRowIndex) tableView.selectRow(nextRowIndex)
let followingRowIndex = nextRowIndex + 1 let followingRowIndex = nextRowIndex + 1
if followingRowIndex > tableMaxIndex { if followingRowIndex > tableMaxIndex {
return return
@ -397,11 +396,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
tableView.scrollToRowIfNotVisible(followingRowIndex) tableView.scrollToRowIfNotVisible(followingRowIndex)
} }
func toggleReadStatusForSelectedArticles() { func toggleReadStatusForSelectedArticles() {
// If any one of the selected articles is unread, then mark them as read. // If any one of the selected articles is unread, then mark them as read.
// If all articles are read, then mark them as unread them. // If all articles are read, then mark them as unread them.
let commandStatus = markReadCommandStatus() let commandStatus = markReadCommandStatus()
let markingRead: Bool let markingRead: Bool
switch commandStatus { switch commandStatus {
@ -412,14 +411,14 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
case .canDoNothing: case .canDoNothing:
return return
} }
guard let undoManager = undoManager, let markStarredCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: markingRead, undoManager: undoManager) else { guard let undoManager = undoManager, let markStarredCommand = MarkStatusCommand(initialArticles: selectedArticles, markingRead: markingRead, undoManager: undoManager) else {
return return
} }
runCommand(markStarredCommand) runCommand(markStarredCommand)
} }
func toggleStarredStatusForSelectedArticles() { func toggleStarredStatusForSelectedArticles() {
// If any one of the selected articles is not starred, then star them. // If any one of the selected articles is not starred, then star them.
@ -448,11 +447,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
func markReadCommandStatus() -> MarkCommandValidationStatus { func markReadCommandStatus() -> MarkCommandValidationStatus {
let articles = selectedArticles let articles = selectedArticles
if articles.anyArticleIsUnread() { if articles.anyArticleIsUnread() {
return .canMark return .canMark
} }
if articles.anyArticleIsReadAndCanMarkUnread() { if articles.anyArticleIsReadAndCanMarkUnread() {
return .canUnmark return .canUnmark
} }
@ -485,12 +484,11 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
func markOlderArticlesRead(_ selectedArticles: [Article]) { func markOlderArticlesRead(_ selectedArticles: [Article]) {
// Mark articles older than the selectedArticles(s) as read. // Mark articles older than the selectedArticles(s) as read.
var cutoffDate: Date? = nil var cutoffDate: Date?
for article in selectedArticles { for article in selectedArticles {
if cutoffDate == nil { if cutoffDate == nil {
cutoffDate = article.logicalDatePublished cutoffDate = article.logicalDatePublished
} } else if cutoffDate! > article.logicalDatePublished {
else if cutoffDate! > article.logicalDatePublished {
cutoffDate = article.logicalDatePublished cutoffDate = article.logicalDatePublished
} }
} }
@ -530,8 +528,8 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
// MARK: - Navigation // MARK: - Navigation
func goToDeepLink(for userInfo: [AnyHashable : Any]) { func goToDeepLink(for userInfo: [AnyHashable: Any]) {
guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return } guard let articleID = userInfo[ArticlePathKey.articleID] as? String else { return }
if isReadFiltered ?? false { if isReadFiltered ?? false {
@ -543,12 +541,12 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
guard let ix = articles.firstIndex(where: { $0.articleID == articleID }) else { return } guard let ix = articles.firstIndex(where: { $0.articleID == articleID }) else { return }
NSCursor.setHiddenUntilMouseMoves(true) NSCursor.setHiddenUntilMouseMoves(true)
tableView.selectRow(ix) tableView.selectRow(ix)
tableView.scrollTo(row: ix) tableView.scrollTo(row: ix)
} }
func goToNextUnread(wrappingToTop wrapping: Bool = false) { func goToNextUnread(wrappingToTop wrapping: Bool = false) {
guard let ix = indexOfNextUnreadArticle() else { guard let ix = indexOfNextUnreadArticle() else {
return return
@ -557,14 +555,14 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
tableView.selectRow(ix) tableView.selectRow(ix)
tableView.scrollTo(row: ix) tableView.scrollTo(row: ix)
} }
func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool { func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool {
guard let _ = indexOfNextUnreadArticle(wrappingToTop: wrapping) else { guard let _ = indexOfNextUnreadArticle(wrappingToTop: wrapping) else {
return false return false
} }
return true return true
} }
func indexOfNextUnreadArticle(wrappingToTop wrapping: Bool = false) -> Int? { func indexOfNextUnreadArticle(wrappingToTop wrapping: Bool = false) -> Int? {
return articles.rowOfNextUnreadArticle(tableView.selectedRow, wrappingToTop: wrapping) return articles.rowOfNextUnreadArticle(tableView.selectedRow, wrappingToTop: wrapping)
} }
@ -573,7 +571,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
guard let window = tableView.window else { guard let window = tableView.window else {
return return
} }
window.makeFirstResponderUnlessDescendantIsFirstResponder(tableView) window.makeFirstResponderUnlessDescendantIsFirstResponder(tableView)
if !hasAtLeastOneSelectedArticle && articles.count > 0 { if !hasAtLeastOneSelectedArticle && articles.count > 0 {
tableView.selectRowAndScrollToVisible(0) tableView.selectRowAndScrollToVisible(0)
@ -648,7 +646,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
fetchAndReplaceArticlesAsync() fetchAndReplaceArticlesAsync()
} }
} }
@objc func accountsDidChange(_ note: Notification) { @objc func accountsDidChange(_ note: Notification) {
if representedObjectsContainsAnyPseudoFeed() { if representedObjectsContainsAnyPseudoFeed() {
fetchAndReplaceArticlesAsync() fetchAndReplaceArticlesAsync()
@ -666,7 +664,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
self.sortDirection = AppDefaults.shared.timelineSortDirection self.sortDirection = AppDefaults.shared.timelineSortDirection
self.groupByFeed = AppDefaults.shared.timelineGroupByFeed self.groupByFeed = AppDefaults.shared.timelineGroupByFeed
} }
// MARK: - Reloading Data // MARK: - Reloading Data
private func cellForRowView(_ rowView: NSView) -> NSView? { private func cellForRowView(_ rowView: NSView) -> NSView? {
@ -682,7 +680,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
reloadVisibleCells(for: indexes) reloadVisibleCells(for: indexes)
} }
private func reloadVisibleCells(for articles: [Article]) { private func reloadVisibleCells(for articles: [Article]) {
reloadVisibleCells(for: Set(articles.articleIDs())) reloadVisibleCells(for: Set(articles.articleIDs()))
} }
@ -690,7 +688,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
private func reloadVisibleCells(for articles: Set<Article>) { private func reloadVisibleCells(for articles: Set<Article>) {
reloadVisibleCells(for: articles.articleIDs()) reloadVisibleCells(for: articles.articleIDs())
} }
private func reloadVisibleCells(for articleIDs: Set<String>) { private func reloadVisibleCells(for articleIDs: Set<String>) {
if articleIDs.isEmpty { if articleIDs.isEmpty {
return return
@ -714,7 +712,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
} }
tableView.reloadData(forRowIndexes: indexes, columnIndexes: NSIndexSet(index: 0) as IndexSet) tableView.reloadData(forRowIndexes: indexes, columnIndexes: NSIndexSet(index: 0) as IndexSet)
} }
// MARK: - Cell Configuring // MARK: - Cell Configuring
private func calculateRowHeight() -> CGFloat { private func calculateRowHeight() -> CGFloat {
@ -833,8 +831,7 @@ extension TimelineViewController: NSTableViewDelegate {
cell.cellAppearance = showIcons ? cellAppearanceWithIcon : cellAppearance cell.cellAppearance = showIcons ? cellAppearanceWithIcon : cellAppearance
if let article = articles.articleAtRow(row) { if let article = articles.articleAtRow(row) {
configureTimelineCell(cell, article: article) configureTimelineCell(cell, article: article)
} } else {
else {
makeTimelineCellEmpty(cell) makeTimelineCellEmpty(cell)
} }
} }
@ -917,16 +914,16 @@ extension TimelineViewController: NSTableViewDelegate {
switch edge { switch edge {
case .leading: case .leading:
let action = NSTableViewRowAction(style: .regular, title: article.status.read ? "Unread" : "Read") { (action, row) in let action = NSTableViewRowAction(style: .regular, title: article.status.read ? "Unread" : "Read") { (_, _) in
self.toggleArticleRead(article); self.toggleArticleRead(article)
tableView.rowActionsVisible = false tableView.rowActionsVisible = false
} }
action.image = article.status.read ? AppAssets.swipeMarkUnreadImage : AppAssets.swipeMarkReadImage action.image = article.status.read ? AppAssets.swipeMarkUnreadImage : AppAssets.swipeMarkReadImage
return [action] return [action]
case .trailing: case .trailing:
let action = NSTableViewRowAction(style: .regular, title: article.status.starred ? "Unstar" : "Star") { (action, row) in let action = NSTableViewRowAction(style: .regular, title: article.status.starred ? "Unstar" : "Star") { (_, _) in
self.toggleArticleStarred(article); self.toggleArticleStarred(article)
tableView.rowActionsVisible = false tableView.rowActionsVisible = false
} }
action.backgroundColor = AppAssets.starColor action.backgroundColor = AppAssets.starColor
@ -944,7 +941,7 @@ extension TimelineViewController: NSTableViewDelegate {
// MARK: - Private // MARK: - Private
private extension TimelineViewController { private extension TimelineViewController {
func fetchAndReplacePreservingSelection() { func fetchAndReplacePreservingSelection() {
if let article = oneSelectedArticle, let account = article.account { if let article = oneSelectedArticle, let account = article.account {
exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: article.articleID) exceptionArticleFetcher = SingleArticleFetcher(account: account, articleID: article.articleID)
@ -953,7 +950,7 @@ private extension TimelineViewController {
fetchAndReplaceArticlesSync() fetchAndReplaceArticlesSync()
} }
} }
@objc func reloadAvailableCells() { @objc func reloadAvailableCells() {
if let indexesToReload = tableView.indexesOfAvailableRows() { if let indexesToReload = tableView.indexesOfAvailableRows() {
reloadCells(for: indexesToReload) reloadCells(for: indexesToReload)
@ -988,7 +985,7 @@ private extension TimelineViewController {
self.showIcons = false self.showIcons = false
return return
} }
for article in articles { for article in articles {
if let authors = article.authors { if let authors = article.authors {
for author in authors { for author in authors {
@ -1015,7 +1012,7 @@ private extension TimelineViewController {
replaceArticles(with: unsortedArticles) replaceArticles(with: unsortedArticles)
} }
} }
func selectedArticleIDs() -> [String] { func selectedArticleIDs() -> [String] {
return selectedArticles.articleIDs() return selectedArticles.articleIDs()
} }
@ -1090,7 +1087,7 @@ private extension TimelineViewController {
} }
// MARK: - Fetching Articles // MARK: - Fetching Articles
func fetchAndReplaceArticlesSync() { func fetchAndReplaceArticlesSync() {
// To be called when the user has made a change of selection in the sidebar. // To be called when the user has made a change of selection in the sidebar.
// It blocks the main thread, so that theres no async delay, // It blocks the main thread, so that theres no async delay,
@ -1101,12 +1098,12 @@ private extension TimelineViewController {
emptyTheTimeline() emptyTheTimeline()
return return
} }
if exceptionArticleFetcher != nil { if exceptionArticleFetcher != nil {
representedObjects.append(exceptionArticleFetcher as AnyObject) representedObjects.append(exceptionArticleFetcher as AnyObject)
exceptionArticleFetcher = nil exceptionArticleFetcher = nil
} }
let fetchedArticles = fetchUnsortedArticlesSync(for: representedObjects) let fetchedArticles = fetchUnsortedArticlesSync(for: representedObjects)
replaceArticles(with: fetchedArticles) replaceArticles(with: fetchedArticles)
} }
@ -1119,12 +1116,12 @@ private extension TimelineViewController {
emptyTheTimeline() emptyTheTimeline()
return return
} }
if exceptionArticleFetcher != nil { if exceptionArticleFetcher != nil {
representedObjects.append(exceptionArticleFetcher as AnyObject) representedObjects.append(exceptionArticleFetcher as AnyObject)
exceptionArticleFetcher = nil exceptionArticleFetcher = nil
} }
fetchUnsortedArticlesAsync(for: representedObjects) { [weak self] (articles) in fetchUnsortedArticlesAsync(for: representedObjects) { [weak self] (articles) in
self?.replaceArticles(with: articles) self?.replaceArticles(with: articles)
} }
@ -1141,7 +1138,7 @@ private extension TimelineViewController {
func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set<Article> { func fetchUnsortedArticlesSync(for representedObjects: [Any]) -> Set<Article> {
cancelPendingAsyncFetches() cancelPendingAsyncFetches()
let fetchers = representedObjects.compactMap{ $0 as? ArticleFetcher } let fetchers = representedObjects.compactMap { $0 as? ArticleFetcher }
if fetchers.isEmpty { if fetchers.isEmpty {
return Set<Article>() return Set<Article>()
} }
@ -1236,8 +1233,7 @@ private extension TimelineViewController {
return true return true
} }
} }
} } else if let folder = representedObject as? Folder {
else if let folder = representedObject as? Folder {
for oneFeed in feeds { for oneFeed in feeds {
if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) { if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) {
return true return true

View File

@ -9,33 +9,33 @@
import AppKit import AppKit
class AccountCell: NSTableCellView { class AccountCell: NSTableCellView {
private var originalImage: NSImage? private var originalImage: NSImage?
var isImageTemplateCapable = true var isImageTemplateCapable = true
override func prepareForReuse() { override func prepareForReuse() {
originalImage = nil originalImage = nil
} }
override var backgroundStyle: NSView.BackgroundStyle { override var backgroundStyle: NSView.BackgroundStyle {
didSet { didSet {
updateImage() updateImage()
} }
} }
} }
private extension AccountCell { private extension AccountCell {
func updateImage() { func updateImage() {
guard isImageTemplateCapable else { return } guard isImageTemplateCapable else { return }
if backgroundStyle != .normal { if backgroundStyle != .normal {
guard !(imageView?.image?.isTemplate ?? false) else { return } guard !(imageView?.image?.isTemplate ?? false) else { return }
originalImage = imageView?.image originalImage = imageView?.image
let templateImage = imageView?.image?.copy() as? NSImage let templateImage = imageView?.image?.copy() as? NSImage
templateImage?.isTemplate = true templateImage?.isTemplate = true
imageView?.image = templateImage imageView?.image = templateImage
@ -44,5 +44,5 @@ private extension AccountCell {
imageView?.image = originalImage imageView?.image = originalImage
} }
} }
} }

View File

@ -11,7 +11,7 @@ import Account
enum AccountsAddCloudKitWindowControllerError: LocalizedError { enum AccountsAddCloudKitWindowControllerError: LocalizedError {
case iCloudDriveMissing case iCloudDriveMissing
var errorDescription: String? { var errorDescription: String? {
return NSLocalizedString("Unable to add iCloud Account. Please make sure you have iCloud and iCloud Drive enabled in System Settings.", comment: "Unable to add iCloud Account.") return NSLocalizedString("Unable to add iCloud Account. Please make sure you have iCloud and iCloud Drive enabled in System Settings.", comment: "Unable to add iCloud Account.")
} }
@ -26,35 +26,35 @@ class AccountsAddCloudKitWindowController: NSWindowController {
convenience init() { convenience init() {
self.init(windowNibName: NSNib.Name("AccountsAddCloudKit")) self.init(windowNibName: NSNib.Name("AccountsAddCloudKit"))
} }
override func windowDidLoad() { override func windowDidLoad() {
super.windowDidLoad() super.windowDidLoad()
let attrString = NSAttributedString(linkText: CloudKitWebDocumentation.limitationsAndSolutionsText, linkURL: CloudKitWebDocumentation.limitationsAndSolutionsURL) let attrString = NSAttributedString(linkText: CloudKitWebDocumentation.limitationsAndSolutionsText, linkURL: CloudKitWebDocumentation.limitationsAndSolutionsURL)
limitationsAndSolutionsTextField.attributedStringValue = attrString limitationsAndSolutionsTextField.attributedStringValue = attrString
} }
// MARK: API // MARK: API
func runSheetOnWindow(_ hostWindow: NSWindow, completion: ((NSApplication.ModalResponse) -> Void)? = nil) { func runSheetOnWindow(_ hostWindow: NSWindow, completion: ((NSApplication.ModalResponse) -> Void)? = nil) {
self.hostWindow = hostWindow self.hostWindow = hostWindow
hostWindow.beginSheet(window!, completionHandler: completion) hostWindow.beginSheet(window!, completionHandler: completion)
} }
// MARK: Actions // MARK: Actions
@IBAction func cancel(_ sender: Any) { @IBAction func cancel(_ sender: Any) {
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
} }
@IBAction func create(_ sender: Any) { @IBAction func create(_ sender: Any) {
guard FileManager.default.ubiquityIdentityToken != nil else { guard FileManager.default.ubiquityIdentityToken != nil else {
NSApplication.shared.presentError(AccountsAddCloudKitWindowControllerError.iCloudDriveMissing) NSApplication.shared.presentError(AccountsAddCloudKitWindowControllerError.iCloudDriveMissing)
return return
} }
let _ = AccountManager.shared.createAccount(type: .cloudKit) _ = AccountManager.shared.createAccount(type: .cloudKit)
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK) hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
} }
} }

View File

@ -13,32 +13,32 @@ class AccountsAddLocalWindowController: NSWindowController {
@IBOutlet private weak var nameTextField: NSTextField! @IBOutlet private weak var nameTextField: NSTextField!
@IBOutlet private weak var localAccountNameTextField: NSTextField! @IBOutlet private weak var localAccountNameTextField: NSTextField!
private weak var hostWindow: NSWindow? private weak var hostWindow: NSWindow?
convenience init() { convenience init() {
self.init(windowNibName: NSNib.Name("AccountsAddLocal")) self.init(windowNibName: NSNib.Name("AccountsAddLocal"))
} }
override func windowDidLoad() { override func windowDidLoad() {
super.windowDidLoad() super.windowDidLoad()
localAccountNameTextField.stringValue = NSLocalizedString("Create a local account on your Mac.", comment: "Account Local") localAccountNameTextField.stringValue = NSLocalizedString("Create a local account on your Mac.", comment: "Account Local")
} }
// MARK: API // MARK: API
func runSheetOnWindow(_ hostWindow: NSWindow) { func runSheetOnWindow(_ hostWindow: NSWindow) {
self.hostWindow = hostWindow self.hostWindow = hostWindow
hostWindow.beginSheet(window!) hostWindow.beginSheet(window!)
} }
// MARK: Actions // MARK: Actions
@IBAction func cancel(_ sender: Any) { @IBAction func cancel(_ sender: Any) {
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
} }
@IBAction func create(_ sender: Any) { @IBAction func create(_ sender: Any) {
let account = AccountManager.shared.createAccount(type: .onMyMac) let account = AccountManager.shared.createAccount(type: .onMyMac)
if !nameTextField.stringValue.isEmpty { if !nameTextField.stringValue.isEmpty {
@ -46,5 +46,5 @@ class AccountsAddLocalWindowController: NSWindowController {
} }
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK) hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.OK)
} }
} }

View File

@ -17,7 +17,7 @@ final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate
@IBOutlet weak var limitationsAndSolutionsRow: NSGridRow! @IBOutlet weak var limitationsAndSolutionsRow: NSGridRow!
@IBOutlet weak var limitationsAndSolutionsTextField: NSTextField! @IBOutlet weak var limitationsAndSolutionsTextField: NSTextField!
@IBOutlet weak var credentialsButton: NSButton! @IBOutlet weak var credentialsButton: NSButton!
private var accountsWindowController: NSWindowController? private var accountsWindowController: NSWindowController?
private var account: Account? private var account: Account?
@ -29,7 +29,7 @@ final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate
public required init?(coder: NSCoder) { public required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
} }
private var hidesCredentialsButton: Bool { private var hidesCredentialsButton: Bool {
guard let account = account else { guard let account = account else {
return true return true
@ -41,25 +41,25 @@ final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate
return false return false
} }
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
nameTextField.delegate = self nameTextField.delegate = self
typeLabel.stringValue = account?.defaultName ?? "" typeLabel.stringValue = account?.defaultName ?? ""
nameTextField.stringValue = account?.name ?? "" nameTextField.stringValue = account?.name ?? ""
activeButton.state = account?.isActive ?? false ? .on : .off activeButton.state = account?.isActive ?? false ? .on : .off
if account?.type == .cloudKit { if account?.type == .cloudKit {
let attrString = NSAttributedString(linkText: CloudKitWebDocumentation.limitationsAndSolutionsText, linkURL: CloudKitWebDocumentation.limitationsAndSolutionsURL) let attrString = NSAttributedString(linkText: CloudKitWebDocumentation.limitationsAndSolutionsText, linkURL: CloudKitWebDocumentation.limitationsAndSolutionsURL)
limitationsAndSolutionsTextField.attributedStringValue = attrString limitationsAndSolutionsTextField.attributedStringValue = attrString
} else { } else {
limitationsAndSolutionsRow.isHidden = true limitationsAndSolutionsRow.isHidden = true
} }
credentialsButton.isHidden = hidesCredentialsButton credentialsButton.isHidden = hidesCredentialsButton
} }
func controlTextDidEndEditing(_ obj: Notification) { func controlTextDidEndEditing(_ obj: Notification) {
if !nameTextField.stringValue.isEmpty { if !nameTextField.stringValue.isEmpty {
account?.name = nameTextField.stringValue account?.name = nameTextField.stringValue
@ -67,28 +67,27 @@ final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate
account?.name = nil account?.name = nil
} }
} }
@IBAction func active(_ sender: NSButtonCell) { @IBAction func active(_ sender: NSButtonCell) {
account?.isActive = sender.state == .on ? true : false account?.isActive = sender.state == .on ? true : false
} }
@IBAction func credentials(_ sender: Any) { @IBAction func credentials(_ sender: Any) {
guard let account = account else { return } guard let account = account else { return }
switch account.type { switch account.type {
case .feedbin: case .feedbin:
let accountsFeedbinWindowController = AccountsFeedbinWindowController() let accountsFeedbinWindowController = AccountsFeedbinWindowController()
accountsFeedbinWindowController.account = account accountsFeedbinWindowController.account = account
accountsFeedbinWindowController.runSheetOnWindow(self.view.window!) accountsFeedbinWindowController.runSheetOnWindow(self.view.window!)
accountsWindowController = accountsFeedbinWindowController accountsWindowController = accountsFeedbinWindowController
case .inoreader, .bazQux, .theOldReader, .freshRSS: case .inoreader, .bazQux, .theOldReader, .freshRSS:
let accountsReaderAPIWindowController = AccountsReaderAPIWindowController() let accountsReaderAPIWindowController = AccountsReaderAPIWindowController()
accountsReaderAPIWindowController.accountType = account.type accountsReaderAPIWindowController.accountType = account.type
accountsReaderAPIWindowController.account = account accountsReaderAPIWindowController.account = account
accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!) accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!)
accountsWindowController = accountsReaderAPIWindowController accountsWindowController = accountsReaderAPIWindowController
break
case .newsBlur: case .newsBlur:
let accountsNewsBlurWindowController = AccountsNewsBlurWindowController() let accountsNewsBlurWindowController = AccountsNewsBlurWindowController()
accountsNewsBlurWindowController.account = account accountsNewsBlurWindowController.account = account
@ -97,7 +96,7 @@ final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate
default: default:
break break
} }
} }
} }

View File

@ -21,15 +21,15 @@ class AccountsFeedbinWindowController: NSWindowController {
@IBOutlet weak var passwordTextField: NSSecureTextField! @IBOutlet weak var passwordTextField: NSSecureTextField!
@IBOutlet weak var errorMessageLabel: NSTextField! @IBOutlet weak var errorMessageLabel: NSTextField!
@IBOutlet weak var actionButton: NSButton! @IBOutlet weak var actionButton: NSButton!
var account: Account? var account: Account?
private weak var hostWindow: NSWindow? private weak var hostWindow: NSWindow?
convenience init() { convenience init() {
self.init(windowNibName: NSNib.Name("AccountsFeedbin")) self.init(windowNibName: NSNib.Name("AccountsFeedbin"))
} }
override func windowDidLoad() { override func windowDidLoad() {
if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) { if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) {
usernameTextField.stringValue = credentials.username usernameTextField.stringValue = credentials.username
@ -41,69 +41,69 @@ class AccountsFeedbinWindowController: NSWindowController {
actionButton.title = NSLocalizedString("Create", comment: "Add Account") actionButton.title = NSLocalizedString("Create", comment: "Add Account")
signInTextField.stringValue = NSLocalizedString("Sign in to your Feedbin account.", comment: "SignIn") signInTextField.stringValue = NSLocalizedString("Sign in to your Feedbin account.", comment: "SignIn")
} }
enableAutofill() enableAutofill()
usernameTextField.becomeFirstResponder() usernameTextField.becomeFirstResponder()
} }
// MARK: API // MARK: API
func runSheetOnWindow(_ hostWindow: NSWindow, completion: ((NSApplication.ModalResponse) -> Void)? = nil) { func runSheetOnWindow(_ hostWindow: NSWindow, completion: ((NSApplication.ModalResponse) -> Void)? = nil) {
self.hostWindow = hostWindow self.hostWindow = hostWindow
hostWindow.beginSheet(window!, completionHandler: completion) hostWindow.beginSheet(window!, completionHandler: completion)
} }
// MARK: Actions // MARK: Actions
@IBAction func cancel(_ sender: Any) { @IBAction func cancel(_ sender: Any) {
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
} }
@IBAction func action(_ sender: Any) { @IBAction func action(_ sender: Any) {
self.errorMessageLabel.stringValue = "" self.errorMessageLabel.stringValue = ""
guard !usernameTextField.stringValue.isEmpty && !passwordTextField.stringValue.isEmpty else { guard !usernameTextField.stringValue.isEmpty && !passwordTextField.stringValue.isEmpty else {
self.errorMessageLabel.stringValue = NSLocalizedString("Username & password required.", comment: "Credentials Error") self.errorMessageLabel.stringValue = NSLocalizedString("Username & password required.", comment: "Credentials Error")
return return
} }
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedbin, username: usernameTextField.stringValue) else { guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedbin, username: usernameTextField.stringValue) else {
self.errorMessageLabel.stringValue = NSLocalizedString("There is already a Feedbin account with that username created.", comment: "Duplicate Error") self.errorMessageLabel.stringValue = NSLocalizedString("There is already a Feedbin account with that username created.", comment: "Duplicate Error")
return return
} }
actionButton.isEnabled = false actionButton.isEnabled = false
progressIndicator.isHidden = false progressIndicator.isHidden = false
progressIndicator.startAnimation(self) progressIndicator.startAnimation(self)
let credentials = Credentials(type: .basic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue) let credentials = Credentials(type: .basic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue)
Account.validateCredentials(type: .feedbin, credentials: credentials) { [weak self] result in Account.validateCredentials(type: .feedbin, credentials: credentials) { [weak self] result in
guard let self = self else { return } guard let self = self else { return }
self.actionButton.isEnabled = true self.actionButton.isEnabled = true
self.progressIndicator.isHidden = true self.progressIndicator.isHidden = true
self.progressIndicator.stopAnimation(self) self.progressIndicator.stopAnimation(self)
switch result { switch result {
case .success(let validatedCredentials): case .success(let validatedCredentials):
guard let validatedCredentials = validatedCredentials else { guard let validatedCredentials = validatedCredentials else {
self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error") self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error")
return return
} }
if self.account == nil { if self.account == nil {
self.account = AccountManager.shared.createAccount(type: .feedbin) self.account = AccountManager.shared.createAccount(type: .feedbin)
} }
do { do {
try self.account?.removeCredentials(type: .basic) try self.account?.removeCredentials(type: .basic)
try self.account?.storeCredentials(validatedCredentials) try self.account?.storeCredentials(validatedCredentials)
self.account?.refreshAll() { result in self.account?.refreshAll { result in
switch result { switch result {
case .success: case .success:
break break
@ -111,30 +111,30 @@ class AccountsFeedbinWindowController: NSWindowController {
NSApplication.shared.presentError(error) NSApplication.shared.presentError(error)
} }
} }
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
} catch { } catch {
self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")
} }
case .failure: case .failure:
self.errorMessageLabel.stringValue = NSLocalizedString("Network error. Try again later.", comment: "Credentials Error") self.errorMessageLabel.stringValue = NSLocalizedString("Network error. Try again later.", comment: "Credentials Error")
} }
} }
} }
@IBAction func createAccountWithProvider(_ sender: Any) { @IBAction func createAccountWithProvider(_ sender: Any) {
NSWorkspace.shared.open(URL(string: "https://feedbin.com/signup")!) NSWorkspace.shared.open(URL(string: "https://feedbin.com/signup")!)
} }
// MARK: Autofill // MARK: Autofill
func enableAutofill() { func enableAutofill() {
usernameTextField.contentType = .username usernameTextField.contentType = .username
passwordTextField.contentType = .password passwordTextField.contentType = .password
} }
} }

View File

@ -12,7 +12,7 @@ import RSWeb
import Secrets import Secrets
class AccountsNewsBlurWindowController: NSWindowController { class AccountsNewsBlurWindowController: NSWindowController {
@IBOutlet weak var signInTextField: NSTextField! @IBOutlet weak var signInTextField: NSTextField!
@IBOutlet weak var noAccountTextField: NSTextField! @IBOutlet weak var noAccountTextField: NSTextField!
@IBOutlet weak var createNewAccountButton: NSButton! @IBOutlet weak var createNewAccountButton: NSButton!
@ -65,12 +65,12 @@ class AccountsNewsBlurWindowController: NSWindowController {
self.errorMessageLabel.stringValue = NSLocalizedString("Username required.", comment: "Credentials Error") self.errorMessageLabel.stringValue = NSLocalizedString("Username required.", comment: "Credentials Error")
return return
} }
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .newsBlur, username: usernameTextField.stringValue) else { guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .newsBlur, username: usernameTextField.stringValue) else {
self.errorMessageLabel.stringValue = NSLocalizedString("There is already a NewsBlur account with that username created.", comment: "Duplicate Error") self.errorMessageLabel.stringValue = NSLocalizedString("There is already a NewsBlur account with that username created.", comment: "Duplicate Error")
return return
} }
actionButton.isEnabled = false actionButton.isEnabled = false
progressIndicator.isHidden = false progressIndicator.isHidden = false
progressIndicator.startAnimation(self) progressIndicator.startAnimation(self)
@ -101,7 +101,7 @@ class AccountsNewsBlurWindowController: NSWindowController {
try self.account?.storeCredentials(credentials) try self.account?.storeCredentials(credentials)
try self.account?.storeCredentials(validatedCredentials) try self.account?.storeCredentials(validatedCredentials)
self.account?.refreshAll() { result in self.account?.refreshAll { result in
switch result { switch result {
case .success: case .success:
break break
@ -109,7 +109,7 @@ class AccountsNewsBlurWindowController: NSWindowController {
NSApplication.shared.presentError(error) NSApplication.shared.presentError(error)
} }
} }
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
} catch { } catch {
self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")
@ -122,14 +122,14 @@ class AccountsNewsBlurWindowController: NSWindowController {
} }
} }
} }
@IBAction func createAccountWithProvider(_ sender: Any) { @IBAction func createAccountWithProvider(_ sender: Any) {
NSWorkspace.shared.open(URL(string: "https://newsblur.com")!) NSWorkspace.shared.open(URL(string: "https://newsblur.com")!)
} }
// MARK: Autofill // MARK: Autofill
func enableAutofill() { func enableAutofill() {
usernameTextField.contentType = .username usernameTextField.contentType = .username
passwordTextField.contentType = .password passwordTextField.contentType = .password
} }
} }

View File

@ -35,44 +35,43 @@ final class AccountsPreferencesViewController: NSViewController {
tableView.delegate = self tableView.delegate = self
tableView.dataSource = self tableView.dataSource = self
addAccountDelegate = self addAccountDelegate = self
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidAddAccount, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidAddAccount, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidDeleteAccount, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .UserDidDeleteAccount, object: nil)
// Fix tableView frame  for some reason IB wants it 1pt wider than the clip view. This leads to unwanted horizontal scrolling. // Fix tableView frame  for some reason IB wants it 1pt wider than the clip view. This leads to unwanted horizontal scrolling.
var rTable = tableView.frame var rTable = tableView.frame
rTable.size.width = tableView.superview!.frame.size.width rTable.size.width = tableView.superview!.frame.size.width
tableView.frame = rTable tableView.frame = rTable
hideController() hideController()
} }
@IBAction func addAccount(_ sender: Any) { @IBAction func addAccount(_ sender: Any) {
let controller = NSHostingController(rootView: AddAccountsView(delegate: self)) let controller = NSHostingController(rootView: AddAccountsView(delegate: self))
controller.rootView.parent = controller controller.rootView.parent = controller
addAccountsViewController = controller addAccountsViewController = controller
presentAsSheet(controller) presentAsSheet(controller)
} }
@IBAction func removeAccount(_ sender: Any) { @IBAction func removeAccount(_ sender: Any) {
guard tableView.selectedRow != -1 else { guard tableView.selectedRow != -1 else {
return return
} }
let acctName = sortedAccounts[tableView.selectedRow].nameForDisplay let acctName = sortedAccounts[tableView.selectedRow].nameForDisplay
let alert = NSAlert() let alert = NSAlert()
alert.alertStyle = .warning alert.alertStyle = .warning
let deletePrompt = NSLocalizedString("Delete", comment: "Delete") let deletePrompt = NSLocalizedString("Delete", comment: "Delete")
alert.messageText = "\(deletePrompt)\(acctName)”?" alert.messageText = "\(deletePrompt)\(acctName)”?"
alert.informativeText = NSLocalizedString("Are you sure you want to delete the account “\(acctName)”? This cannot be undone.", comment: "Delete text") alert.informativeText = NSLocalizedString("Are you sure you want to delete the account “\(acctName)”? This cannot be undone.", comment: "Delete text")
alert.addButton(withTitle: NSLocalizedString("Delete", comment: "Delete Account")) alert.addButton(withTitle: NSLocalizedString("Delete", comment: "Delete Account"))
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Delete Account")) alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Delete Account"))
alert.beginSheetModal(for: view.window!) { [weak self] result in alert.beginSheetModal(for: view.window!) { [weak self] result in
if result == NSApplication.ModalResponse.alertFirstButtonReturn { if result == NSApplication.ModalResponse.alertFirstButtonReturn {
guard let self = self else { return } guard let self = self else { return }
@ -80,19 +79,19 @@ final class AccountsPreferencesViewController: NSViewController {
self.hideController() self.hideController()
} }
} }
} }
@objc func displayNameDidChange(_ note: Notification) { @objc func displayNameDidChange(_ note: Notification) {
updateSortedAccounts() updateSortedAccounts()
tableView.reloadData() tableView.reloadData()
} }
@objc func accountsDidChange(_ note: Notification) { @objc func accountsDidChange(_ note: Notification) {
updateSortedAccounts() updateSortedAccounts()
tableView.reloadData() tableView.reloadData()
} }
} }
// MARK: - NSTableViewDataSource // MARK: - NSTableViewDataSource
@ -118,18 +117,18 @@ extension AccountsPreferencesViewController: NSTableViewDelegate {
let account = sortedAccounts[row] let account = sortedAccounts[row]
cell.textField?.stringValue = account.nameForDisplay cell.textField?.stringValue = account.nameForDisplay
cell.imageView?.image = account.smallIcon?.image cell.imageView?.image = account.smallIcon?.image
if account.type == .feedbin { if account.type == .feedbin {
cell.isImageTemplateCapable = false cell.isImageTemplateCapable = false
} }
return cell return cell
} }
return nil return nil
} }
func tableViewSelectionDidChange(_ notification: Notification) { func tableViewSelectionDidChange(_ notification: Notification) {
let selectedRow = tableView.selectedRow let selectedRow = tableView.selectedRow
if tableView.selectedRow == -1 { if tableView.selectedRow == -1 {
deleteButton.isEnabled = false deleteButton.isEnabled = false
@ -143,12 +142,12 @@ extension AccountsPreferencesViewController: NSTableViewDelegate {
if AccountManager.shared.defaultAccount == account { if AccountManager.shared.defaultAccount == account {
deleteButton.isEnabled = false deleteButton.isEnabled = false
} }
let controller = AccountsDetailViewController(account: account) let controller = AccountsDetailViewController(account: account)
showController(controller) showController(controller)
} }
} }
extension AccountsPreferencesViewController: AccountsPreferencesAddAccountDelegate { extension AccountsPreferencesViewController: AccountsPreferencesAddAccountDelegate {
@ -187,26 +186,26 @@ extension AccountsPreferencesViewController: AccountsPreferencesAddAccountDelega
addAccountWindowController = accountsNewsBlurWindowController addAccountWindowController = accountsNewsBlurWindowController
} }
} }
private func runAwaitingFeedlyLoginAlertModal(forLifetimeOf operation: OAuthAccountAuthorizationOperation) { private func runAwaitingFeedlyLoginAlertModal(forLifetimeOf operation: OAuthAccountAuthorizationOperation) {
let alert = NSAlert() let alert = NSAlert()
alert.alertStyle = .informational alert.alertStyle = .informational
alert.messageText = NSLocalizedString("Waiting for access to Feedly", alert.messageText = NSLocalizedString("Waiting for access to Feedly",
comment: "Alert title when adding a Feedly account and waiting for authorization from the user.") comment: "Alert title when adding a Feedly account and waiting for authorization from the user.")
alert.informativeText = NSLocalizedString("A web browser will open the Feedly login for you to authorize access.", alert.informativeText = NSLocalizedString("A web browser will open the Feedly login for you to authorize access.",
comment: "Alert informative text when adding a Feedly account and waiting for authorization from the user.") comment: "Alert informative text when adding a Feedly account and waiting for authorization from the user.")
alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel")) alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel"))
let attachedWindow = self.view.window! let attachedWindow = self.view.window!
alert.beginSheetModal(for: attachedWindow) { response in alert.beginSheetModal(for: attachedWindow) { response in
if response == .alertFirstButtonReturn { if response == .alertFirstButtonReturn {
operation.cancel() operation.cancel()
} }
} }
operation.completionBlock = { _ in operation.completionBlock = { _ in
guard alert.window.isVisible else { guard alert.window.isVisible else {
return return
@ -223,22 +222,22 @@ private extension AccountsPreferencesViewController {
func updateSortedAccounts() { func updateSortedAccounts() {
sortedAccounts = AccountManager.shared.sortedAccounts sortedAccounts = AccountManager.shared.sortedAccounts
} }
func showController(_ controller: NSViewController) { func showController(_ controller: NSViewController) {
hideController() hideController()
addChild(controller) addChild(controller)
controller.view.translatesAutoresizingMaskIntoConstraints = false controller.view.translatesAutoresizingMaskIntoConstraints = false
detailView.addSubview(controller.view) detailView.addSubview(controller.view)
detailView.addFullSizeConstraints(forSubview: controller.view) detailView.addFullSizeConstraints(forSubview: controller.view)
} }
func hideController() { func hideController() {
if let controller = children.first { if let controller = children.first {
children.removeAll() children.removeAll()
controller.view.removeFromSuperview() controller.view.removeFromSuperview()
} }
if tableView.selectedRow == -1 { if tableView.selectedRow == -1 {
var helpText = "" var helpText = ""
if sortedAccounts.count == 0 { if sortedAccounts.count == 0 {
@ -246,7 +245,7 @@ private extension AccountsPreferencesViewController {
} else { } else {
helpText = NSLocalizedString("Select an account or add a new account by clicking the + button.", comment: "Add Account Explainer") helpText = NSLocalizedString("Select an account or add a new account by clicking the + button.", comment: "Add Account Explainer")
} }
let textHostingController = NSHostingController(rootView: let textHostingController = NSHostingController(rootView:
AddAccountHelpView(delegate: addAccountDelegate, helpText: helpText)) AddAccountHelpView(delegate: addAccountDelegate, helpText: helpText))
addChild(textHostingController) addChild(textHostingController)
@ -257,21 +256,21 @@ private extension AccountsPreferencesViewController {
NSLayoutConstraint(item: textHostingController.view, attribute: .bottom, relatedBy: .equal, toItem: detailView, attribute: .bottom, multiplier: 1, constant: -deleteButton.frame.height), NSLayoutConstraint(item: textHostingController.view, attribute: .bottom, relatedBy: .equal, toItem: detailView, attribute: .bottom, multiplier: 1, constant: -deleteButton.frame.height),
NSLayoutConstraint(item: textHostingController.view, attribute: .width, relatedBy: .equal, toItem: detailView, attribute: .width, multiplier: 1, constant: 1) NSLayoutConstraint(item: textHostingController.view, attribute: .width, relatedBy: .equal, toItem: detailView, attribute: .width, multiplier: 1, constant: 1)
]) ])
} }
} }
} }
extension AccountsPreferencesViewController: OAuthAccountAuthorizationOperationDelegate { extension AccountsPreferencesViewController: OAuthAccountAuthorizationOperationDelegate {
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) { func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) {
// `OAuthAccountAuthorizationOperation` is using `ASWebAuthenticationSession` which bounces the user // `OAuthAccountAuthorizationOperation` is using `ASWebAuthenticationSession` which bounces the user
// to their browser on macOS for authorizing NetNewsWire to access the user's Feedly account. // to their browser on macOS for authorizing NetNewsWire to access the user's Feedly account.
// When this authorization is granted, the browser remains the foreground app which is unfortunate // When this authorization is granted, the browser remains the foreground app which is unfortunate
// because the user probably wants to see the result of authorizing NetNewsWire to act on their behalf. // because the user probably wants to see the result of authorizing NetNewsWire to act on their behalf.
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
account.refreshAll { [weak self] result in account.refreshAll { [weak self] result in
switch result { switch result {
case .success: case .success:
@ -281,12 +280,12 @@ extension AccountsPreferencesViewController: OAuthAccountAuthorizationOperationD
} }
} }
} }
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) { func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) {
// `OAuthAccountAuthorizationOperation` is using `ASWebAuthenticationSession` which bounces the user // `OAuthAccountAuthorizationOperation` is using `ASWebAuthenticationSession` which bounces the user
// to their browser on macOS for authorizing NetNewsWire to access the user's Feedly account. // to their browser on macOS for authorizing NetNewsWire to access the user's Feedly account.
NSApp.activate(ignoringOtherApps: true) NSApp.activate(ignoringOtherApps: true)
view.window?.presentError(error) view.window?.presentError(error)
} }
} }

View File

@ -15,7 +15,7 @@ class AccountsReaderAPIWindowController: NSWindowController {
@IBOutlet weak var titleImageView: NSImageView! @IBOutlet weak var titleImageView: NSImageView!
@IBOutlet weak var titleLabel: NSTextField! @IBOutlet weak var titleLabel: NSTextField!
@IBOutlet weak var gridView: NSGridView! @IBOutlet weak var gridView: NSGridView!
@IBOutlet weak var progressIndicator: NSProgressIndicator! @IBOutlet weak var progressIndicator: NSProgressIndicator!
@IBOutlet weak var usernameTextField: NSTextField! @IBOutlet weak var usernameTextField: NSTextField!
@ -25,16 +25,16 @@ class AccountsReaderAPIWindowController: NSWindowController {
@IBOutlet weak var errorMessageLabel: NSTextField! @IBOutlet weak var errorMessageLabel: NSTextField!
@IBOutlet weak var actionButton: NSButton! @IBOutlet weak var actionButton: NSButton!
@IBOutlet weak var noAccountTextField: NSTextField! @IBOutlet weak var noAccountTextField: NSTextField!
var account: Account? var account: Account?
var accountType: AccountType? var accountType: AccountType?
private weak var hostWindow: NSWindow? private weak var hostWindow: NSWindow?
convenience init() { convenience init() {
self.init(windowNibName: NSNib.Name("AccountsReaderAPI")) self.init(windowNibName: NSNib.Name("AccountsReaderAPI"))
} }
override func windowDidLoad() { override func windowDidLoad() {
if let accountType = accountType { if let accountType = accountType {
switch accountType { switch accountType {
@ -63,7 +63,7 @@ class AccountsReaderAPIWindowController: NSWindowController {
break break
} }
} }
if let account = account, let credentials = try? account.retrieveCredentials(type: .readerBasic) { if let account = account, let credentials = try? account.retrieveCredentials(type: .readerBasic) {
usernameTextField.stringValue = credentials.username usernameTextField.stringValue = credentials.username
apiURLTextField.stringValue = account.endpointURL?.absoluteString ?? "" apiURLTextField.stringValue = account.endpointURL?.absoluteString ?? ""
@ -71,42 +71,42 @@ class AccountsReaderAPIWindowController: NSWindowController {
} else { } else {
actionButton.title = NSLocalizedString("Create", comment: "Create") actionButton.title = NSLocalizedString("Create", comment: "Create")
} }
enableAutofill() enableAutofill()
usernameTextField.becomeFirstResponder() usernameTextField.becomeFirstResponder()
} }
// MARK: API // MARK: API
func runSheetOnWindow(_ hostWindow: NSWindow, completion: ((NSApplication.ModalResponse) -> Void)? = nil) { func runSheetOnWindow(_ hostWindow: NSWindow, completion: ((NSApplication.ModalResponse) -> Void)? = nil) {
self.hostWindow = hostWindow self.hostWindow = hostWindow
hostWindow.beginSheet(window!, completionHandler: completion) hostWindow.beginSheet(window!, completionHandler: completion)
} }
// MARK: Actions // MARK: Actions
@IBAction func cancel(_ sender: Any) { @IBAction func cancel(_ sender: Any) {
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel) hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
} }
@IBAction func action(_ sender: Any) { @IBAction func action(_ sender: Any) {
self.errorMessageLabel.stringValue = "" self.errorMessageLabel.stringValue = ""
guard !usernameTextField.stringValue.isEmpty && !passwordTextField.stringValue.isEmpty else { guard !usernameTextField.stringValue.isEmpty && !passwordTextField.stringValue.isEmpty else {
self.errorMessageLabel.stringValue = NSLocalizedString("Username, password & API URL are required.", comment: "Credentials Error") self.errorMessageLabel.stringValue = NSLocalizedString("Username, password & API URL are required.", comment: "Credentials Error")
return return
} }
guard let accountType = accountType, !(accountType == .freshRSS && apiURLTextField.stringValue.isEmpty) else { guard let accountType = accountType, !(accountType == .freshRSS && apiURLTextField.stringValue.isEmpty) else {
self.errorMessageLabel.stringValue = NSLocalizedString("Username, password & API URL are required.", comment: "Credentials Error") self.errorMessageLabel.stringValue = NSLocalizedString("Username, password & API URL are required.", comment: "Credentials Error")
return return
} }
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: accountType, username: usernameTextField.stringValue) else { guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: accountType, username: usernameTextField.stringValue) else {
self.errorMessageLabel.stringValue = NSLocalizedString("There is already an account of this type with that username created.", comment: "Duplicate Error") self.errorMessageLabel.stringValue = NSLocalizedString("There is already an account of this type with that username created.", comment: "Duplicate Error")
return return
} }
let apiURL: URL let apiURL: URL
switch accountType { switch accountType {
case .freshRSS: case .freshRSS:
@ -125,31 +125,31 @@ class AccountsReaderAPIWindowController: NSWindowController {
self.errorMessageLabel.stringValue = NSLocalizedString("Unrecognized account type.", comment: "Bad account type") self.errorMessageLabel.stringValue = NSLocalizedString("Unrecognized account type.", comment: "Bad account type")
return return
} }
actionButton.isEnabled = false actionButton.isEnabled = false
progressIndicator.isHidden = false progressIndicator.isHidden = false
progressIndicator.startAnimation(self) progressIndicator.startAnimation(self)
let credentials = Credentials(type: .readerBasic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue) let credentials = Credentials(type: .readerBasic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue)
Account.validateCredentials(type: accountType, credentials: credentials, endpoint: apiURL) { [weak self] result in Account.validateCredentials(type: accountType, credentials: credentials, endpoint: apiURL) { [weak self] result in
guard let self = self else { return } guard let self = self else { return }
self.actionButton.isEnabled = true self.actionButton.isEnabled = true
self.progressIndicator.isHidden = true self.progressIndicator.isHidden = true
self.progressIndicator.stopAnimation(self) self.progressIndicator.stopAnimation(self)
switch result { switch result {
case .success(let validatedCredentials): case .success(let validatedCredentials):
guard let validatedCredentials = validatedCredentials else { guard let validatedCredentials = validatedCredentials else {
self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error") self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error")
return return
} }
if self.account == nil { if self.account == nil {
self.account = AccountManager.shared.createAccount(type: self.accountType!) self.account = AccountManager.shared.createAccount(type: self.accountType!)
} }
do { do {
self.account?.endpointURL = apiURL self.account?.endpointURL = apiURL
@ -157,8 +157,8 @@ class AccountsReaderAPIWindowController: NSWindowController {
try self.account?.removeCredentials(type: .readerAPIKey) try self.account?.removeCredentials(type: .readerAPIKey)
try self.account?.storeCredentials(credentials) try self.account?.storeCredentials(credentials)
try self.account?.storeCredentials(validatedCredentials) try self.account?.storeCredentials(validatedCredentials)
self.account?.refreshAll() { result in self.account?.refreshAll { result in
switch result { switch result {
case .success: case .success:
break break
@ -166,20 +166,20 @@ class AccountsReaderAPIWindowController: NSWindowController {
NSApplication.shared.presentError(error) NSApplication.shared.presentError(error)
} }
} }
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK) self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
} catch { } catch {
self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")
} }
case .failure: case .failure:
self.errorMessageLabel.stringValue = NSLocalizedString("Network error. Try again later.", comment: "Credentials Error") self.errorMessageLabel.stringValue = NSLocalizedString("Network error. Try again later.", comment: "Credentials Error")
} }
} }
} }
@IBAction func createAccountWithProvider(_ sender: Any) { @IBAction func createAccountWithProvider(_ sender: Any) {
switch accountType { switch accountType {
case .freshRSS: case .freshRSS:
@ -194,10 +194,10 @@ class AccountsReaderAPIWindowController: NSWindowController {
return return
} }
} }
// MARK: Autofill // MARK: Autofill
func enableAutofill() { func enableAutofill() {
usernameTextField.contentType = .username usernameTextField.contentType = .username
passwordTextField.contentType = .password passwordTextField.contentType = .password
} }
} }

View File

@ -10,12 +10,12 @@ import SwiftUI
import Account import Account
struct AddAccountHelpView: View { struct AddAccountHelpView: View {
let accountTypes: [AccountType] = AddAccountSections.allOrdered.sectionContent let accountTypes: [AccountType] = AddAccountSections.allOrdered.sectionContent
var delegate: AccountsPreferencesAddAccountDelegate? var delegate: AccountsPreferencesAddAccountDelegate?
var helpText: String var helpText: String
@State private var iCloudUnavailableError: Bool = false @State private var iCloudUnavailableError: Bool = false
var body: some View { var body: some View {
VStack { VStack {
HStack { HStack {
@ -36,11 +36,11 @@ struct AddAccountHelpView: View {
} }
} }
} }
Text(helpText) Text(helpText)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
.padding(.top, 8) .padding(.top, 8)
} }
.alert(isPresented: $iCloudUnavailableError, content: { .alert(isPresented: $iCloudUnavailableError, content: {
Alert(title: Text(NSLocalizedString("Error", comment: "Error")), Alert(title: Text(NSLocalizedString("Error", comment: "Error")),
@ -50,5 +50,5 @@ struct AddAccountHelpView: View {
})) }))
}) })
} }
} }

View File

@ -16,7 +16,7 @@ enum AddAccountSections: Int, CaseIterable {
case web case web
case selfhosted case selfhosted
case allOrdered case allOrdered
var sectionHeader: String { var sectionHeader: String {
switch self { switch self {
case .local: case .local:
@ -31,7 +31,7 @@ enum AddAccountSections: Int, CaseIterable {
return "" return ""
} }
} }
var sectionFooter: String { var sectionFooter: String {
switch self { switch self {
case .local: case .local:
@ -46,7 +46,7 @@ enum AddAccountSections: Int, CaseIterable {
return "" return ""
} }
} }
var sectionContent: [AccountType] { var sectionContent: [AccountType] {
switch self { switch self {
case .local: case .local:
@ -68,38 +68,35 @@ enum AddAccountSections: Int, CaseIterable {
AddAccountSections.selfhosted.sectionContent AddAccountSections.selfhosted.sectionContent
} }
} }
} }
struct AddAccountsView: View { struct AddAccountsView: View {
weak var parent: NSHostingController<AddAccountsView>? // required because presentationMode.dismiss() doesn't work weak var parent: NSHostingController<AddAccountsView>? // required because presentationMode.dismiss() doesn't work
var addAccountDelegate: AccountsPreferencesAddAccountDelegate? var addAccountDelegate: AccountsPreferencesAddAccountDelegate?
private let chunkLimit = 4 // use this to control number of accounts in each web account column private let chunkLimit = 4 // use this to control number of accounts in each web account column
@State private var selectedAccount: AccountType = .onMyMac @State private var selectedAccount: AccountType = .onMyMac
init(delegate: AccountsPreferencesAddAccountDelegate?) { init(delegate: AccountsPreferencesAddAccountDelegate?) {
self.addAccountDelegate = delegate self.addAccountDelegate = delegate
} }
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 8) { VStack(alignment: .leading, spacing: 8) {
Text("Choose an account type to add...") Text("Choose an account type to add...")
.font(.headline) .font(.headline)
.padding() .padding()
localAccount localAccount
if !AppDefaults.shared.isDeveloperBuild { if !AppDefaults.shared.isDeveloperBuild {
icloudAccount icloudAccount
} }
webAccounts webAccounts
selfhostedAccounts selfhostedAccounts
HStack(spacing: 12) { HStack(spacing: 12) {
Spacer() Spacer()
Button(action: { Button(action: {
@ -135,7 +132,7 @@ struct AddAccountsView: View {
Text("Local") Text("Local")
.font(.headline) .font(.headline)
.padding(.horizontal) .padding(.horizontal)
Picker(selection: $selectedAccount, label: Text(""), content: { Picker(selection: $selectedAccount, label: Text(""), content: {
ForEach(AddAccountSections.local.sectionContent, id: \.self, content: { account in ForEach(AddAccountSections.local.sectionContent, id: \.self, content: { account in
HStack(alignment: .center) { HStack(alignment: .center) {
@ -151,23 +148,23 @@ struct AddAccountsView: View {
}) })
.pickerStyle(RadioGroupPickerStyle()) .pickerStyle(RadioGroupPickerStyle())
.offset(x: 7.5, y: 0) .offset(x: 7.5, y: 0)
Text(AddAccountSections.local.sectionFooter).foregroundColor(.gray) Text(AddAccountSections.local.sectionFooter).foregroundColor(.gray)
.padding(.horizontal) .padding(.horizontal)
.lineLimit(3) .lineLimit(3)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} }
} }
var icloudAccount: some View { var icloudAccount: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("iCloud") Text("iCloud")
.font(.headline) .font(.headline)
.padding(.horizontal) .padding(.horizontal)
.padding(.top, 8) .padding(.top, 8)
Picker(selection: $selectedAccount, label: Text(""), content: { Picker(selection: $selectedAccount, label: Text(""), content: {
ForEach(AddAccountSections.icloud.sectionContent, id: \.self, content: { account in ForEach(AddAccountSections.icloud.sectionContent, id: \.self, content: { account in
HStack(alignment: .center) { HStack(alignment: .center) {
@ -176,7 +173,7 @@ struct AddAccountsView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20, alignment: .center) .frame(width: 20, height: 20, alignment: .center)
.padding(.leading, 4) .padding(.leading, 4)
Text(account.localizedAccountName()) Text(account.localizedAccountName())
} }
.tag(account) .tag(account)
@ -184,14 +181,14 @@ struct AddAccountsView: View {
}) })
.offset(x: 7.5, y: 0) .offset(x: 7.5, y: 0)
.disabled(isCloudInUse()) .disabled(isCloudInUse())
Text(AddAccountSections.icloud.sectionFooter).foregroundColor(.gray) Text(AddAccountSections.icloud.sectionFooter).foregroundColor(.gray)
.padding(.horizontal) .padding(.horizontal)
.lineLimit(3) .lineLimit(3)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} }
} }
@ViewBuilder @ViewBuilder
var webAccounts: some View { var webAccounts: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
@ -199,13 +196,13 @@ struct AddAccountsView: View {
.font(.headline) .font(.headline)
.padding(.horizontal) .padding(.horizontal)
.padding(.top, 8) .padding(.top, 8)
HStack { HStack {
ForEach(0..<chunkedWebAccounts().count, id: \.self, content: { chunk in ForEach(0..<chunkedWebAccounts().count, id: \.self, content: { chunk in
VStack { VStack {
Picker(selection: $selectedAccount, label: Text(""), content: { Picker(selection: $selectedAccount, label: Text(""), content: {
ForEach(chunkedWebAccounts()[chunk], id: \.self, content: { account in ForEach(chunkedWebAccounts()[chunk], id: \.self, content: { account in
HStack(alignment: .center) { HStack(alignment: .center) {
account.image() account.image()
.resizable() .resizable()
@ -215,7 +212,7 @@ struct AddAccountsView: View {
Text(account.localizedAccountName()) Text(account.localizedAccountName())
} }
.tag(account) .tag(account)
}) })
}) })
Spacer() Spacer()
@ -223,21 +220,21 @@ struct AddAccountsView: View {
}) })
} }
.offset(x: 7.5, y: 0) .offset(x: 7.5, y: 0)
Text(AddAccountSections.web.sectionFooter).foregroundColor(.gray) Text(AddAccountSections.web.sectionFooter).foregroundColor(.gray)
.padding(.horizontal) .padding(.horizontal)
.lineLimit(3) .lineLimit(3)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} }
} }
var selfhostedAccounts: some View { var selfhostedAccounts: some View {
VStack(alignment: .leading) { VStack(alignment: .leading) {
Text("Self-hosted") Text("Self-hosted")
.font(.headline) .font(.headline)
.padding(.horizontal) .padding(.horizontal)
.padding(.top, 8) .padding(.top, 8)
Picker(selection: $selectedAccount, label: Text(""), content: { Picker(selection: $selectedAccount, label: Text(""), content: {
ForEach(AddAccountSections.selfhosted.sectionContent, id: \.self, content: { account in ForEach(AddAccountSections.selfhosted.sectionContent, id: \.self, content: { account in
HStack(alignment: .center) { HStack(alignment: .center) {
@ -246,34 +243,32 @@ struct AddAccountsView: View {
.aspectRatio(contentMode: .fit) .aspectRatio(contentMode: .fit)
.frame(width: 20, height: 20, alignment: .center) .frame(width: 20, height: 20, alignment: .center)
.padding(.leading, 4) .padding(.leading, 4)
Text(account.localizedAccountName()) Text(account.localizedAccountName())
}.tag(account) }.tag(account)
}) })
}) })
.offset(x: 7.5, y: 0) .offset(x: 7.5, y: 0)
Text(AddAccountSections.selfhosted.sectionFooter).foregroundColor(.gray) Text(AddAccountSections.selfhosted.sectionFooter).foregroundColor(.gray)
.padding(.horizontal) .padding(.horizontal)
.lineLimit(3) .lineLimit(3)
.fixedSize(horizontal: false, vertical: true) .fixedSize(horizontal: false, vertical: true)
} }
} }
private func isCloudInUse() -> Bool { private func isCloudInUse() -> Bool {
AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit }) AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit })
} }
private func chunkedWebAccounts() -> [[AccountType]] { private func chunkedWebAccounts() -> [[AccountType]] {
AddAccountSections.web.sectionContent.chunked(into: chunkLimit) AddAccountSections.web.sectionContent.chunked(into: chunkLimit)
} }
} }
struct AddAccountsView_Previews: PreviewProvider { struct AddAccountsView_Previews: PreviewProvider {
static var previews: some View { static var previews: some View {
AddAccountsView(delegate: nil) AddAccountsView(delegate: nil)
} }
} }

View File

@ -53,12 +53,11 @@ private extension AdvancedPreferencesViewController {
func updateUI() { func updateUI() {
if wantsTestBuilds { if wantsTestBuilds {
testBuildsButton.state = .on testBuildsButton.state = .on
} } else {
else {
releaseBuildsButton.state = .on releaseBuildsButton.state = .on
} }
} }
func currentAppcastURL() -> String { func currentAppcastURL() -> String {
return UserDefaults.standard.string(forKey: appcastDefaultsKey) ?? "" return UserDefaults.standard.string(forKey: appcastDefaultsKey) ?? ""
} }

View File

@ -51,7 +51,7 @@ final class GeneralPreferencesViewController: NSViewController {
let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath) let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath)
NSWorkspace.shared.open(url) NSWorkspace.shared.open(url)
} }
@IBAction func articleThemePopUpDidChange(_ sender: Any) { @IBAction func articleThemePopUpDidChange(_ sender: Any) {
guard let menuItem = articleThemePopup.selectedItem else { guard let menuItem = articleThemePopup.selectedItem else {
return return
@ -59,7 +59,7 @@ final class GeneralPreferencesViewController: NSViewController {
ArticleThemesManager.shared.currentThemeName = menuItem.title ArticleThemesManager.shared.currentThemeName = menuItem.title
updateArticleThemePopup() updateArticleThemePopup()
} }
@IBAction func browserPopUpDidChangeValue(_ sender: Any?) { @IBAction func browserPopUpDidChangeValue(_ sender: Any?) {
guard let menuItem = defaultBrowserPopup.selectedItem else { guard let menuItem = defaultBrowserPopup.selectedItem else {
return return
@ -84,18 +84,18 @@ private extension GeneralPreferencesViewController {
updateArticleThemePopup() updateArticleThemePopup()
updateBrowserPopup() updateBrowserPopup()
} }
func updateArticleThemePopup() { func updateArticleThemePopup() {
let menu = articleThemePopup.menu! let menu = articleThemePopup.menu!
menu.removeAllItems() menu.removeAllItems()
menu.addItem(NSMenuItem(title: ArticleTheme.defaultTheme.name, action: nil, keyEquivalent: "")) menu.addItem(NSMenuItem(title: ArticleTheme.defaultTheme.name, action: nil, keyEquivalent: ""))
menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem.separator())
for themeName in ArticleThemesManager.shared.themeNames { for themeName in ArticleThemesManager.shared.themeNames {
menu.addItem(NSMenuItem(title: themeName, action: nil, keyEquivalent: "")) menu.addItem(NSMenuItem(title: themeName, action: nil, keyEquivalent: ""))
} }
articleThemePopup.selectItem(withTitle: ArticleThemesManager.shared.currentThemeName) articleThemePopup.selectItem(withTitle: ArticleThemesManager.shared.currentThemeName)
if articleThemePopup.indexOfSelectedItem == -1 { if articleThemePopup.indexOfSelectedItem == -1 {
articleThemePopup.selectItem(withTitle: ArticleTheme.defaultTheme.name) articleThemePopup.selectItem(withTitle: ArticleTheme.defaultTheme.name)

View File

@ -29,7 +29,7 @@ final class PreferencesControlsBackgroundView: NSView {
let fillColor = self.effectiveAppearance.isDarkMode ? darkModeFillColor : lightModeFillColor let fillColor = self.effectiveAppearance.isDarkMode ? darkModeFillColor : lightModeFillColor
fillColor.setFill() fillColor.setFill()
let r = NSIntersectionRect(dirtyRect, bounds) let r = dirtyRect.intersection(bounds)
r.fill() r.fill()
let borderColor = self.effectiveAppearance.isDarkMode ? darkModeBorderColor : lightModeBorderColor let borderColor = self.effectiveAppearance.isDarkMode ? darkModeBorderColor : lightModeBorderColor

View File

@ -17,7 +17,7 @@ final class PreferencesTableViewBackgroundView: NSView {
let color = self.effectiveAppearance.isDarkMode ? darkBorderColor : lightBorderColor let color = self.effectiveAppearance.isDarkMode ? darkBorderColor : lightBorderColor
color.setFill() color.setFill()
let r = NSIntersectionRect(dirtyRect, bounds) let r = dirtyRect.intersection(bounds)
r.fill() r.fill()
} }
} }

View File

@ -13,7 +13,7 @@ private struct PreferencesToolbarItemSpec {
let identifier: NSToolbarItem.Identifier let identifier: NSToolbarItem.Identifier
let name: String let name: String
let image: NSImage? let image: NSImage?
init(identifierRawValue: String, name: String, image: NSImage?) { init(identifierRawValue: String, name: String, image: NSImage?) {
self.identifier = NSToolbarItem.Identifier(identifierRawValue) self.identifier = NSToolbarItem.Identifier(identifierRawValue)
self.name = name self.name = name
@ -27,8 +27,8 @@ private struct ToolbarItemIdentifier {
static let Advanced = "Advanced" static let Advanced = "Advanced"
} }
class PreferencesWindowController : NSWindowController, NSToolbarDelegate { class PreferencesWindowController: NSWindowController, NSToolbarDelegate {
private let windowWidth = CGFloat(512.0) // Width is constant for all views; only the height changes private let windowWidth = CGFloat(512.0) // Width is constant for all views; only the height changes
private var viewControllers = [String: NSViewController]() private var viewControllers = [String: NSViewController]()
private let toolbarItemSpecs: [PreferencesToolbarItemSpec] = { private let toolbarItemSpecs: [PreferencesToolbarItemSpec] = {
@ -126,7 +126,7 @@ private extension PreferencesWindowController {
assertionFailure("Preferences window: no view controller matching \(identifier).") assertionFailure("Preferences window: no view controller matching \(identifier).")
return return
} }
if newViewController.view == currentView { if newViewController.view == currentView {
return return
} }
@ -140,8 +140,7 @@ private extension PreferencesWindowController {
if let currentView = currentView { if let currentView = currentView {
window!.contentView?.replaceSubview(currentView, with: newViewController.view) window!.contentView?.replaceSubview(currentView, with: newViewController.view)
} } else {
else {
window!.contentView?.addSubview(newViewController.view) window!.contentView?.addSubview(newViewController.view)
} }
@ -167,23 +166,23 @@ private extension PreferencesWindowController {
let viewFrame = view.frame let viewFrame = view.frame
let windowFrame = window!.frame let windowFrame = window!.frame
let contentViewFrame = window!.contentView!.frame let contentViewFrame = window!.contentView!.frame
let deltaHeight = NSHeight(contentViewFrame) - NSHeight(viewFrame) let deltaHeight = contentViewFrame.height - viewFrame.height
let heightForWindow = NSHeight(windowFrame) - deltaHeight let heightForWindow = windowFrame.height - deltaHeight
let windowOriginY = NSMinY(windowFrame) + deltaHeight let windowOriginY = windowFrame.minY + deltaHeight
var updatedWindowFrame = windowFrame var updatedWindowFrame = windowFrame
updatedWindowFrame.size.height = heightForWindow updatedWindowFrame.size.height = heightForWindow
updatedWindowFrame.origin.y = windowOriginY updatedWindowFrame.origin.y = windowOriginY
updatedWindowFrame.size.width = windowWidth //NSWidth(viewFrame) updatedWindowFrame.size.width = windowWidth // NSWidth(viewFrame)
var updatedViewFrame = viewFrame var updatedViewFrame = viewFrame
updatedViewFrame.origin = NSZeroPoint updatedViewFrame.origin = NSPoint.zero
updatedViewFrame.size.width = windowWidth updatedViewFrame.size.width = windowWidth
if viewFrame != updatedViewFrame { if viewFrame != updatedViewFrame {
view.frame = updatedViewFrame view.frame = updatedViewFrame
} }
if windowFrame != updatedWindowFrame { if windowFrame != updatedWindowFrame {
window!.contentView?.alphaValue = 0.0 window!.contentView?.alphaValue = 0.0
window!.setFrame(updatedWindowFrame, display: true, animate: true) window!.setFrame(updatedWindowFrame, display: true, animate: true)

View File

@ -29,7 +29,7 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
} }
// Maps from UUID to a validation wrapper // Maps from UUID to a validation wrapper
static var gPingPongMap = Dictionary<String, ValidationWrapper>() static var gPingPongMap = [String: ValidationWrapper]()
static var validationQueue = DispatchQueue(label: "Toolbar Validation") static var validationQueue = DispatchQueue(label: "Toolbar Validation")
// Bottleneck for calling through to a validation handler we have saved, and removing it from the list. // Bottleneck for calling through to a validation handler we have saved, and removing it from the list.
@ -40,8 +40,8 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
} }
} }
override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String : Any]?) { override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String: Any]?) {
if (messageName == "subscribeToFeed") { if messageName == "subscribeToFeed" {
if var feedURLString = userInfo?["url"] as? String { if var feedURLString = userInfo?["url"] as? String {
var openInDefaultBrowser = false var openInDefaultBrowser = false
@ -61,16 +61,15 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
NSWorkspace.shared.open(feedURL) NSWorkspace.shared.open(feedURL)
} }
} }
} } else if messageName == "pong" {
else if (messageName == "pong") {
if let validationIDString = userInfo?["validationID"] as? String { if let validationIDString = userInfo?["validationID"] as? String {
// Should we validate the button? // Should we validate the button?
let shouldValidate = userInfo?["shouldValidate"] as? Bool ?? false let shouldValidate = userInfo?["shouldValidate"] as? Bool ?? false
SafariExtensionHandler.callValidationHandler(forHandlerID: validationIDString, withShouldValidate:shouldValidate) SafariExtensionHandler.callValidationHandler(forHandlerID: validationIDString, withShouldValidate: shouldValidate)
} }
} }
} }
override func toolbarItemClicked(in window: SFSafariWindow) { override func toolbarItemClicked(in window: SFSafariWindow) {
window.getActiveTab { (activeTab) in window.getActiveTab { (activeTab) in
activeTab?.getActivePage(completionHandler: { (activePage) in activeTab?.getActivePage(completionHandler: { (activePage) in
@ -97,7 +96,7 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
if thisValidationID != uniqueValidationID { if thisValidationID != uniqueValidationID {
// Default to valid ... we'll know soon enough whether the latest state // Default to valid ... we'll know soon enough whether the latest state
// is actually still valid or not... // is actually still valid or not...
SafariExtensionHandler.callValidationHandler(forHandlerID: thisValidationID, withShouldValidate: true); SafariExtensionHandler.callValidationHandler(forHandlerID: thisValidationID, withShouldValidate: true)
} }
} }
@ -108,14 +107,14 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
// a timeout period has elapsed // a timeout period has elapsed
window.getActiveTab { (activeTab) in window.getActiveTab { (activeTab) in
guard let activeTab = activeTab else { guard let activeTab = activeTab else {
SafariExtensionHandler.callValidationHandler(forHandlerID: uniqueValidationID, withShouldValidate:false); SafariExtensionHandler.callValidationHandler(forHandlerID: uniqueValidationID, withShouldValidate: false)
return return
} }
activeTab.getActivePage { (activePage) in activeTab.getActivePage { (activePage) in
guard let activePage = activePage else { guard let activePage = activePage else {
SafariExtensionHandler.callValidationHandler(forHandlerID: uniqueValidationID, withShouldValidate:false); SafariExtensionHandler.callValidationHandler(forHandlerID: uniqueValidationID, withShouldValidate: false)
return return
} }
activePage.getPropertiesWithCompletionHandler { (pageProperties) in activePage.getPropertiesWithCompletionHandler { (pageProperties) in
@ -127,7 +126,7 @@ class SafariExtensionHandler: SFSafariExtensionHandler {
let pongTimeoutInNanoseconds = Int(Double(NSEC_PER_SEC) * 0.5) let pongTimeoutInNanoseconds = Int(Double(NSEC_PER_SEC) * 0.5)
let timeoutDeadline = DispatchTime.now() + DispatchTimeInterval.nanoseconds(pongTimeoutInNanoseconds) let timeoutDeadline = DispatchTime.now() + DispatchTimeInterval.nanoseconds(pongTimeoutInNanoseconds)
DispatchQueue.main.asyncAfter(deadline: timeoutDeadline, execute: { [timedOutValidationID = uniqueValidationID] in DispatchQueue.main.asyncAfter(deadline: timeoutDeadline, execute: { [timedOutValidationID = uniqueValidationID] in
SafariExtensionHandler.callValidationHandler(forHandlerID: timedOutValidationID, withShouldValidate:false) SafariExtensionHandler.callValidationHandler(forHandlerID: timedOutValidationID, withShouldValidate: false)
}) })
} }
} }

View File

@ -13,7 +13,7 @@ class SafariExtensionViewController: SFSafariExtensionViewController {
// This would be the place to handle a popover that could, for example, list the possibly multiple feeds offered by a site. // This would be the place to handle a popover that could, for example, list the possibly multiple feeds offered by a site.
static let shared: SafariExtensionViewController = { static let shared: SafariExtensionViewController = {
let shared = SafariExtensionViewController() let shared = SafariExtensionViewController()
shared.preferredContentSize = NSSize(width:320, height:240) shared.preferredContentSize = NSSize(width: 320, height: 240)
return shared return shared
}() }()

View File

@ -13,16 +13,16 @@ import RSCore
@objc(ScriptableAccount) @objc(ScriptableAccount)
class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let account:Account let account: Account
init (_ account:Account) { init (_ account: Account) {
self.account = account self.account = account
} }
@objc(objectSpecifier) @objc(objectSpecifier)
override var objectSpecifier: NSScriptObjectSpecifier? { override var objectSpecifier: NSScriptObjectSpecifier? {
let myContainer = NSApplication.shared let myContainer = NSApplication.shared
let scriptObjectSpecifier = myContainer.makeFormUniqueIDScriptObjectSpecifier(forObject:self) let scriptObjectSpecifier = myContainer.makeFormUniqueIDScriptObjectSpecifier(forObject: self)
return (scriptObjectSpecifier) return (scriptObjectSpecifier)
} }
@ -47,91 +47,91 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
} }
// MARK: --- ScriptingObject protocol --- // MARK: --- ScriptingObject protocol ---
var scriptingKey: String { var scriptingKey: String {
return "accounts" return "accounts"
} }
// MARK: --- UniqueIdScriptingObject protocol --- // MARK: --- UniqueIdScriptingObject protocol ---
// I am not sure if account should prefer to be specified by name or by ID // I am not sure if account should prefer to be specified by name or by ID
// but in either case it seems like the accountID would be used as the keydata, so I chose ID // but in either case it seems like the accountID would be used as the keydata, so I chose ID
@objc(uniqueId) @objc(uniqueId)
var scriptingUniqueId:Any { var scriptingUniqueId: Any {
return account.accountID return account.accountID
} }
// MARK: --- ScriptingObjectContainer protocol --- // MARK: --- ScriptingObjectContainer protocol ---
var scriptingClassDescription: NSScriptClassDescription { var scriptingClassDescription: NSScriptClassDescription {
return self.classDescription as! NSScriptClassDescription return self.classDescription as! NSScriptClassDescription
} }
func deleteElement(_ element:ScriptingObject) { func deleteElement(_ element: ScriptingObject) {
if let scriptableFolder = element as? ScriptableFolder { if let scriptableFolder = element as? ScriptableFolder {
BatchUpdate.shared.perform { BatchUpdate.shared.perform {
account.removeFolder(scriptableFolder.folder) { result in account.removeFolder(scriptableFolder.folder) { _ in
} }
} }
} else if let scriptableFeed = element as? ScriptableFeed { } else if let scriptableFeed = element as? ScriptableFeed {
BatchUpdate.shared.perform { BatchUpdate.shared.perform {
var container: Container? = nil var container: Container?
if let scriptableFolder = scriptableFeed.container as? ScriptableFolder { if let scriptableFolder = scriptableFeed.container as? ScriptableFolder {
container = scriptableFolder.folder container = scriptableFolder.folder
} else { } else {
container = account container = account
} }
account.removeFeed(scriptableFeed.feed, from: container!) { result in account.removeFeed(scriptableFeed.feed, from: container!) { _ in
} }
} }
} }
} }
@objc(isLocationRequiredToCreateForKey:) @objc(isLocationRequiredToCreateForKey:)
func isLocationRequiredToCreate(forKey key:String) -> Bool { func isLocationRequiredToCreate(forKey key: String) -> Bool {
return false; return false
} }
// MARK: --- Scriptable elements --- // MARK: --- Scriptable elements ---
@objc(feeds) @objc(feeds)
var feeds:NSArray { var feeds: NSArray {
return account.topLevelFeeds.map { ScriptableFeed($0, container:self) } as NSArray return account.topLevelFeeds.map { ScriptableFeed($0, container: self) } as NSArray
} }
@objc(valueInFeedsWithUniqueID:) @objc(valueInFeedsWithUniqueID:)
func valueInFeeds(withUniqueID id:String) -> ScriptableFeed? { func valueInFeeds(withUniqueID id: String) -> ScriptableFeed? {
guard let feed = account.existingFeed(withFeedID: id) else { return nil } guard let feed = account.existingFeed(withFeedID: id) else { return nil }
return ScriptableFeed(feed, container:self) return ScriptableFeed(feed, container: self)
} }
@objc(valueInFeedsWithName:) @objc(valueInFeedsWithName:)
func valueInFeeds(withName name:String) -> ScriptableFeed? { func valueInFeeds(withName name: String) -> ScriptableFeed? {
let feeds = Array(account.flattenedFeeds()) let feeds = Array(account.flattenedFeeds())
guard let feed = feeds.first(where:{$0.name == name}) else { return nil } guard let feed = feeds.first(where: {$0.name == name}) else { return nil }
return ScriptableFeed(feed, container:self) return ScriptableFeed(feed, container: self)
} }
@objc(folders) @objc(folders)
var folders:NSArray { var folders: NSArray {
let foldersSet = account.folders ?? Set<Folder>() let foldersSet = account.folders ?? Set<Folder>()
let folders = Array(foldersSet) let folders = Array(foldersSet)
return folders.map { ScriptableFolder($0, container:self) } as NSArray return folders.map { ScriptableFolder($0, container: self) } as NSArray
} }
@objc(valueInFoldersWithUniqueID:) @objc(valueInFoldersWithUniqueID:)
func valueInFolders(withUniqueID id:NSNumber) -> ScriptableFolder? { func valueInFolders(withUniqueID id: NSNumber) -> ScriptableFolder? {
let folderId = id.intValue let folderId = id.intValue
let foldersSet = account.folders ?? Set<Folder>() let foldersSet = account.folders ?? Set<Folder>()
let folders = Array(foldersSet) let folders = Array(foldersSet)
guard let folder = folders.first(where:{$0.folderID == folderId}) else { return nil } guard let folder = folders.first(where: {$0.folderID == folderId}) else { return nil }
return ScriptableFolder(folder, container:self) return ScriptableFolder(folder, container: self)
} }
// MARK: --- Scriptable properties --- // MARK: --- Scriptable properties ---
@objc(allFeeds) @objc(allFeeds)
var allFeeds: NSArray { var allFeeds: NSArray {
var feeds = [ScriptableFeed]() var feeds = [ScriptableFeed]()
for feed in account.topLevelFeeds { for feed in account.topLevelFeeds {
feeds.append(ScriptableFeed(feed, container: self)) feeds.append(ScriptableFeed(feed, container: self))
@ -148,13 +148,13 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
} }
@objc(opmlRepresentation) @objc(opmlRepresentation)
var opmlRepresentation:String { var opmlRepresentation: String {
return self.account.OPMLString(indentLevel:0) return self.account.OPMLString(indentLevel: 0)
} }
@objc(accountType) @objc(accountType)
var accountType:OSType { var accountType: OSType {
var osType:String = "" var osType: String = ""
switch self.account.type { switch self.account.type {
case .onMyMac: case .onMyMac:
osType = "Locl" osType = "Locl"

View File

@ -25,19 +25,19 @@ protocol AppDelegateAppleEvents {
} }
protocol ScriptingAppDelegate { protocol ScriptingAppDelegate {
var scriptingCurrentArticle: Article? {get} var scriptingCurrentArticle: Article? {get}
var scriptingSelectedArticles: [Article] {get} var scriptingSelectedArticles: [Article] {get}
var scriptingMainWindowController:ScriptingMainWindowController? {get} var scriptingMainWindowController: ScriptingMainWindowController? {get}
} }
extension AppDelegate : AppDelegateAppleEvents { extension AppDelegate: AppDelegateAppleEvents {
// MARK: GetURL Apple Event // MARK: GetURL Apple Event
func installAppleEventHandlers() { func installAppleEventHandlers() {
NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(AppDelegate.getURL(_:_:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL)) NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(AppDelegate.getURL(_:_:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
} }
@objc func getURL(_ event: NSAppleEventDescriptor, _ withReplyEvent: NSAppleEventDescriptor) { @objc func getURL(_ event: NSAppleEventDescriptor, _ withReplyEvent: NSAppleEventDescriptor) {
guard var urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue else { guard var urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue else {
@ -51,14 +51,14 @@ extension AppDelegate : AppDelegateAppleEvents {
let themeURLString = queryItems.first(where: { $0.name == "url" })?.value else { let themeURLString = queryItems.first(where: { $0.name == "url" })?.value else {
return return
} }
if let themeURL = URL(string: themeURLString) { if let themeURL = URL(string: themeURLString) {
let request = URLRequest(url: themeURL) let request = URLRequest(url: themeURL)
let task = URLSession.shared.downloadTask(with: request) { location, response, error in let task = URLSession.shared.downloadTask(with: request) { location, _, error in
guard let location = location else { guard let location = location else {
return return
} }
do { do {
try ArticleThemeDownloader.shared.handleFile(at: location) try ArticleThemeDownloader.shared.handleFile(at: location)
} catch { } catch {
@ -68,10 +68,9 @@ extension AppDelegate : AppDelegateAppleEvents {
task.resume() task.resume()
} }
return return
} }
// Special case URL with specific scheme handler x-netnewswire-feed: intended to ensure we open // Special case URL with specific scheme handler x-netnewswire-feed: intended to ensure we open
// it regardless of which news reader may be set as the default // it regardless of which news reader may be set as the default
let nnwScheme = "x-netnewswire-feed:" let nnwScheme = "x-netnewswire-feed:"
@ -91,13 +90,13 @@ extension AppDelegate : AppDelegateAppleEvents {
} }
} }
class NetNewsWireCreateElementCommand : NSCreateCommand { class NetNewsWireCreateElementCommand: NSCreateCommand {
override func performDefaultImplementation() -> Any? { override func performDefaultImplementation() -> Any? {
let classDescription = self.createClassDescription let classDescription = self.createClassDescription
if (classDescription.className == "feed") { if classDescription.className == "feed" {
return ScriptableFeed.handleCreateElement(command:self) return ScriptableFeed.handleCreateElement(command: self)
} else if (classDescription.className == "folder") { } else if classDescription.className == "folder" {
return ScriptableFolder.handleCreateElement(command:self) return ScriptableFolder.handleCreateElement(command: self)
} }
return nil return nil
} }
@ -111,7 +110,7 @@ class NetNewsWireCreateElementCommand : NSCreateCommand {
is ambiguity about whether specifiers are lists or single objects, the code switches is ambiguity about whether specifiers are lists or single objects, the code switches
based on which it is. based on which it is.
*/ */
class NetNewsWireDeleteCommand : NSDeleteCommand { class NetNewsWireDeleteCommand: NSDeleteCommand {
/* /*
delete(objectToDelete:, from container:) delete(objectToDelete:, from container:)
@ -119,16 +118,16 @@ class NetNewsWireDeleteCommand : NSDeleteCommand {
Here the code unravels the case of objectToDelete being a list or a single object, Here the code unravels the case of objectToDelete being a list or a single object,
ultimately calling container.deleteElement(element) for each element to delete ultimately calling container.deleteElement(element) for each element to delete
*/ */
func delete(objectToDelete:Any, from container:ScriptingObjectContainer) { func delete(objectToDelete: Any, from container: ScriptingObjectContainer) {
if let objectList = objectToDelete as? [Any] { if let objectList = objectToDelete as? [Any] {
for nthObject in objectList { for nthObject in objectList {
self.delete(objectToDelete:nthObject, from:container) self.delete(objectToDelete: nthObject, from: container)
} }
} else if let element = objectToDelete as? ScriptingObject { } else if let element = objectToDelete as? ScriptingObject {
container.deleteElement(element) container.deleteElement(element)
} }
} }
/* /*
delete(specifier:, from container:) delete(specifier:, from container:)
At this point in handling the command, the container could be a list or a single object, At this point in handling the command, the container could be a list or a single object,
@ -138,14 +137,14 @@ class NetNewsWireDeleteCommand : NSDeleteCommand {
After resolving, we call delete(objectToDelete:, from container:) with the container and After resolving, we call delete(objectToDelete:, from container:) with the container and
the resolved objects the resolved objects
*/ */
func delete(specifier:NSScriptObjectSpecifier, from container:Any) { func delete(specifier: NSScriptObjectSpecifier, from container: Any) {
if let containerList = container as? [Any] { if let containerList = container as? [Any] {
for nthObject in containerList { for nthObject in containerList {
self.delete(specifier:specifier, from:nthObject) self.delete(specifier: specifier, from: nthObject)
} }
} else if let container = container as? ScriptingObjectContainer { } else if let container = container as? ScriptingObjectContainer {
if let resolvedObjects = specifier.objectsByEvaluating(withContainers:container) { if let resolvedObjects = specifier.objectsByEvaluating(withContainers: container) {
self.delete(objectToDelete:resolvedObjects, from:container) self.delete(objectToDelete: resolvedObjects, from: container)
} }
} }
} }
@ -159,14 +158,14 @@ class NetNewsWireDeleteCommand : NSDeleteCommand {
override func performDefaultImplementation() -> Any? { override func performDefaultImplementation() -> Any? {
if let receiversSpecifier = self.receiversSpecifier { if let receiversSpecifier = self.receiversSpecifier {
if let receiverObjects = receiversSpecifier.objectsByEvaluatingSpecifier { if let receiverObjects = receiversSpecifier.objectsByEvaluatingSpecifier {
self.delete(specifier:self.keySpecifier, from:receiverObjects) self.delete(specifier: self.keySpecifier, from: receiverObjects)
} }
} }
return nil return nil
} }
} }
class NetNewsWireExistsCommand : NSExistsCommand { class NetNewsWireExistsCommand: NSExistsCommand {
// cocoa default behavior doesn't work here, because of cases where we define an object's property // cocoa default behavior doesn't work here, because of cases where we define an object's property
// to be another object type. e.g., 'permalink of the current article' parses as // to be another object type. e.g., 'permalink of the current article' parses as
@ -177,10 +176,9 @@ class NetNewsWireExistsCommand : NSExistsCommand {
// must not exist. Otherwise, we return the result of the defaultImplementation // must not exist. Otherwise, we return the result of the defaultImplementation
// The wrinkle is that it is possible that the direct object is a list, so we need to // The wrinkle is that it is possible that the direct object is a list, so we need to
// handle that case as well // handle that case as well
override func performDefaultImplementation() -> Any? { override func performDefaultImplementation() -> Any? {
guard let result = super.performDefaultImplementation() else { return NSNumber(booleanLiteral:false) } guard let result = super.performDefaultImplementation() else { return NSNumber(booleanLiteral: false) }
return result return result
} }
} }

View File

@ -13,17 +13,17 @@ import Articles
@objc(ScriptableArticle) @objc(ScriptableArticle)
class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let article:Article let article: Article
let container:ScriptingObjectContainer let container: ScriptingObjectContainer
init (_ article:Article, container:ScriptingObjectContainer) { init (_ article: Article, container: ScriptingObjectContainer) {
self.article = article self.article = article
self.container = container self.container = container
} }
@objc(objectSpecifier) @objc(objectSpecifier)
override var objectSpecifier: NSScriptObjectSpecifier? { override var objectSpecifier: NSScriptObjectSpecifier? {
let scriptObjectSpecifier = self.container.makeFormUniqueIDScriptObjectSpecifier(forObject:self) let scriptObjectSpecifier = self.container.makeFormUniqueIDScriptObjectSpecifier(forObject: self)
return (scriptObjectSpecifier) return (scriptObjectSpecifier)
} }
@ -39,76 +39,76 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
// article.uniqueID here is the feed unique id // article.uniqueID here is the feed unique id
@objc(uniqueId) @objc(uniqueId)
var scriptingUniqueId:Any { var scriptingUniqueId: Any {
return article.uniqueID return article.uniqueID
} }
// MARK: --- ScriptingObjectContainer protocol --- // MARK: --- ScriptingObjectContainer protocol ---
var scriptingClassDescription: NSScriptClassDescription { var scriptingClassDescription: NSScriptClassDescription {
return self.classDescription as! NSScriptClassDescription return self.classDescription as! NSScriptClassDescription
} }
func deleteElement(_ element:ScriptingObject) { func deleteElement(_ element: ScriptingObject) {
print ("delete event not handled") print("delete event not handled")
} }
// MARK: --- Scriptable properties --- // MARK: --- Scriptable properties ---
@objc(url) @objc(url)
var url:String? { var url: String? {
return article.preferredLink return article.preferredLink
} }
@objc(permalink) @objc(permalink)
var permalink:String? { var permalink: String? {
return article.link return article.link
} }
@objc(externalUrl) @objc(externalUrl)
var externalUrl:String? { var externalUrl: String? {
return article.externalLink return article.externalLink
} }
@objc(title) @objc(title)
var title:String { var title: String {
return article.title ?? "" return article.title ?? ""
} }
@objc(contents) @objc(contents)
var contents:String { var contents: String {
return article.contentText ?? "" return article.contentText ?? ""
} }
@objc(html) @objc(html)
var html:String { var html: String {
return article.contentHTML ?? "" return article.contentHTML ?? ""
} }
@objc(summary) @objc(summary)
var summary:String { var summary: String {
return article.summary ?? "" return article.summary ?? ""
} }
@objc(datePublished) @objc(datePublished)
var datePublished:Date? { var datePublished: Date? {
return article.datePublished return article.datePublished
} }
@objc(dateModified) @objc(dateModified)
var dateModified:Date? { var dateModified: Date? {
return article.dateModified return article.dateModified
} }
@objc(dateArrived) @objc(dateArrived)
var dateArrived:Date { var dateArrived: Date {
return article.status.dateArrived return article.status.dateArrived
} }
@objc(read) @objc(read)
var read:Bool { var read: Bool {
get { get {
return article.status.boolStatus(forKey:.read) return article.status.boolStatus(forKey: .read)
} }
set { set {
markArticles([self.article], statusKey: .read, flag: newValue) markArticles([self.article], statusKey: .read, flag: newValue)
@ -116,9 +116,9 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
} }
@objc(starred) @objc(starred)
var starred:Bool { var starred: Bool {
get { get {
return article.status.boolStatus(forKey:.starred) return article.status.boolStatus(forKey: .starred)
} }
set { set {
markArticles([self.article], statusKey: .starred, flag: newValue) markArticles([self.article], statusKey: .starred, flag: newValue)
@ -126,19 +126,19 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
} }
@objc(deleted) @objc(deleted)
var deleted:Bool { var deleted: Bool {
return false return false
} }
@objc(imageURL) @objc(imageURL)
var imageURL:String { var imageURL: String {
return article.imageLink ?? "" return article.imageLink ?? ""
} }
@objc(authors) @objc(authors)
var authors:NSArray { var authors: NSArray {
let articleAuthors = article.authors ?? [] let articleAuthors = article.authors ?? []
return articleAuthors.map { ScriptableAuthor($0, container:self) } as NSArray return articleAuthors.map { ScriptableAuthor($0, container: self) } as NSArray
} }
@objc(feed) @objc(feed)

View File

@ -13,17 +13,17 @@ import Articles
@objc(ScriptableAuthor) @objc(ScriptableAuthor)
class ScriptableAuthor: NSObject, UniqueIdScriptingObject { class ScriptableAuthor: NSObject, UniqueIdScriptingObject {
let author:Author let author: Author
let container:ScriptingObjectContainer let container: ScriptingObjectContainer
init (_ author:Author, container:ScriptingObjectContainer) { init (_ author: Author, container: ScriptingObjectContainer) {
self.author = author self.author = author
self.container = container self.container = container
} }
@objc(objectSpecifier) @objc(objectSpecifier)
override var objectSpecifier: NSScriptObjectSpecifier? { override var objectSpecifier: NSScriptObjectSpecifier? {
let scriptObjectSpecifier = self.container.makeFormUniqueIDScriptObjectSpecifier(forObject:self) let scriptObjectSpecifier = self.container.makeFormUniqueIDScriptObjectSpecifier(forObject: self)
return (scriptObjectSpecifier) return (scriptObjectSpecifier)
} }
@ -41,29 +41,29 @@ class ScriptableAuthor: NSObject, UniqueIdScriptingObject {
// MARK: --- UniqueIdScriptingObject protocol --- // MARK: --- UniqueIdScriptingObject protocol ---
@objc(uniqueId) @objc(uniqueId)
var scriptingUniqueId:Any { var scriptingUniqueId: Any {
return author.authorID return author.authorID
} }
// MARK: --- Scriptable properties --- // MARK: --- Scriptable properties ---
@objc(url) @objc(url)
var url:String { var url: String {
return self.author.url ?? "" return self.author.url ?? ""
} }
@objc(name) @objc(name)
var name:String { var name: String {
return self.author.name ?? "" return self.author.name ?? ""
} }
@objc(avatarURL) @objc(avatarURL)
var avatarURL:String { var avatarURL: String {
return self.author.avatarURL ?? "" return self.author.avatarURL ?? ""
} }
@objc(emailAddress) @objc(emailAddress)
var emailAddress:String { var emailAddress: String {
return self.author.emailAddress ?? "" return self.author.emailAddress ?? ""
} }
} }

View File

@ -14,17 +14,17 @@ import RSCore
@objc(ScriptableFolder) @objc(ScriptableFolder)
class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer { class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let folder:Folder let folder: Folder
let container:ScriptingObjectContainer let container: ScriptingObjectContainer
init (_ folder:Folder, container:ScriptingObjectContainer) { init (_ folder: Folder, container: ScriptingObjectContainer) {
self.folder = folder self.folder = folder
self.container = container self.container = container
} }
@objc(objectSpecifier) @objc(objectSpecifier)
override var objectSpecifier: NSScriptObjectSpecifier? { override var objectSpecifier: NSScriptObjectSpecifier? {
let scriptObjectSpecifier = self.container.makeFormUniqueIDScriptObjectSpecifier(forObject:self) let scriptObjectSpecifier = self.container.makeFormUniqueIDScriptObjectSpecifier(forObject: self)
return (scriptObjectSpecifier) return (scriptObjectSpecifier)
} }
@ -40,20 +40,20 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai
// but in either case it seems like the accountID would be used as the keydata, so I chose ID // but in either case it seems like the accountID would be used as the keydata, so I chose ID
@objc(uniqueId) @objc(uniqueId)
var scriptingUniqueId:Any { var scriptingUniqueId: Any {
return folder.folderID return folder.folderID
} }
// MARK: --- ScriptingObjectContainer protocol --- // MARK: --- ScriptingObjectContainer protocol ---
var scriptingClassDescription: NSScriptClassDescription { var scriptingClassDescription: NSScriptClassDescription {
return self.classDescription as! NSScriptClassDescription return self.classDescription as! NSScriptClassDescription
} }
func deleteElement(_ element:ScriptingObject) { func deleteElement(_ element: ScriptingObject) {
if let scriptableFeed = element as? ScriptableFeed { if let scriptableFeed = element as? ScriptableFeed {
BatchUpdate.shared.perform { BatchUpdate.shared.perform {
folder.account?.removeFeed(scriptableFeed.feed, from: folder) { result in } folder.account?.removeFeed(scriptableFeed.feed, from: folder) { _ in }
} }
} }
} }
@ -65,52 +65,52 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai
or or
tell account X to make new folder at end with properties {name:"new folder name"} tell account X to make new folder at end with properties {name:"new folder name"}
*/ */
class func handleCreateElement(command:NSCreateCommand) -> Any? { class func handleCreateElement(command: NSCreateCommand) -> Any? {
guard command.isCreateCommand(forClass:"fold") else { return nil } guard command.isCreateCommand(forClass: "fold") else { return nil }
let name = command.property(forKey:"name") as? String ?? "" let name = command.property(forKey: "name") as? String ?? ""
// some combination of the tell target and the location specifier ("in" or "at") // some combination of the tell target and the location specifier ("in" or "at")
// identifies where the new folder should be created // identifies where the new folder should be created
let (account, folder) = command.accountAndFolderForNewChild() let (account, folder) = command.accountAndFolderForNewChild()
guard folder == nil else { guard folder == nil else {
print("support for folders within folders is NYI"); print("support for folders within folders is NYI")
return nil return nil
} }
command.suspendExecution() command.suspendExecution()
account.addFolder(name) { result in account.addFolder(name) { result in
switch result { switch result {
case .success(let folder): case .success(let folder):
let scriptableAccount = ScriptableAccount(account) let scriptableAccount = ScriptableAccount(account)
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount) let scriptableFolder = ScriptableFolder(folder, container: scriptableAccount)
command.resumeExecution(withResult:scriptableFolder.objectSpecifier) command.resumeExecution(withResult: scriptableFolder.objectSpecifier)
case .failure: case .failure:
command.resumeExecution(withResult:nil) command.resumeExecution(withResult: nil)
} }
} }
return nil return nil
} }
// MARK: --- Scriptable elements --- // MARK: --- Scriptable elements ---
@objc(feeds) @objc(feeds)
var feeds:NSArray { var feeds: NSArray {
let feeds = Array(folder.topLevelFeeds) let feeds = Array(folder.topLevelFeeds)
return feeds.map { ScriptableFeed($0, container:self) } as NSArray return feeds.map { ScriptableFeed($0, container: self) } as NSArray
} }
// MARK: --- Scriptable properties --- // MARK: --- Scriptable properties ---
@objc(name) @objc(name)
var name:String { var name: String {
return self.folder.name ?? "" return self.folder.name ?? ""
} }
@objc(opmlRepresentation) @objc(opmlRepresentation)
var opmlRepresentation:String { var opmlRepresentation: String {
return self.folder.OPMLString(indentLevel:0) return self.folder.OPMLString(indentLevel: 0)
} }
} }

View File

@ -13,4 +13,3 @@ protocol ScriptingMainWindowController {
var scriptingCurrentArticle: Article? { get } var scriptingCurrentArticle: Article? { get }
var scriptingSelectedArticles: [Article] { get } var scriptingSelectedArticles: [Article] { get }
} }

View File

@ -10,7 +10,7 @@ import AppKit
import Account import Account
import Articles import Articles
extension NSApplication : ScriptingObjectContainer { extension NSApplication: ScriptingObjectContainer {
// MARK: --- ScriptingObjectContainer protocol --- // MARK: --- ScriptingObjectContainer protocol ---
@ -18,21 +18,21 @@ extension NSApplication : ScriptingObjectContainer {
return NSApplication.shared.classDescription as! NSScriptClassDescription return NSApplication.shared.classDescription as! NSScriptClassDescription
} }
func deleteElement(_ element:ScriptingObject) { func deleteElement(_ element: ScriptingObject) {
print ("delete event not handled") print("delete event not handled")
} }
var scriptingKey: String { var scriptingKey: String {
return "application" return "application"
} }
@objc(currentArticle) @objc(currentArticle)
func currentArticle() -> ScriptableArticle? { func currentArticle() -> ScriptableArticle? {
var scriptableArticle: ScriptableArticle? var scriptableArticle: ScriptableArticle?
if let currentArticle = appDelegate.scriptingCurrentArticle { if let currentArticle = appDelegate.scriptingCurrentArticle {
if let feed = currentArticle.feed { if let feed = currentArticle.feed {
let scriptableFeed = ScriptableFeed(feed, container:self) let scriptableFeed = ScriptableFeed(feed, container: self)
scriptableArticle = ScriptableArticle(currentArticle, container:scriptableFeed) scriptableArticle = ScriptableArticle(currentArticle, container: scriptableFeed)
} }
} }
return scriptableArticle return scriptableArticle
@ -41,8 +41,8 @@ extension NSApplication : ScriptingObjectContainer {
@objc(selectedArticles) @objc(selectedArticles)
func selectedArticles() -> NSArray { func selectedArticles() -> NSArray {
let articles = appDelegate.scriptingSelectedArticles let articles = appDelegate.scriptingSelectedArticles
let scriptableArticles:[ScriptableArticle] = articles.compactMap { article in let scriptableArticles: [ScriptableArticle] = articles.compactMap { article in
if let feed = article.feed, let account = feed.account { if let feed = article.feed, let account = feed.account {
let scriptableFeed = ScriptableFeed(feed, container: ScriptableAccount(account)) let scriptableFeed = ScriptableFeed(feed, container: ScriptableAccount(account))
return ScriptableArticle(article, container: scriptableFeed) return ScriptableArticle(article, container: scriptableFeed)
} else { } else {
@ -59,11 +59,11 @@ extension NSApplication : ScriptingObjectContainer {
let accounts = AccountManager.shared.accounts let accounts = AccountManager.shared.accounts
return accounts.map { ScriptableAccount($0) } as NSArray return accounts.map { ScriptableAccount($0) } as NSArray
} }
@objc(valueInAccountsWithUniqueID:) @objc(valueInAccountsWithUniqueID:)
func valueInAccounts(withUniqueID id:String) -> ScriptableAccount? { func valueInAccounts(withUniqueID id: String) -> ScriptableAccount? {
let accounts = AccountManager.shared.accounts let accounts = AccountManager.shared.accounts
guard let account = accounts.first(where:{$0.accountID == id}) else { return nil } guard let account = accounts.first(where: {$0.accountID == id}) else { return nil }
return ScriptableAccount(account) return ScriptableAccount(account)
} }
@ -72,10 +72,10 @@ extension NSApplication : ScriptingObjectContainer {
this allows a script like 'articles of feed "The Shape of Everything"' as a shorthand this allows a script like 'articles of feed "The Shape of Everything"' as a shorthand
for 'articles of feed "The Shape of Everything" of account "On My Mac"' for 'articles of feed "The Shape of Everything" of account "On My Mac"'
*/ */
func allFeeds() -> [Feed] { func allFeeds() -> [Feed] {
let accounts = AccountManager.shared.activeAccounts let accounts = AccountManager.shared.activeAccounts
let emptyFeeds:[Feed] = [] let emptyFeeds: [Feed] = []
return accounts.reduce(emptyFeeds) { (result, nthAccount) -> [Feed] in return accounts.reduce(emptyFeeds) { (result, nthAccount) -> [Feed] in
let accountFeeds = Array(nthAccount.topLevelFeeds) let accountFeeds = Array(nthAccount.topLevelFeeds)
return result + accountFeeds return result + accountFeeds
@ -85,15 +85,13 @@ extension NSApplication : ScriptingObjectContainer {
@objc(feeds) @objc(feeds)
func feeds() -> NSArray { func feeds() -> NSArray {
let feeds = self.allFeeds() let feeds = self.allFeeds()
return feeds.map { ScriptableFeed($0, container:self) } as NSArray return feeds.map { ScriptableFeed($0, container: self) } as NSArray
} }
@objc(valueInFeedsWithUniqueID:) @objc(valueInFeedsWithUniqueID:)
func valueInFeeds(withUniqueID id:String) -> ScriptableFeed? { func valueInFeeds(withUniqueID id: String) -> ScriptableFeed? {
let feeds = self.allFeeds() let feeds = self.allFeeds()
guard let feed = feeds.first(where:{$0.feedID == id}) else { return nil } guard let feed = feeds.first(where: {$0.feedID == id}) else { return nil }
return ScriptableFeed(feed, container:self) return ScriptableFeed(feed, container: self)
} }
} }

View File

@ -10,43 +10,43 @@ import Foundation
import Account import Account
extension NSScriptCommand { extension NSScriptCommand {
func property(forKey key:String) -> Any? { func property(forKey key: String) -> Any? {
if let evaluatedArguments = self.evaluatedArguments { if let evaluatedArguments = self.evaluatedArguments {
if let props = evaluatedArguments["KeyDictionary"] as? [String: Any] { if let props = evaluatedArguments["KeyDictionary"] as? [String: Any] {
return props[key] return props[key]
} }
} }
return nil return nil
} }
func isCreateCommand(forClass whatClass:String) -> Bool { func isCreateCommand(forClass whatClass: String) -> Bool {
guard let arguments = self.arguments else {return false} guard let arguments = self.arguments else {return false}
guard let newObjectClass = arguments["ObjectClass"] as? Int else {return false} guard let newObjectClass = arguments["ObjectClass"] as? Int else {return false}
guard (newObjectClass.fourCharCode == whatClass.fourCharCode) else {return false} guard newObjectClass.fourCharCode == whatClass.fourCharCode else {return false}
return true return true
} }
func accountAndFolderForNewChild() -> (Account, Folder?) { func accountAndFolderForNewChild() -> (Account, Folder?) {
let appleEvent = self.appleEvent let appleEvent = self.appleEvent
var account = AccountManager.shared.defaultAccount var account = AccountManager.shared.defaultAccount
var folder:Folder? = nil var folder: Folder?
if let appleEvent = appleEvent { if let appleEvent = appleEvent {
var descriptorToConsider:NSAppleEventDescriptor? var descriptorToConsider: NSAppleEventDescriptor?
if let insertionLocationDescriptor = appleEvent.paramDescriptor(forKeyword:keyAEInsertHere) { if let insertionLocationDescriptor = appleEvent.paramDescriptor(forKeyword: keyAEInsertHere) {
print("insertionLocation : \(insertionLocationDescriptor)") print("insertionLocation : \(insertionLocationDescriptor)")
// insertion location can be a typeObjectSpecifier, e.g. 'in account "Acct"' // insertion location can be a typeObjectSpecifier, e.g. 'in account "Acct"'
// or a typeInsertionLocation, e.g. 'at end of folder " // or a typeInsertionLocation, e.g. 'at end of folder "
if (insertionLocationDescriptor.descriptorType == "insl".fourCharCode) { if insertionLocationDescriptor.descriptorType == "insl".fourCharCode {
descriptorToConsider = insertionLocationDescriptor.forKeyword("kobj".fourCharCode) descriptorToConsider = insertionLocationDescriptor.forKeyword("kobj".fourCharCode)
} else if ( insertionLocationDescriptor.descriptorType == "obj ".fourCharCode) { } else if insertionLocationDescriptor.descriptorType == "obj ".fourCharCode {
descriptorToConsider = insertionLocationDescriptor descriptorToConsider = insertionLocationDescriptor
} }
} else if let subjectDescriptor = appleEvent.attributeDescriptor(forKeyword:"subj".fourCharCode) { } else if let subjectDescriptor = appleEvent.attributeDescriptor(forKeyword: "subj".fourCharCode) {
descriptorToConsider = subjectDescriptor descriptorToConsider = subjectDescriptor
} }
if let descriptorToConsider = descriptorToConsider { if let descriptorToConsider = descriptorToConsider {
guard let newContainerSpecifier = NSScriptObjectSpecifier(descriptor:descriptorToConsider) else {return (account, folder)} guard let newContainerSpecifier = NSScriptObjectSpecifier(descriptor: descriptorToConsider) else {return (account, folder)}
let newContainer = newContainerSpecifier.objectsByEvaluatingSpecifier let newContainer = newContainerSpecifier.objectsByEvaluatingSpecifier
if let scriptableAccount = newContainer as? ScriptableAccount { if let scriptableAccount = newContainer as? ScriptableAccount {
account = scriptableAccount.account account = scriptableAccount.account

View File

@ -9,14 +9,14 @@
import Foundation import Foundation
protocol ScriptingObject { protocol ScriptingObject {
var objectSpecifier: NSScriptObjectSpecifier? { get } var objectSpecifier: NSScriptObjectSpecifier? { get }
var scriptingKey: String { get } var scriptingKey: String { get }
} }
protocol NamedScriptingObject: ScriptingObject { protocol NamedScriptingObject: ScriptingObject {
var name:String { get } var name: String { get }
} }
protocol UniqueIdScriptingObject: ScriptingObject { protocol UniqueIdScriptingObject: ScriptingObject {
var scriptingUniqueId:Any { get } var scriptingUniqueId: Any { get }
} }

View File

@ -10,30 +10,29 @@ import AppKit
import Account import Account
protocol ScriptingObjectContainer: ScriptingObject { protocol ScriptingObjectContainer: ScriptingObject {
var scriptingClassDescription:NSScriptClassDescription { get } var scriptingClassDescription: NSScriptClassDescription { get }
func deleteElement(_ element:ScriptingObject) func deleteElement(_ element: ScriptingObject)
} }
extension ScriptingObjectContainer { extension ScriptingObjectContainer {
func makeFormNameScriptObjectSpecifier(forObject object:NamedScriptingObject) -> NSScriptObjectSpecifier? { func makeFormNameScriptObjectSpecifier(forObject object: NamedScriptingObject) -> NSScriptObjectSpecifier? {
let containerClassDescription = self.scriptingClassDescription let containerClassDescription = self.scriptingClassDescription
let containerScriptObjectSpecifier = self.objectSpecifier let containerScriptObjectSpecifier = self.objectSpecifier
let scriptingKey = object.scriptingKey let scriptingKey = object.scriptingKey
let name = object.name let name = object.name
let specifier = NSNameSpecifier(containerClassDescription:containerClassDescription, let specifier = NSNameSpecifier(containerClassDescription: containerClassDescription,
containerSpecifier:containerScriptObjectSpecifier, key:scriptingKey, name:name) containerSpecifier: containerScriptObjectSpecifier, key: scriptingKey, name: name)
return specifier return specifier
} }
func makeFormUniqueIDScriptObjectSpecifier(forObject object:UniqueIdScriptingObject) -> NSScriptObjectSpecifier? { func makeFormUniqueIDScriptObjectSpecifier(forObject object: UniqueIdScriptingObject) -> NSScriptObjectSpecifier? {
let containerClassDescription = self.scriptingClassDescription let containerClassDescription = self.scriptingClassDescription
let containerScriptObjectSpecifier = self.objectSpecifier let containerScriptObjectSpecifier = self.objectSpecifier
let scriptingKey = object.scriptingKey let scriptingKey = object.scriptingKey
let uniqueId = object.scriptingUniqueId let uniqueId = object.scriptingUniqueId
let specifier = NSUniqueIDSpecifier(containerClassDescription:containerClassDescription, let specifier = NSUniqueIDSpecifier(containerClassDescription: containerClassDescription,
containerSpecifier:containerScriptObjectSpecifier, key:scriptingKey, uniqueID: uniqueId) containerSpecifier: containerScriptObjectSpecifier, key: scriptingKey, uniqueID: uniqueId)
return specifier return specifier
} }
} }

View File

@ -16,15 +16,15 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
let feed: Feed let feed: Feed
let container: ScriptingObjectContainer let container: ScriptingObjectContainer
init (_ feed:Feed, container:ScriptingObjectContainer) { init (_ feed: Feed, container: ScriptingObjectContainer) {
self.feed = feed self.feed = feed
self.container = container self.container = container
} }
@objc(objectSpecifier) @objc(objectSpecifier)
override var objectSpecifier: NSScriptObjectSpecifier? { override var objectSpecifier: NSScriptObjectSpecifier? {
let scriptObjectSpecifier = self.container.makeFormUniqueIDScriptObjectSpecifier(forObject:self) let scriptObjectSpecifier = self.container.makeFormUniqueIDScriptObjectSpecifier(forObject: self)
return (scriptObjectSpecifier) return (scriptObjectSpecifier)
} }
@ -44,56 +44,56 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
// I am not sure if account should prefer to be specified by name or by ID // I am not sure if account should prefer to be specified by name or by ID
// but in either case it seems like the accountID would be used as the keydata, so I chose ID // but in either case it seems like the accountID would be used as the keydata, so I chose ID
@objc(uniqueId) @objc(uniqueId)
var scriptingUniqueId:Any { var scriptingUniqueId: Any {
return feed.feedID return feed.feedID
} }
// MARK: --- ScriptingObjectContainer protocol --- // MARK: --- ScriptingObjectContainer protocol ---
var scriptingClassDescription: NSScriptClassDescription { var scriptingClassDescription: NSScriptClassDescription {
return self.classDescription as! NSScriptClassDescription return self.classDescription as! NSScriptClassDescription
} }
func deleteElement(_ element:ScriptingObject) { func deleteElement(_ element: ScriptingObject) {
} }
// MARK: --- handle NSCreateCommand --- // MARK: --- handle NSCreateCommand ---
class func urlForNewFeed(arguments:[String:Any]) -> String? { class func urlForNewFeed(arguments: [String: Any]) -> String? {
var url:String? var url: String?
if let withDataParam = arguments["ObjectData"] { if let withDataParam = arguments["ObjectData"] {
if let objectDataDescriptor = withDataParam as? NSAppleEventDescriptor { if let objectDataDescriptor = withDataParam as? NSAppleEventDescriptor {
url = objectDataDescriptor.stringValue url = objectDataDescriptor.stringValue
} }
} else if let withPropsParam = arguments["ObjectProperties"] as? [String:Any] { } else if let withPropsParam = arguments["ObjectProperties"] as? [String: Any] {
url = withPropsParam["url"] as? String url = withPropsParam["url"] as? String
} }
return url return url
} }
class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableFeed { class func scriptableFeed(_ feed: Feed, account: Account, folder: Folder?) -> ScriptableFeed {
let scriptableAccount = ScriptableAccount(account) let scriptableAccount = ScriptableAccount(account)
if let folder = folder { if let folder = folder {
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount) let scriptableFolder = ScriptableFolder(folder, container: scriptableAccount)
return ScriptableFeed(feed, container:scriptableFolder) return ScriptableFeed(feed, container: scriptableFolder)
} else { } else {
return ScriptableFeed(feed, container:scriptableAccount) return ScriptableFeed(feed, container: scriptableAccount)
} }
} }
class func handleCreateElement(command:NSCreateCommand) -> Any? { class func handleCreateElement(command: NSCreateCommand) -> Any? {
guard command.isCreateCommand(forClass:"Feed") else { return nil } guard command.isCreateCommand(forClass: "Feed") else { return nil }
guard let arguments = command.arguments else {return nil} guard let arguments = command.arguments else {return nil}
let titleFromArgs = command.property(forKey:"name") as? String let titleFromArgs = command.property(forKey: "name") as? String
let (account, folder) = command.accountAndFolderForNewChild() let (account, folder) = command.accountAndFolderForNewChild()
guard let url = self.urlForNewFeed(arguments:arguments) else {return nil} guard let url = self.urlForNewFeed(arguments: arguments) else {return nil}
if let existingFeed = account.existingFeed(withURL:url) { if let existingFeed = account.existingFeed(withURL: url) {
return scriptableFeed(existingFeed, account:account, folder:folder).objectSpecifier return scriptableFeed(existingFeed, account: account, folder: folder).objectSpecifier
} }
let container: Container = folder != nil ? folder! : account let container: Container = folder != nil ? folder! : account
// We need to download the feed and parse it. // We need to download the feed and parse it.
// Parser does the callback for the download on the main thread. // Parser does the callback for the download on the main thread.
// Because we can't wait here (on the main thread) for the callback, we have to return from this function. // Because we can't wait here (on the main thread) for the callback, we have to return from this function.
@ -101,83 +101,83 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
// but we dont yet have the result of the event yet, so we prevent the Apple event from returning by calling // but we dont yet have the result of the event yet, so we prevent the Apple event from returning by calling
// suspendExecution(). When we get the callback, we supply the event result and call resumeExecution(). // suspendExecution(). When we get the callback, we supply the event result and call resumeExecution().
command.suspendExecution() command.suspendExecution()
account.createFeed(url: url, name: titleFromArgs, container: container, validateFeed: true) { result in account.createFeed(url: url, name: titleFromArgs, container: container, validateFeed: true) { result in
switch result { switch result {
case .success(let feed): case .success(let feed):
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed]) NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
let scriptableFeed = self.scriptableFeed(feed, account:account, folder:folder) let scriptableFeed = self.scriptableFeed(feed, account: account, folder: folder)
command.resumeExecution(withResult:scriptableFeed.objectSpecifier) command.resumeExecution(withResult: scriptableFeed.objectSpecifier)
case .failure: case .failure:
command.resumeExecution(withResult:nil) command.resumeExecution(withResult: nil)
} }
} }
return nil return nil
} }
// MARK: --- Scriptable properties --- // MARK: --- Scriptable properties ---
@objc(url) @objc(url)
var url:String { var url: String {
return self.feed.url return self.feed.url
} }
@objc(name) @objc(name)
var name:String { var name: String {
return self.feed.name ?? "" return self.feed.name ?? ""
} }
@objc(homePageURL) @objc(homePageURL)
var homePageURL:String { var homePageURL: String {
return self.feed.homePageURL ?? "" return self.feed.homePageURL ?? ""
} }
@objc(iconURL) @objc(iconURL)
var iconURL:String { var iconURL: String {
return self.feed.iconURL ?? "" return self.feed.iconURL ?? ""
} }
@objc(faviconURL) @objc(faviconURL)
var faviconURL:String { var faviconURL: String {
return self.feed.faviconURL ?? "" return self.feed.faviconURL ?? ""
} }
@objc(opmlRepresentation) @objc(opmlRepresentation)
var opmlRepresentation:String { var opmlRepresentation: String {
return self.feed.OPMLString(indentLevel:0) return self.feed.OPMLString(indentLevel: 0)
} }
// MARK: --- scriptable elements --- // MARK: --- scriptable elements ---
@objc(authors) @objc(authors)
var authors:NSArray { var authors: NSArray {
let feedAuthors = feed.authors ?? [] let feedAuthors = feed.authors ?? []
return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray return feedAuthors.map { ScriptableAuthor($0, container: self) } as NSArray
} }
@objc(valueInAuthorsWithUniqueID:) @objc(valueInAuthorsWithUniqueID:)
func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? { func valueInAuthors(withUniqueID id: String) -> ScriptableAuthor? {
guard let author = feed.authors?.first(where:{$0.authorID == id}) else { return nil } guard let author = feed.authors?.first(where: {$0.authorID == id}) else { return nil }
return ScriptableAuthor(author, container:self) return ScriptableAuthor(author, container: self)
} }
@objc(articles) @objc(articles)
var articles:NSArray { var articles: NSArray {
let feedArticles = (try? feed.fetchArticles()) ?? Set<Article>() let feedArticles = (try? feed.fetchArticles()) ?? Set<Article>()
// the articles are a set, use the sorting algorithm from the viewer // the articles are a set, use the sorting algorithm from the viewer
let sortedArticles = feedArticles.sorted(by:{ let sortedArticles = feedArticles.sorted(by: {
return $0.logicalDatePublished > $1.logicalDatePublished return $0.logicalDatePublished > $1.logicalDatePublished
}) })
return sortedArticles.map { ScriptableArticle($0, container:self) } as NSArray return sortedArticles.map { ScriptableArticle($0, container: self) } as NSArray
} }
@objc(valueInArticlesWithUniqueID:) @objc(valueInArticlesWithUniqueID:)
func valueInArticles(withUniqueID id:String) -> ScriptableArticle? { func valueInArticles(withUniqueID id: String) -> ScriptableArticle? {
let articles = (try? feed.fetchArticles()) ?? Set<Article>() let articles = (try? feed.fetchArticles()) ?? Set<Article>()
guard let article = articles.first(where:{$0.uniqueID == id}) else { return nil } guard let article = articles.first(where: {$0.uniqueID == id}) else { return nil }
return ScriptableArticle(article, container:self) return ScriptableArticle(article, container: self)
} }
} }

View File

@ -14,7 +14,7 @@ class ShareViewController: NSViewController {
@IBOutlet weak var nameTextField: NSTextField! @IBOutlet weak var nameTextField: NSTextField!
@IBOutlet weak var folderPopUpButton: NSPopUpButton! @IBOutlet weak var folderPopUpButton: NSPopUpButton!
private var url: URL? private var url: URL?
private var extensionContainers: ExtensionContainers? private var extensionContainers: ExtensionContainers?
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ShareViewController") private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ShareViewController")
@ -25,11 +25,11 @@ class ShareViewController: NSViewController {
override func loadView() { override func loadView() {
super.loadView() super.loadView()
extensionContainers = ExtensionContainersFile.read() extensionContainers = ExtensionContainersFile.read()
buildFolderPopupMenu() buildFolderPopupMenu()
var provider: NSItemProvider? = nil var provider: NSItemProvider?
// Try to get any HTML that is maybe passed in // Try to get any HTML that is maybe passed in
for item in self.extensionContext!.inputItems as! [NSExtensionItem] { for item in self.extensionContext!.inputItems as! [NSExtensionItem] {
@ -40,7 +40,7 @@ class ShareViewController: NSViewController {
} }
} }
if provider != nil { if provider != nil {
provider!.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { [weak self] (pList, error) in provider!.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { [weak self] (pList, error) in
if error != nil { if error != nil {
return return
@ -67,7 +67,7 @@ class ShareViewController: NSViewController {
} }
} }
if provider != nil { if provider != nil {
provider!.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil, completionHandler: { [weak self] (urlCoded, error) in provider!.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil, completionHandler: { [weak self] (urlCoded, error) in
if error != nil { if error != nil {
return return
@ -93,7 +93,7 @@ class ShareViewController: NSViewController {
let name = nameTextField.stringValue.isEmpty ? nil : nameTextField.stringValue let name = nameTextField.stringValue.isEmpty ? nil : nameTextField.stringValue
let request = ExtensionFeedAddRequest(name: name, feedURL: url, destinationContainerID: containerID) let request = ExtensionFeedAddRequest(name: name, feedURL: url, destinationContainerID: containerID)
ExtensionFeedAddRequestFile.save(request) ExtensionFeedAddRequestFile.save(request)
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
} }
@ -105,31 +105,31 @@ class ShareViewController: NSViewController {
} }
private extension ShareViewController { private extension ShareViewController {
func buildFolderPopupMenu() { func buildFolderPopupMenu() {
let menu = NSMenu(title: "Folders") let menu = NSMenu(title: "Folders")
menu.autoenablesItems = false menu.autoenablesItems = false
guard let extensionContainers = extensionContainers else { guard let extensionContainers = extensionContainers else {
folderPopUpButton.menu = nil folderPopUpButton.menu = nil
return return
} }
let defaultContainer = ShareDefaultContainer.defaultContainer(containers: extensionContainers) let defaultContainer = ShareDefaultContainer.defaultContainer(containers: extensionContainers)
var defaultMenuItem: NSMenuItem? = nil var defaultMenuItem: NSMenuItem?
for account in extensionContainers.accounts { for account in extensionContainers.accounts {
let menuItem = NSMenuItem(title: account.name, action: nil, keyEquivalent: "") let menuItem = NSMenuItem(title: account.name, action: nil, keyEquivalent: "")
menuItem.representedObject = account menuItem.representedObject = account
if account.disallowFeedInRootFolder { if account.disallowFeedInRootFolder {
menuItem.isEnabled = false menuItem.isEnabled = false
} }
menu.addItem(menuItem) menu.addItem(menuItem)
if defaultContainer?.containerID == account.containerID { if defaultContainer?.containerID == account.containerID {
defaultMenuItem = menuItem defaultMenuItem = menuItem
} }
@ -143,15 +143,15 @@ private extension ShareViewController {
defaultMenuItem = menuItem defaultMenuItem = menuItem
} }
} }
} }
folderPopUpButton.menu = menu folderPopUpButton.menu = menu
folderPopUpButton.select(defaultMenuItem) folderPopUpButton.select(defaultMenuItem)
} }
func selectedContainer() -> ExtensionContainer? { func selectedContainer() -> ExtensionContainer? {
return folderPopUpButton.selectedItem?.representedObject as? ExtensionContainer return folderPopUpButton.selectedItem?.representedObject as? ExtensionContainer
} }
} }