Fix lint issues.
This commit is contained in:
parent
8f1379360c
commit
40ada2ba5a
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
|
@ -59,4 +59,3 @@ final class CrashReportWindowController: NSWindowController {
|
|||||||
close()
|
close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
//}
|
// }
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,4 +33,3 @@ final class MainWindowKeyboardHandler: KeyboardDelegate {
|
|||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,4 +141,3 @@ extension NNW3Feed: OPMLRepresentable {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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.")
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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?
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 {
|
||||||
|
@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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 = ""
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -34,4 +34,3 @@ final class TimelineContainerView: NSView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 doesn’t.")
|
assertionFailure("Expected timelineViewController to match either regular or search timelineViewController, but it doesn’t.")
|
||||||
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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: "")
|
||||||
|
@ -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 there’s no async delay,
|
// It blocks the main thread, so that there’s 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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
|||||||
}))
|
}))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) ?? ""
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
@ -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"
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
|
@ -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 ?? ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -13,4 +13,3 @@ protocol ScriptingMainWindowController {
|
|||||||
var scriptingCurrentArticle: Article? { get }
|
var scriptingCurrentArticle: Article? { get }
|
||||||
var scriptingSelectedArticles: [Article] { get }
|
var scriptingSelectedArticles: [Article] { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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 }
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 don’t yet have the result of the event yet, so we prevent the Apple event from returning by calling
|
// but we don’t 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user