diff --git a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift index 5c8733317..0553513e1 100644 --- a/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift +++ b/Account/Sources/Account/CloudKit/CloudKitArticlesZone.swift @@ -223,10 +223,10 @@ private extension CloudKitArticlesZone { record[CloudKitArticle.Fields.title] = article.title record[CloudKitArticle.Fields.contentHTML] = article.contentHTML record[CloudKitArticle.Fields.contentText] = article.contentText - record[CloudKitArticle.Fields.url] = article.url - record[CloudKitArticle.Fields.externalURL] = article.externalURL + record[CloudKitArticle.Fields.url] = article.rawLink + record[CloudKitArticle.Fields.externalURL] = article.rawExternalLink record[CloudKitArticle.Fields.summary] = article.summary - record[CloudKitArticle.Fields.imageURL] = article.imageURL + record[CloudKitArticle.Fields.imageURL] = article.rawImageLink record[CloudKitArticle.Fields.datePublished] = article.datePublished record[CloudKitArticle.Fields.dateModified] = article.dateModified diff --git a/Account/Sources/Account/Feed.swift b/Account/Sources/Account/Feed.swift index 958b25106..23d842f0a 100644 --- a/Account/Sources/Account/Feed.swift +++ b/Account/Sources/Account/Feed.swift @@ -17,6 +17,7 @@ public enum ReadFilterType { public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider { + var account: Account? { get } var defaultReadFilterType: ReadFilterType { get } } diff --git a/Account/Sources/Account/FeedIdentifier.swift b/Account/Sources/Account/FeedIdentifier.swift index 842963e74..05d3d7f23 100644 --- a/Account/Sources/Account/FeedIdentifier.swift +++ b/Account/Sources/Account/FeedIdentifier.swift @@ -12,7 +12,7 @@ public protocol FeedIdentifiable { var feedID: FeedIdentifier? { get } } -public enum FeedIdentifier: CustomStringConvertible, Hashable { +public enum FeedIdentifier: CustomStringConvertible, Hashable, Equatable { case smartFeed(String) // String is a unique identifier case script(String) // String is a unique identifier @@ -80,22 +80,4 @@ public enum FeedIdentifier: CustomStringConvertible, Hashable { } } - // MARK: - Hashable - - public func hash(into hasher: inout Hasher) { - switch self { - case .smartFeed(let id): - hasher.combine("smartFeed") - hasher.combine(id) - case .script(let id): - hasher.combine("smartFeed") - hasher.combine(id) - case .webFeed(_, let webFeedID): - hasher.combine("webFeed") - hasher.combine(webFeedID) - case .folder(_, let folderName): - hasher.combine("folder") - hasher.combine(folderName) - } - } } diff --git a/Account/Sources/Account/FeedProvider/Twitter/TwitterStatus.swift b/Account/Sources/Account/FeedProvider/Twitter/TwitterStatus.swift index 047b6a422..fa89f005c 100644 --- a/Account/Sources/Account/FeedProvider/Twitter/TwitterStatus.swift +++ b/Account/Sources/Account/FeedProvider/Twitter/TwitterStatus.swift @@ -91,8 +91,8 @@ private extension TwitterStatus { } } - let offsetStartIndex = entity.startIndex - unicodeScalarOffset - let offsetEndIndex = entity.endIndex - unicodeScalarOffset + let offsetStartIndex = unicodeScalarOffset < entity.startIndex ? entity.startIndex - unicodeScalarOffset : entity.startIndex + let offsetEndIndex = unicodeScalarOffset < entity.endIndex ? entity.endIndex - unicodeScalarOffset : entity.endIndex let entityStartIndex = text.index(text.startIndex, offsetBy: offsetStartIndex, limitedBy: text.endIndex) ?? text.startIndex let entityEndIndex = text.index(text.startIndex, offsetBy: offsetEndIndex, limitedBy: text.endIndex) ?? text.endIndex @@ -115,7 +115,7 @@ private extension TwitterStatus { } if prevIndex < displayEndIndex { - html += String(text[prevIndex..") } return html diff --git a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index 812085fbe..58422a96f 100644 --- a/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Account/Sources/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -136,8 +136,35 @@ final class ReaderAPIAccountDelegate: AccountDelegate { case .failure(let error): DispatchQueue.main.async { self.refreshProgress.clear() + let wrappedError = AccountError.wrappedError(error: error, account: account) - completion(.failure(wrappedError)) + if wrappedError.isCredentialsError, let basicCredentials = try? account.retrieveCredentials(type: .readerBasic), let endpoint = account.endpointURL { + self.caller.credentials = basicCredentials + + self.caller.validateCredentials(endpoint: endpoint) { result in + switch result { + case .success(let apiCredentials): + if let apiCredentials = apiCredentials { + DispatchQueue.main.async { + try? account.storeCredentials(apiCredentials) + self.caller.credentials = apiCredentials + self.refreshAll(for: account, completion: completion) + } + } else { + DispatchQueue.main.async { + completion(.failure(wrappedError)) + } + } + case .failure: + DispatchQueue.main.async { + completion(.failure(wrappedError)) + } + } + } + + } else { + completion(.failure(wrappedError)) + } } } diff --git a/Appcasts/netnewswire-beta.xml b/Appcasts/netnewswire-beta.xml index 60494ecba..3863623c9 100755 --- a/Appcasts/netnewswire-beta.xml +++ b/Appcasts/netnewswire-beta.xml @@ -6,7 +6,38 @@ Most recent NetNewsWire changes with links to updates. en + + NetNewsWire 6.0.3 + Same as 6.0.3b2 except for the version number.

+ ]]>
+ Sun, 05 Sep 2021 12:20:00 -0700 + + 10.15.0 +
+ + + NetNewsWire 6.0.3b2 + Feedly: preserve custom feed names with Feedly when moving them between folders

+

Preferences: use full-width row style in accounts and extensions panes

+

Fixed a crashing bug triggered by running some UI code outside of main thread

+

Fixed a crashing bug that could happen when the app tries to find a feed for a website

+

Fixed a crashing bug that could happen when rendering tweets

+

Changed how images are placed in Twitter articles so that you can better see who Tweeted the image

+

Fixed bug where iCloud syncing could stop prematurely when the sync database has records not in the local database

+

Fixed bug where favicons wouldn’t be found when a home page URL has non-ASCII characters

+

Fixed bug where external URLs in Feedbin feeds might be lost

+

Fixed bug where words prepended with $ wouldn’t appear in Twitter feeds

+

Fixed bug where newlines would be just a space in Twitter feeds

+

Fixed bug where BazQux-synced feeds might stop updating

+ ]]>
+ Sun, 29 Aug 2021 15:25:00 -0700 + + 10.15.0 +
+ NetNewsWire 6.0.3b1 Most recent NetNewsWire releases (not test builds). en - + + NetNewsWire 6.0.3 + Feedly: preserve custom feed names with Feedly when moving them between folders

+

Feedly: handle API change with deleting and don’t show a spurious error

+

NewsBlur: don’t fetch articles marked hidden by NewsBlur

+

FreshRSS: add API endpoint URL example in setup form

+

iCloud: fixed bug not retaining feeds in a folder where the folder hasn’t been synced yet

+

iCloud: fixed bug where iCloud syncing could stop prematurely when the sync database has records not in the local database

+

BazQux: fixed bug where BazQux-synced feeds might stop updating

+

Feedbin: fixed bug where external URLs in Feedbin feeds might be lost

+

Twitter extension: fixed weird bug where an extra https:/ could appear in tweet text

+

Preferences: use full-width row style in accounts and extensions panes

+

Fixed a crashing bug triggered by running some UI code outside of main thread

+

Fixed a crashing bug that could happen when the app tries to find a feed for a website

+

Fixed a crashing bug that could happen when rendering tweets

+

Changed how images are placed in Twitter articles so that you can better see who Tweeted the image

+

Fixed bug where favicons wouldn’t be found when a home page URL has non-ASCII characters

+

Fixed bug where words prepended with $ wouldn’t appear in Twitter feeds

+

Fixed bug where newlines would be just a space in Twitter feeds

+

Feeds list: smart feeds remain visible despite Hide Read Feeds setting

+

Keyboard shortcuts: fixed regression where L key wouldn’t go to next unread when feed is all read

+ ]]>
+ Sun, 05 Sep 2021 12:20:00 -0700 + + 10.15.0 +
+ + NetNewsWire 6.0.2 Inoreader sync: fixed (hopefully) cause of rate limit errors — now doing background sync of statuses much less often - note that this fix needs to be rolled out across all NetNewsWire users in order for it to have full effect

diff --git a/Articles/Sources/Articles/Article.swift b/Articles/Sources/Articles/Article.swift index 5884dd919..a979c7229 100644 --- a/Articles/Sources/Articles/Article.swift +++ b/Articles/Sources/Articles/Article.swift @@ -19,10 +19,10 @@ public struct Article: Hashable { public let title: String? public let contentHTML: String? public let contentText: String? - public let url: String? - public let externalURL: String? + public let rawLink: String? // We store raw source value, but use computed url or link other than where raw value required. + public let rawExternalLink: String? // We store raw source value, but use computed externalURL or externalLink other than where raw value required. public let summary: String? - public let imageURL: String? + public let rawImageLink: String? // We store raw source value, but use computed imageURL or imageLink other than where raw value required. public let datePublished: Date? public let dateModified: Date? public let authors: Set? @@ -35,10 +35,10 @@ public struct Article: Hashable { self.title = title self.contentHTML = contentHTML self.contentText = contentText - self.url = url - self.externalURL = externalURL + self.rawLink = url + self.rawExternalLink = externalURL self.summary = summary - self.imageURL = imageURL + self.rawImageLink = imageURL self.datePublished = datePublished self.dateModified = dateModified self.authors = authors @@ -65,7 +65,7 @@ public struct Article: Hashable { // MARK: - Equatable static public func ==(lhs: Article, rhs: Article) -> Bool { - return lhs.articleID == rhs.articleID && lhs.accountID == rhs.accountID && lhs.webFeedID == rhs.webFeedID && lhs.uniqueID == rhs.uniqueID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.contentText == rhs.contentText && lhs.url == rhs.url && lhs.externalURL == rhs.externalURL && lhs.summary == rhs.summary && lhs.imageURL == rhs.imageURL && lhs.datePublished == rhs.datePublished && lhs.dateModified == rhs.dateModified && lhs.authors == rhs.authors + return lhs.articleID == rhs.articleID && lhs.accountID == rhs.accountID && lhs.webFeedID == rhs.webFeedID && lhs.uniqueID == rhs.uniqueID && lhs.title == rhs.title && lhs.contentHTML == rhs.contentHTML && lhs.contentText == rhs.contentText && lhs.rawLink == rhs.rawLink && lhs.rawExternalLink == rhs.rawExternalLink && lhs.summary == rhs.summary && lhs.rawImageLink == rhs.rawImageLink && lhs.datePublished == rhs.datePublished && lhs.dateModified == rhs.dateModified && lhs.authors == rhs.authors } } diff --git a/ArticlesDatabase/Sources/ArticlesDatabase/Extensions/Article+Database.swift b/ArticlesDatabase/Sources/ArticlesDatabase/Extensions/Article+Database.swift index 31c2d3608..30ac2bfd3 100644 --- a/ArticlesDatabase/Sources/ArticlesDatabase/Extensions/Article+Database.swift +++ b/ArticlesDatabase/Sources/ArticlesDatabase/Extensions/Article+Database.swift @@ -71,7 +71,7 @@ extension Article { if authors.isEmpty { return self } - return Article(accountID: self.accountID, articleID: self.articleID, webFeedID: self.webFeedID, uniqueID: self.uniqueID, title: self.title, contentHTML: self.contentHTML, contentText: self.contentText, url: self.url, externalURL: self.externalURL, summary: self.summary, imageURL: self.imageURL, datePublished: self.datePublished, dateModified: self.dateModified, authors: authors, status: self.status) + return Article(accountID: self.accountID, articleID: self.articleID, webFeedID: self.webFeedID, uniqueID: self.uniqueID, title: self.title, contentHTML: self.contentHTML, contentText: self.contentText, url: self.rawLink, externalURL: self.rawExternalLink, summary: self.summary, imageURL: self.rawImageLink, datePublished: self.datePublished, dateModified: self.dateModified, authors: authors, status: self.status) } func changesFrom(_ existingArticle: Article) -> DatabaseDictionary? { @@ -87,10 +87,10 @@ extension Article { addPossibleStringChangeWithKeyPath(\Article.title, existingArticle, DatabaseKey.title, &d) addPossibleStringChangeWithKeyPath(\Article.contentHTML, existingArticle, DatabaseKey.contentHTML, &d) addPossibleStringChangeWithKeyPath(\Article.contentText, existingArticle, DatabaseKey.contentText, &d) - addPossibleStringChangeWithKeyPath(\Article.url, existingArticle, DatabaseKey.url, &d) - addPossibleStringChangeWithKeyPath(\Article.externalURL, existingArticle, DatabaseKey.externalURL, &d) + addPossibleStringChangeWithKeyPath(\Article.rawLink, existingArticle, DatabaseKey.url, &d) + addPossibleStringChangeWithKeyPath(\Article.rawExternalLink, existingArticle, DatabaseKey.externalURL, &d) addPossibleStringChangeWithKeyPath(\Article.summary, existingArticle, DatabaseKey.summary, &d) - addPossibleStringChangeWithKeyPath(\Article.imageURL, existingArticle, DatabaseKey.imageURL, &d) + addPossibleStringChangeWithKeyPath(\Article.rawImageLink, existingArticle, DatabaseKey.imageURL, &d) // If updated versions of dates are nil, and we have existing dates, keep the existing dates. // This is data that’s good to have, and it’s likely that a feed removing dates is doing so in error. @@ -154,17 +154,17 @@ extension Article: DatabaseObject { if let contentText = contentText { d[DatabaseKey.contentText] = contentText } - if let url = url { - d[DatabaseKey.url] = url + if let rawLink = rawLink { + d[DatabaseKey.url] = rawLink } - if let externalURL = externalURL { - d[DatabaseKey.externalURL] = externalURL + if let rawExternalLink = rawExternalLink { + d[DatabaseKey.externalURL] = rawExternalLink } if let summary = summary { d[DatabaseKey.summary] = summary } - if let imageURL = imageURL { - d[DatabaseKey.imageURL] = imageURL + if let rawImageLink = rawImageLink { + d[DatabaseKey.imageURL] = rawImageLink } if let datePublished = datePublished { d[DatabaseKey.datePublished] = datePublished diff --git a/Mac/AppAssets.swift b/Mac/AppAssets.swift index be4065a9f..74c69df40 100644 --- a/Mac/AppAssets.swift +++ b/Mac/AppAssets.swift @@ -69,6 +69,11 @@ struct AppAssets { return RSImage(named: "articleExtractorOn")! }() + @available(macOS 11.0, *) + static var articleTheme: RSImage = { + return NSImage(systemSymbolName: "doc.richtext", accessibilityDescription: nil)! + }() + @available(macOS 11.0, *) static var cleanUpImage: RSImage = { return NSImage(systemSymbolName: "wind", accessibilityDescription: nil)! diff --git a/Mac/AppDefaults.swift b/Mac/AppDefaults.swift index 3c0eaf09d..df9b7c618 100644 --- a/Mac/AppDefaults.swift +++ b/Mac/AppDefaults.swift @@ -16,6 +16,8 @@ enum FontSize: Int { final class AppDefaults { + static let defaultThemeName = "Default" + static var shared = AppDefaults() private init() {} @@ -39,6 +41,7 @@ final class AppDefaults { static let importOPMLAccountID = "importOPMLAccountID" static let exportOPMLAccountID = "exportOPMLAccountID" static let defaultBrowserID = "defaultBrowserID" + static let currentThemeName = "currentThemeName" // Hidden prefs static let showDebugMenu = "ShowDebugMenu" @@ -209,6 +212,15 @@ final class AppDefaults { } } + var currentThemeName: String? { + get { + return AppDefaults.string(for: Key.currentThemeName) + } + set { + AppDefaults.setString(for: Key.currentThemeName, newValue) + } + } + var showTitleOnMainWindow: Bool { return AppDefaults.bool(for: Key.showTitleOnMainWindow) } @@ -311,7 +323,8 @@ final class AppDefaults { Key.timelineGroupByFeed: false, "NSScrollViewShouldScrollUnderTitlebar": false, Key.refreshInterval: RefreshInterval.everyHour.rawValue, - Key.showDebugMenu: showDebugMenu] + Key.showDebugMenu: showDebugMenu, + Key.currentThemeName: Self.defaultThemeName] UserDefaults.standard.register(defaults: defaults) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index f55bd5d74..ad024b046 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -107,6 +107,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, private var softwareUpdater: SPUUpdater! private var crashReporter: PLCrashReporter! #endif + + private var themeImportPath: String? override init() { NSWindow.allowsAutomaticWindowTabbing = false @@ -120,10 +122,13 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, SecretsManager.provider = Secrets() AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!) + ArticleThemesManager.shared = ArticleThemesManager(folderPath: Platform.dataSubfolder(forApplication: nil, folderName: "Themes")!) FeedProviderManager.shared.delegate = ExtensionPointManager.shared NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(themeImportError(_:)), name: .didFailToImportThemeWithError, object: nil) NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil) appDelegate = self @@ -318,16 +323,24 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) } + func application(_ sender: NSApplication, openFile filename: String) -> Bool { + guard filename.hasSuffix(ArticleTheme.nnwThemeSuffix) else { return false } + importTheme(filename: filename) + return true + } + func applicationWillTerminate(_ notification: Notification) { shuttingDown = true saveState() + ArticleThemeDownloader.shared.cleanUp() + AccountManager.shared.sendArticleStatusAll() { self.isShutDownSyncDone = true } let timeout = Date().addingTimeInterval(2) - while !isShutDownSyncDone && RunLoop.current.run(mode: .default, before: .distantFuture) && timeout > Date() { } + while !isShutDownSyncDone && RunLoop.current.run(mode: .default, before: timeout) && timeout > Date() { } } // MARK: Notifications @@ -368,6 +381,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, @objc func didWakeNotification(_ note: Notification) { fireOldTimers() } + + @objc func importDownloadedTheme(_ note: Notification) { + guard let userInfo = note.userInfo, + let url = userInfo["url"] as? URL else { + return + } + DispatchQueue.main.async { + self.importTheme(filename: url.path) + } + } // MARK: Main Window @@ -758,7 +781,7 @@ extension AppDelegate { } -private extension AppDelegate { +internal extension AppDelegate { func fireOldTimers() { // It’s possible there’s a refresh timer set to go off in the past. @@ -768,7 +791,6 @@ private extension AppDelegate { } func objectsForInspector() -> [Any]? { - guard let window = NSApplication.shared.mainWindow, let windowController = window.windowController as? MainWindowController else { return nil } @@ -781,7 +803,6 @@ private extension AppDelegate { } func updateSortMenuItems() { - let sortByNewestOnTop = AppDefaults.shared.timelineSortDirection == .orderedDescending sortByNewestArticleOnTopMenuItem.state = sortByNewestOnTop ? .on : .off sortByOldestArticleOnTopMenuItem.state = sortByNewestOnTop ? .off : .on @@ -791,6 +812,169 @@ private extension AppDelegate { let groupByFeedEnabled = AppDefaults.shared.timelineGroupByFeed groupArticlesByFeedMenuItem.state = groupByFeedEnabled ? .on : .off } + + func importTheme(filename: String) { + guard let window = mainWindowController?.window else { return } + + do { + let theme = try ArticleTheme(path: filename) + let alert = NSAlert() + alert.alertStyle = .informational + + let localizedMessageText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text") + alert.messageText = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name, theme.creatorName) as String + + var attrs = [NSAttributedString.Key : Any]() + attrs[.font] = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) + attrs[.foregroundColor] = NSColor.textColor + + if #available(macOS 11.0, *) { + let titleParagraphStyle = NSMutableParagraphStyle() + titleParagraphStyle.alignment = .center + attrs[.paragraphStyle] = titleParagraphStyle + } + + let websiteText = NSMutableAttributedString() + websiteText.append(NSAttributedString(string: NSLocalizedString("Author's Website", comment: "Author's Website"), attributes: attrs)) + + if #available(macOS 11.0, *) { + websiteText.append(NSAttributedString(string: "\n")) + } else { + websiteText.append(NSAttributedString(string: " ")) + } + + attrs[.link] = theme.creatorHomePage + websiteText.append(NSAttributedString(string: theme.creatorHomePage, attributes: attrs)) + + let textViewWidth: CGFloat + if #available(macOS 11.0, *) { + textViewWidth = 200 + } else { + textViewWidth = 400 + } + + let textView = NSTextView(frame: CGRect(x: 0, y: 0, width: textViewWidth, height: 15)) + textView.isEditable = false + textView.drawsBackground = false + textView.textStorage?.setAttributedString(websiteText) + alert.accessoryView = textView + + alert.addButton(withTitle: NSLocalizedString("Install Theme", comment: "Install Theme")) + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme")) + + func importTheme() { + do { + try ArticleThemesManager.shared.importTheme(filename: filename) + confirmImportSuccess(themeName: theme.name) + } catch { + NSApplication.shared.presentError(error) + } + } + + alert.beginSheetModal(for: window) { result in + if result == NSApplication.ModalResponse.alertFirstButtonReturn { + + if ArticleThemesManager.shared.themeExists(filename: filename) { + let alert = NSAlert() + alert.alertStyle = .warning + + let localizedMessageText = NSLocalizedString("The theme “%@” already exists. Overwrite it?", comment: "Overwrite theme") + alert.messageText = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name) as String + + alert.addButton(withTitle: NSLocalizedString("Overwrite", comment: "Overwrite")) + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme")) + + alert.beginSheetModal(for: window) { result in + if result == NSApplication.ModalResponse.alertFirstButtonReturn { + importTheme() + } + } + } else { + importTheme() + } + } + } + } catch { + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error, "path": filename]) + } + } + + func confirmImportSuccess(themeName: String) { + guard let window = mainWindowController?.window else { return } + + let alert = NSAlert() + alert.alertStyle = .informational + alert.messageText = NSLocalizedString("Theme 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.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) + + alert.beginSheetModal(for: window) + } + + @objc func themeImportError(_ note: Notification) { + guard let userInfo = note.userInfo, + let error = userInfo["error"] as? Error else { + return + } + themeImportPath = userInfo["path"] as? String + var informativeText: String = "" + if let decodingError = error as? DecodingError { + switch decodingError { + case .typeMismatch(let type, _): + let localizedError = NSLocalizedString("This theme cannot be used because the the type—“%@”—is mismatched in the Info.plist", comment: "Type mismatch") + informativeText = NSString.localizedStringWithFormat(localizedError as NSString, type as! CVarArg) as String + case .valueNotFound(let value, _): + let localizedError = NSLocalizedString("This theme cannot be used because the the value—“%@”—is not found in the Info.plist.", comment: "Decoding value missing") + informativeText = NSString.localizedStringWithFormat(localizedError as NSString, value as! CVarArg) as String + case .keyNotFound(let codingKey, _): + let localizedError = NSLocalizedString("This theme cannot be used because the the key—“%@”—is not found in the Info.plist.", comment: "Decoding key missing") + informativeText = NSString.localizedStringWithFormat(localizedError as NSString, codingKey.stringValue) as String + case .dataCorrupted(let context): + guard let underlyingError = context.underlyingError as NSError?, + let debugDescription = underlyingError.userInfo["NSDebugDescription"] as? String else { + informativeText = error.localizedDescription + break + } + 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 + + default: + informativeText = error.localizedDescription + } + } else { + informativeText = error.localizedDescription + } + + DispatchQueue.main.async { + let alert = NSAlert() + alert.alertStyle = .warning + alert.messageText = NSLocalizedString("Theme Error", comment: "Theme download error") + alert.informativeText = informativeText + alert.addButton(withTitle: NSLocalizedString("Open Theme Folder", comment: "Open Theme Folder")) + alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) + + let button = alert.buttons.first + button?.target = self + button?.action = #selector(self.openThemesFolder(_:)) + alert.buttons[0].keyEquivalent = "\033" + alert.buttons[1].keyEquivalent = "\r" + alert.runModal() + } + } + + @objc func openThemesFolder(_ sender: Any) { + if themeImportPath == nil { + let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath) + NSWorkspace.shared.open(url) + } else { + let url = URL(fileURLWithPath: themeImportPath!) + NSWorkspace.shared.open(url.deletingLastPathComponent()) + } + } + } /* diff --git a/Mac/Base.lproj/Main.storyboard b/Mac/Base.lproj/Main.storyboard index a1f665aa2..d7e35cf17 100644 --- a/Mac/Base.lproj/Main.storyboard +++ b/Mac/Base.lproj/Main.storyboard @@ -1,8 +1,8 @@ - + - + @@ -69,24 +69,24 @@ - + - + - + - + @@ -157,6 +157,18 @@ + + + + + + + + + + + + @@ -183,7 +195,7 @@ - + diff --git a/Mac/Base.lproj/MainWindow.storyboard b/Mac/Base.lproj/MainWindow.storyboard index 1a054538f..6b01165e4 100644 --- a/Mac/Base.lproj/MainWindow.storyboard +++ b/Mac/Base.lproj/MainWindow.storyboard @@ -1,8 +1,8 @@ - + - + @@ -196,6 +196,24 @@ + + + + + + + + + + + + + + + + + + @@ -221,6 +239,7 @@ + diff --git a/Mac/Base.lproj/Preferences.storyboard b/Mac/Base.lproj/Preferences.storyboard index 99e55070e..bbe72eefa 100644 --- a/Mac/Base.lproj/Preferences.storyboard +++ b/Mac/Base.lproj/Preferences.storyboard @@ -1,8 +1,8 @@ - + - + @@ -31,15 +31,15 @@ - - + + - + - + @@ -47,7 +47,7 @@ - + @@ -75,11 +75,36 @@ + + + + + + + + + + + + + + + + - + - + @@ -87,7 +112,7 @@ - + @@ -102,7 +127,7 @@ - + @@ -130,10 +155,10 @@ - + - + @@ -141,7 +166,7 @@ - + @@ -176,7 +201,7 @@ - + @@ -184,7 +209,7 @@ + + + + + + + + - @@ -220,15 +252,20 @@ + + + + + @@ -238,16 +275,20 @@ + + + + @@ -259,13 +300,14 @@ + - + @@ -445,16 +487,16 @@ - + - + - + - - + + @@ -473,7 +515,7 @@ - + @@ -534,7 +576,7 @@ - + - + diff --git a/Mac/Browser.swift b/Mac/Browser.swift index 5f8b7a0ee..d574bc7ad 100644 --- a/Mac/Browser.swift +++ b/Mac/Browser.swift @@ -43,7 +43,17 @@ struct Browser { /// - Note: Some browsers (specifically Chromium-derived ones) will ignore the request /// to open in the background. static func open(_ urlString: String, inBackground: Bool) { - if let url = URL(unicodeString: urlString) { + guard let url = URL(unicodeString: urlString), let preparedURL = url.preparedForOpeningInBrowser() else { return } + + let configuration = NSWorkspace.OpenConfiguration() + configuration.requiresUniversalLinks = true + configuration.promptsUserIfNeeded = false + if inBackground { + configuration.activates = false + } + + NSWorkspace.shared.open(preparedURL, configuration: configuration) { (runningApplication, error) in + guard error != nil else { return } if let defaultBrowser = defaultBrowser { defaultBrowser.openURL(url, inBackground: inBackground) } else { diff --git a/Mac/MainWindow/Detail/DetailViewController.swift b/Mac/MainWindow/Detail/DetailViewController.swift index 250a04c3a..f8758a852 100644 --- a/Mac/MainWindow/Detail/DetailViewController.swift +++ b/Mac/MainWindow/Detail/DetailViewController.swift @@ -16,8 +16,8 @@ enum DetailState: Equatable { case noSelection case multipleSelection case loading - case article(Article) - case extracted(Article, ExtractedArticle) + case article(Article, CGFloat?) + case extracted(Article, ExtractedArticle, CGFloat?) } final class DetailViewController: NSViewController, WKUIDelegate { @@ -81,13 +81,18 @@ final class DetailViewController: NSViewController, WKUIDelegate { // MARK: - Navigation func focus() { - guard let window = currentWebViewController.webView.window else { return } window.makeFirstResponderUnlessDescendantIsFirstResponder(currentWebViewController.webView) } + // MARK: State Restoration + + func saveState(to state: inout [AnyHashable : Any]) { + currentWebViewController.saveState(to: &state) + } + } // MARK: - DetailWebViewControllerDelegate diff --git a/Mac/MainWindow/Detail/DetailWebViewController.swift b/Mac/MainWindow/Detail/DetailWebViewController.swift index e6ccb7b69..b62b41dac 100644 --- a/Mac/MainWindow/Detail/DetailWebViewController.swift +++ b/Mac/MainWindow/Detail/DetailWebViewController.swift @@ -24,6 +24,12 @@ final class DetailWebViewController: NSViewController { var state: DetailState = .noSelection { didSet { if state != oldValue { + switch state { + case .article(_, let scrollY), .extracted(_, _, let scrollY): + windowScrollY = scrollY + default: + break + } reloadHTML() } } @@ -31,9 +37,9 @@ final class DetailWebViewController: NSViewController { var article: Article? { switch state { - case .article(let article): + case .article(let article, _): return article - case .extracted(let article, _): + case .extracted(let article, _, _): return article default: return nil @@ -56,10 +62,21 @@ final class DetailWebViewController: NSViewController { private let detailIconSchemeHandler = DetailIconSchemeHandler() private var waitingForFirstReload = false private let keyboardDelegate = DetailKeyboardDelegate() - + private var windowScrollY: CGFloat? + + private var isShowingExtractedArticle: Bool { + switch state { + case .extracted(_, _, _): + return true + default: + return false + } + } + private struct MessageName { static let mouseDidEnter = "mouseDidEnter" static let mouseDidExit = "mouseDidExit" + static let windowDidScroll = "windowDidScroll" } override func loadView() { @@ -73,6 +90,7 @@ final class DetailWebViewController: NSViewController { configuration.setURLSchemeHandler(detailIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) let userContentController = WKUserContentController() + userContentController.add(self, name: MessageName.windowDidScroll) userContentController.add(self, name: MessageName.mouseDidEnter) userContentController.add(self, name: MessageName.mouseDidExit) configuration.userContentController = userContentController @@ -116,6 +134,7 @@ final class DetailWebViewController: NSViewController { NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(currentArticleThemeDidChangeNotification(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil) webView.loadFileURL(ArticleRenderer.blank.url, allowingReadAccessTo: ArticleRenderer.blank.baseURL) } @@ -136,11 +155,14 @@ final class DetailWebViewController: NSViewController { @objc func userDefaultsDidChange(_ note: Notification) { if articleTextSize != AppDefaults.shared.articleTextSize { - articleTextSize = AppDefaults.shared.articleTextSize - webView.evaluateJavaScript("updateTextSize(\"\(articleTextSize.cssClass)\");") + reloadHTMLMaintainingScrollPosition() } } + @objc func currentArticleThemeDidChangeNotification(_ note: Notification) { + reloadHTMLMaintainingScrollPosition() + } + // MARK: Media Functions func stopMediaPlayback() { @@ -168,6 +190,14 @@ final class DetailWebViewController: NSViewController { override func scrollPageUp(_ sender: Any?) { webView.scrollPageUp(sender) } + + // MARK: State Restoration + + func saveState(to state: inout [AnyHashable : Any]) { + state[UserInfoKey.isShowingExtractedArticle] = isShowingExtractedArticle + state[UserInfoKey.articleWindowScrollY] = windowScrollY + } + } // MARK: - WKScriptMessageHandler @@ -175,10 +205,11 @@ final class DetailWebViewController: NSViewController { extension DetailWebViewController: WKScriptMessageHandler { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - if message.name == MessageName.mouseDidEnter, let link = message.body as? String { + if message.name == MessageName.windowDidScroll { + windowScrollY = message.body as? CGFloat + } else if message.name == MessageName.mouseDidEnter, let link = message.body as? String { delegate?.mouseDidEnter(self, link: link) - } - else if message.name == MessageName.mouseDidExit { + } else if message.name == MessageName.mouseDidExit { delegate?.mouseDidExit(self) } } @@ -220,6 +251,11 @@ extension DetailWebViewController: WKNavigationDelegate, WKUIDelegate { DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { webView.isHidden = false } + } else { + if let windowScrollY = windowScrollY { + webView.evaluateJavaScript("window.scrollTo(0, \(windowScrollY));") + self.windowScrollY = nil + } } } @@ -253,26 +289,33 @@ private extension DetailWebViewController { webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")") } } + + func reloadHTMLMaintainingScrollPosition() { + fetchScrollInfo() { scrollInfo in + self.windowScrollY = scrollInfo?.offsetY + self.reloadHTML() + } + } func reloadHTML() { delegate?.mouseDidExit(self) - let style = ArticleStylesManager.shared.currentStyle + let theme = ArticleThemesManager.shared.currentTheme let rendering: ArticleRenderer.Rendering switch state { case .noSelection: - rendering = ArticleRenderer.noSelectionHTML(style: style) + rendering = ArticleRenderer.noSelectionHTML(theme: theme) case .multipleSelection: - rendering = ArticleRenderer.multipleSelectionHTML(style: style) + rendering = ArticleRenderer.multipleSelectionHTML(theme: theme) case .loading: - rendering = ArticleRenderer.loadingHTML(style: style) - case .article(let article): + rendering = ArticleRenderer.loadingHTML(theme: theme) + case .article(let article, _): detailIconSchemeHandler.currentArticle = article - rendering = ArticleRenderer.articleHTML(article: article, style: style) - case .extracted(let article, let extractedArticle): + rendering = ArticleRenderer.articleHTML(article: article, theme: theme) + case .extracted(let article, let extractedArticle, _): detailIconSchemeHandler.currentArticle = article - rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style) + rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, theme: theme) } let substitutions = [ diff --git a/Mac/MainWindow/Detail/main_mac.js b/Mac/MainWindow/Detail/main_mac.js index 90329f7b4..45995d080 100644 --- a/Mac/MainWindow/Detail/main_mac.js +++ b/Mac/MainWindow/Detail/main_mac.js @@ -1,4 +1,9 @@ -// Add the mouse listeners for the above functions +function scrollDetection() { + window.onscroll = function(event) { + window.webkit.messageHandlers.windowDidScroll.postMessage(window.scrollY); + } +} + function linkHover() { window.onmouseover = function(event) { var closestAnchor = event.target.closest('a') @@ -15,5 +20,6 @@ function linkHover() { } function postRenderProcessing() { - linkHover() + scrollDetection(); + linkHover(); } diff --git a/Mac/MainWindow/Detail/styleSheet.css b/Mac/MainWindow/Detail/styleSheet.css deleted file mode 100644 index fe3d5242a..000000000 --- a/Mac/MainWindow/Detail/styleSheet.css +++ /dev/null @@ -1,54 +0,0 @@ -body { - margin-top: 20px; - margin-bottom: 64px; - padding-left: 48px; - padding-right: 48px; - font-family: -apple-system; -} - -.smallText { - font-size: 14px; -} - -.mediumText { - font-size: 16px; -} - -.largeText { - font-size: 18px; -} - -.xlargeText { - font-size: 20px; -} - -.xxlargeText { - font-size: 22px; -} - -:root { - color-scheme: light dark; - --accent-color: rgba(8, 106, 238, 1); - --block-quote-border-color: rgba(8, 106, 238, .50); -} - -@media(prefers-color-scheme: dark) { - :root { - --accent-color: rgba(94, 158, 244, 1); - --block-quote-border-color: rgba(94, 158, 244, .50); - --header-table-border-color: rgba(255, 255, 255, 0.1); - } -} - -body a, body a:visited { - color: var(--accent-color); -} - -pre { - border: 1px solid var(--accent-color); - padding: 10px; -} - -.nnw-overflow table { - border: 1px solid var(--accent-color); -} diff --git a/Mac/MainWindow/MainWindowController.swift b/Mac/MainWindow/MainWindowController.swift index fe988889e..87c3bd6fc 100644 --- a/Mac/MainWindow/MainWindowController.swift +++ b/Mac/MainWindow/MainWindowController.swift @@ -18,7 +18,9 @@ enum TimelineSourceMode { class MainWindowController : NSWindowController, NSUserInterfaceValidations { - private var activityManager = ActivityManager() + @IBOutlet weak var articleThemePopUpButton: NSPopUpButton? + + private var activityManager = ActivityManager() private var isShowingExtractedArticle = false private var articleExtractor: ArticleExtractor? = nil @@ -44,6 +46,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { private var timelineContainerViewController: TimelineContainerViewController? private var detailViewController: DetailViewController? private var currentSearchField: NSSearchField? = nil + private let articleThemeMenuToolbarItem = NSMenuToolbarItem(itemIdentifier: .articleThemeMenu) private var searchString: String? = nil private var lastSentSearchString: String? = nil private var timelineSourceMode: TimelineSourceMode = .regular { @@ -53,6 +56,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } } private var searchSmartFeed: SmartFeed? = nil + private var restoreArticleWindowScrollY: CGFloat? // MARK: - NSWindowController @@ -61,6 +65,8 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { sharingServicePickerDelegate = SharingServicePickerDelegate(self.window) + updateArticleThemeMenu() + if #available(macOS 11.0, *) { let toolbar = NSToolbar(identifier: "MainWindowToolbar") toolbar.allowsUserCustomization = true @@ -98,6 +104,9 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { 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(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(currentArticleThemeDidChangeNotification(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil) + DispatchQueue.main.async { self.updateWindowTitle() } @@ -150,6 +159,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { updateWindowTitleIfNecessary(note.object) } + @objc func articleThemeNamesDidChangeNotification(_ note: Notification) { + updateArticleThemeMenu() + } + + @objc func currentArticleThemeDidChangeNotification(_ note: Notification) { + updateArticleThemeMenu() + } + private func updateWindowTitleIfNecessary(_ noteObject: Any?) { if let folder = currentFeedOrFolder as? Folder, let noteObject = noteObject as? Folder { @@ -188,6 +205,14 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { public func validateUserInterfaceItem(_ item: NSValidatedUserInterfaceItem) -> Bool { + if item.action == #selector(copyArticleURL(_:)) { + return canCopyArticleURL() + } + + if item.action == #selector(copyExternalURL(_:)) { + return canCopyExternalURL() + } + if item.action == #selector(openArticleInBrowser(_:)) { if let item = item as? NSMenuItem, item.keyEquivalentModifierMask.contains(.shift) { item.title = Browser.titleForOpenInBrowserInverted @@ -286,6 +311,18 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { } + @IBAction func copyArticleURL(_ sender: Any?) { + if let link = oneSelectedArticle?.preferredURL?.absoluteString { + URLPasteboardWriter.write(urlString: link, to: .general) + } + } + + @IBAction func copyExternalURL(_ sender: Any?) { + if let link = oneSelectedArticle?.externalLink { + URLPasteboardWriter.write(urlString: link, to: .general) + } + } + @IBAction func openArticleInBrowser(_ sender: Any?) { if let link = currentLink { Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false) @@ -374,20 +411,20 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { articleExtractor?.cancel() articleExtractor = nil isShowingExtractedArticle = false - detailViewController?.setState(DetailState.article(article), mode: timelineSourceMode) + detailViewController?.setState(DetailState.article(article, nil), mode: timelineSourceMode) return } guard !isShowingExtractedArticle else { isShowingExtractedArticle = false - detailViewController?.setState(DetailState.article(article), mode: timelineSourceMode) + detailViewController?.setState(DetailState.article(article, nil), mode: timelineSourceMode) return } if let articleExtractor = articleExtractor, let extractedArticle = articleExtractor.article { if currentLink == articleExtractor.articleLink { isShowingExtractedArticle = true - let detailState = DetailState.extracted(article, extractedArticle) + let detailState = DetailState.extracted(article, extractedArticle, nil) detailViewController?.setState(detailState, mode: timelineSourceMode) } } else { @@ -506,6 +543,10 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations { timelineContainerViewController?.toggleReadFilter() } + @objc func selectArticleTheme(_ menuItem: NSMenuItem) { + ArticleThemesManager.shared.currentThemeName = menuItem.title + } + } // MARK: NSWindowDelegate @@ -579,7 +620,8 @@ extension MainWindowController: TimelineContainerViewControllerDelegate { detailState = .loading startArticleExtractorForCurrentLink() } else { - detailState = .article(articles.first!) + detailState = .article(articles.first!, restoreArticleWindowScrollY) + restoreArticleWindowScrollY = nil } } else { detailState = .multipleSelection @@ -679,7 +721,8 @@ extension MainWindowController: ArticleExtractorDelegate { func articleExtractionDidComplete(extractedArticle: ExtractedArticle) { if let article = oneSelectedArticle, articleExtractor?.state != .cancelled { isShowingExtractedArticle = true - let detailState = DetailState.extracted(article, extractedArticle) + let detailState = DetailState.extracted(article, extractedArticle, restoreArticleWindowScrollY) + restoreArticleWindowScrollY = nil detailViewController?.setState(detailState, mode: timelineSourceMode) makeToolbarValidate() } @@ -726,6 +769,7 @@ extension NSToolbarItem.Identifier { static let readerView = NSToolbarItem.Identifier("readerView") static let openInBrowser = NSToolbarItem.Identifier("openInBrowser") static let share = NSToolbarItem.Identifier("share") + static let articleThemeMenu = NSToolbarItem.Identifier("articleThemeMenu") static let cleanUp = NSToolbarItem.Identifier("cleanUp") } @@ -795,6 +839,13 @@ extension MainWindowController: NSToolbarDelegate { let title = NSLocalizedString("Open in Browser", comment: "Open in Browser") return buildToolbarButton(.openInBrowser, title, AppAssets.openInBrowserImage, "openArticleInBrowser:") + case .articleThemeMenu: + articleThemeMenuToolbarItem.image = AppAssets.articleTheme + let description = NSLocalizedString("Article Theme", comment: "Article Theme") + articleThemeMenuToolbarItem.toolTip = description + articleThemeMenuToolbarItem.label = description + return articleThemeMenuToolbarItem + case .search: let toolbarItem = NSSearchToolbarItem(itemIdentifier: .search) let description = NSLocalizedString("Search", comment: "Search") @@ -832,6 +883,7 @@ extension MainWindowController: NSToolbarDelegate { .readerView, .openInBrowser, .share, + .articleThemeMenu, .search, .cleanUp ] @@ -994,6 +1046,7 @@ private extension MainWindowController { saveSplitViewState(to: &state) sidebarViewController?.saveState(to: &state) timelineContainerViewController?.saveState(to: &state) + detailViewController?.saveState(to: &state) return state } @@ -1002,11 +1055,30 @@ private extension MainWindowController { window?.toggleFullScreen(self) } restoreSplitViewState(from: state) + sidebarViewController?.restoreState(from: state) + + let articleWindowScrollY = state[UserInfoKey.articleWindowScrollY] as? CGFloat + restoreArticleWindowScrollY = articleWindowScrollY timelineContainerViewController?.restoreState(from: state) + + let isShowingExtractedArticle = state[UserInfoKey.isShowingExtractedArticle] as? Bool ?? false + if isShowingExtractedArticle { + restoreArticleWindowScrollY = articleWindowScrollY + startArticleExtractorForCurrentLink() + } + } // MARK: - Command Validation + + func canCopyArticleURL() -> Bool { + return currentLink != nil + } + + func canCopyExternalURL() -> Bool { + return oneSelectedArticle?.externalLink != nil && oneSelectedArticle?.externalLink != currentLink + } func canGoToNextUnread(wrappingToTop wrapping: Bool = false) -> Bool { @@ -1365,27 +1437,50 @@ private extension MainWindowController { let menu = NSMenu() let newWebFeedItem = NSMenuItem() - newWebFeedItem.title = NSLocalizedString("New Web Feed", comment: "New Web Feed") + newWebFeedItem.title = NSLocalizedString("New Web Feed…", comment: "New Web Feed") newWebFeedItem.action = Selector(("showAddWebFeedWindow:")) menu.addItem(newWebFeedItem) let newRedditFeedItem = NSMenuItem() - newRedditFeedItem.title = NSLocalizedString("New Reddit Feed", comment: "New Reddit Feed") + newRedditFeedItem.title = NSLocalizedString("New Reddit Feed…", comment: "New Reddit Feed") newRedditFeedItem.action = Selector(("showAddRedditFeedWindow:")) menu.addItem(newRedditFeedItem) let newTwitterFeedItem = NSMenuItem() - newTwitterFeedItem.title = NSLocalizedString("New Twitter Feed", comment: "New Twitter Feed") + newTwitterFeedItem.title = NSLocalizedString("New Twitter Feed…", comment: "New Twitter Feed") newTwitterFeedItem.action = Selector(("showAddTwitterFeedWindow:")) menu.addItem(newTwitterFeedItem) let newFolderFeedItem = NSMenuItem() - newFolderFeedItem.title = NSLocalizedString("New Folder", comment: "New Folder") + newFolderFeedItem.title = NSLocalizedString("New Folder…", comment: "New Folder") newFolderFeedItem.action = Selector(("showAddFolderWindow:")) menu.addItem(newFolderFeedItem) return menu } + func updateArticleThemeMenu() { + let articleThemeMenu = NSMenu() + + let defaultThemeItem = NSMenuItem() + defaultThemeItem.title = ArticleTheme.defaultTheme.name + defaultThemeItem.action = #selector(selectArticleTheme(_:)) + defaultThemeItem.state = defaultThemeItem.title == ArticleThemesManager.shared.currentThemeName ? .on : .off + articleThemeMenu.addItem(defaultThemeItem) + + articleThemeMenu.addItem(NSMenuItem.separator()) + + for themeName in ArticleThemesManager.shared.themeNames { + let themeItem = NSMenuItem() + themeItem.title = themeName + themeItem.action = #selector(selectArticleTheme(_:)) + themeItem.state = themeItem.title == ArticleThemesManager.shared.currentThemeName ? .on : .off + articleThemeMenu.addItem(themeItem) + } + + articleThemeMenuToolbarItem.menu = articleThemeMenu + articleThemePopUpButton?.menu = articleThemeMenu + } + } diff --git a/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift b/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift index 908eed74b..4dd5b80d5 100644 --- a/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift +++ b/Mac/MainWindow/Timeline/ArticlePasteboardWriter.swift @@ -25,7 +25,7 @@ extension Article: PasteboardWriterOwner { static let articleUTIInternalType = NSPasteboard.PasteboardType(rawValue: articleUTIInternal) private lazy var renderedHTML: String = { - let rendering = ArticleRenderer.articleHTML(article: article, style: ArticleStylesManager.shared.currentStyle) + let rendering = ArticleRenderer.articleHTML(article: article, theme: ArticleThemesManager.shared.currentTheme) return rendering.html }() @@ -87,11 +87,11 @@ private extension ArticlePasteboardWriter { s += "\(convertedHTML)\n\n" } - if let url = article.url { - s += "URL: \(url)\n\n" + if let link = article.link { + s += "URL: \(link)\n\n" } - if let externalURL = article.externalURL { - s += "external URL: \(externalURL)\n\n" + if let externalLink = article.externalLink { + s += "external URL: \(externalLink)\n\n" } s += "Date: \(article.logicalDatePublished)\n\n" @@ -151,10 +151,10 @@ private extension ArticlePasteboardWriter { d[Key.title] = article.title ?? nil d[Key.contentHTML] = article.contentHTML ?? nil d[Key.contentText] = article.contentText ?? nil - d[Key.url] = article.url ?? nil - d[Key.externalURL] = article.externalURL ?? nil + d[Key.url] = article.rawLink ?? nil + d[Key.externalURL] = article.rawExternalLink ?? nil d[Key.summary] = article.summary ?? nil - d[Key.imageURL] = article.imageURL ?? nil + d[Key.imageURL] = article.rawImageLink ?? nil d[Key.datePublished] = article.datePublished ?? nil d[Key.dateModified] = article.dateModified ?? nil d[Key.dateArrived] = article.status.dateArrived diff --git a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift index 1df39dca4..dd83afdcb 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController+ContextualMenus.swift @@ -90,6 +90,13 @@ extension TimelineViewController { } Browser.open(urlString, inBackground: false) } + + @objc func copyURLFromContextualMenu(_ sender: Any?) { + guard let menuItem = sender as? NSMenuItem, let urlString = menuItem.representedObject as? String else { + return + } + URLPasteboardWriter.write(urlString: urlString, to: .general) + } @objc func performShareServiceFromContextualMenu(_ sender: Any?) { guard let menuItem = sender as? NSMenuItem, let sharingCommandInfo = menuItem.representedObject as? SharingCommandInfo else { @@ -168,6 +175,12 @@ private extension TimelineViewController { if articles.count == 1, let link = articles.first!.preferredLink { menu.addSeparatorIfNeeded() menu.addItem(openInBrowserMenuItem(link)) + menu.addSeparatorIfNeeded() + menu.addItem(copyArticleURLMenuItem(link)) + + if let externalLink = articles.first?.externalLink, externalLink != link { + menu.addItem(copyExternalURLMenuItem(externalLink)) + } } if let sharingMenu = shareMenu(for: articles) { @@ -260,6 +273,15 @@ private extension TimelineViewController { return menuItem(NSLocalizedString("Open in Browser", comment: "Command"), #selector(openInBrowserFromContextualMenu(_:)), urlString) } + + func copyArticleURLMenuItem(_ urlString: String) -> NSMenuItem { + return menuItem(NSLocalizedString("Copy Article URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString) + } + + func copyExternalURLMenuItem(_ urlString: String) -> NSMenuItem { + return menuItem(NSLocalizedString("Copy External URL", comment: "Command"), #selector(copyURLFromContextualMenu(_:)), urlString) + } + func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem { diff --git a/Mac/Preferences/General/GeneralPrefencesViewController.swift b/Mac/Preferences/General/GeneralPrefencesViewController.swift index 08de5b27d..19fa9b7a0 100644 --- a/Mac/Preferences/General/GeneralPrefencesViewController.swift +++ b/Mac/Preferences/General/GeneralPrefencesViewController.swift @@ -15,7 +15,8 @@ final class GeneralPreferencesViewController: NSViewController { private var userNotificationSettings: UNNotificationSettings? - @IBOutlet var defaultBrowserPopup: NSPopUpButton! + @IBOutlet weak var articleThemePopup: NSPopUpButton! + @IBOutlet weak var defaultBrowserPopup: NSPopUpButton! public override init(nibName nibNameOrNil: NSNib.Name?, bundle nibBundleOrNil: Bundle?) { super.init(nibName: nibNameOrNil, bundle: nibBundleOrNil) @@ -39,15 +40,32 @@ final class GeneralPreferencesViewController: NSViewController { updateUI() } + @objc func articleThemeNamesDidChangeNotification(_ note: Notification) { + updateArticleThemePopup() + } + // MARK: - Actions + @IBAction func showThemesFolder(_ sender: Any) { + let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath) + NSWorkspace.shared.open(url) + } + + @IBAction func articleThemePopUpDidChange(_ sender: Any) { + guard let menuItem = articleThemePopup.selectedItem else { + return + } + ArticleThemesManager.shared.currentThemeName = menuItem.title + updateArticleThemePopup() + } + @IBAction func browserPopUpDidChangeValue(_ sender: Any?) { guard let menuItem = defaultBrowserPopup.selectedItem else { return } let bundleID = menuItem.representedObject as? String AppDefaults.shared.defaultBrowserID = bundleID - updateUI() + updateBrowserPopup() } } @@ -58,15 +76,29 @@ private extension GeneralPreferencesViewController { func commonInit() { NotificationCenter.default.addObserver(self, selector: #selector(applicationWillBecomeActive(_:)), name: NSApplication.willBecomeActiveNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil) } func updateUI() { + updateArticleThemePopup() updateBrowserPopup() } + + func updateArticleThemePopup() { + let menu = articleThemePopup.menu! + menu.removeAllItems() + + menu.addItem(NSMenuItem(title: ArticleTheme.defaultTheme.name, action: nil, keyEquivalent: "")) + menu.addItem(NSMenuItem.separator()) - func registerAppWithBundleID(_ bundleID: String) { - NSWorkspace.shared.setDefaultAppBundleID(forURLScheme: "feed", to: bundleID) - NSWorkspace.shared.setDefaultAppBundleID(forURLScheme: "feeds", to: bundleID) + for themeName in ArticleThemesManager.shared.themeNames { + menu.addItem(NSMenuItem(title: themeName, action: nil, keyEquivalent: "")) + } + + articleThemePopup.selectItem(withTitle: ArticleThemesManager.shared.currentThemeName) + if articleThemePopup.indexOfSelectedItem == -1 { + articleThemePopup.selectItem(withTitle: ArticleTheme.defaultTheme.name) + } } func updateBrowserPopup() { diff --git a/Mac/Resources/Credits.rtf b/Mac/Resources/Credits.rtf index 60ff2af48..08ee6ff19 100644 --- a/Mac/Resources/Credits.rtf +++ b/Mac/Resources/Credits.rtf @@ -25,7 +25,7 @@ NewsBlur syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/quanganhdo"} Under-the-hood magic and CSS stylin\'92s: {\field{\*\fldinst{HYPERLINK "https://github.com/wevah"}}{\fldrslt Nate Weaver}}\ Newsfoot (JS footnote displayer): {\field{\*\fldinst{HYPERLINK "https://github.com/brehaut/"}}{\fldrslt Andrew Brehaut}}\ Help book: {\field{\*\fldinst{HYPERLINK "https://nostodnayr.net/"}}{\fldrslt Ryan Dotson}}\ -And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://github.com/danielpunkass"}}{\fldrslt Daniel Jalkut}}, {\field{\*\fldinst{HYPERLINK "https://rhonabwy.com/"}}{\fldrslt Joe Heck}}, {\field{\*\fldinst{HYPERLINK "https://github.com/olofhellman"}}{\fldrslt Olof Hellman}}, {\field{\*\fldinst{HYPERLINK "https://blog.rizwan.dev/"}}{\fldrslt Rizwan Mohamed Ibrahim}}, {\field{\*\fldinst{HYPERLINK "https://stuartbreckenridge.com/"}}{\fldrslt Stuart Breckenridge}}, {\field{\*\fldinst{HYPERLINK "https://twitter.com/philviso"}}{\fldrslt Phil Viso}}, and {\field{\*\fldinst{HYPERLINK "https://github.com/Ranchero-Software/NetNewsWire/graphs/contributors"}}{\fldrslt many more}}!\ +And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://github.com/danielpunkass"}}{\fldrslt Daniel Jalkut}}, {\field{\*\fldinst{HYPERLINK "https://rhonabwy.com/"}}{\fldrslt Joe Heck}}, {\field{\*\fldinst{HYPERLINK "https://github.com/olofhellman"}}{\fldrslt Olof Hellman}}, {\field{\*\fldinst{HYPERLINK "https://blog.rizwan.dev/"}}{\fldrslt Rizwan Mohamed Ibrahim}}, {\field{\*\fldinst{HYPERLINK "https://mynameisstuart.com/"}}{\fldrslt Stuart Breckenridge}}, {\field{\*\fldinst{HYPERLINK "https://twitter.com/philviso"}}{\fldrslt Phil Viso}}, and {\field{\*\fldinst{HYPERLINK "https://github.com/Ranchero-Software/NetNewsWire/graphs/contributors"}}{\fldrslt many more}}!\ \ \pard\pardeftab720\sa60\partightenfactor0 @@ -49,4 +49,4 @@ And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://github.co \pard\pardeftab720\li360\sa60\partightenfactor0 \f1\b0 \cf2 NetNewsWire 6 is dedicated to everyone working to save democracy around the world.\ -} \ No newline at end of file +} diff --git a/Mac/Resources/Info.plist b/Mac/Resources/Info.plist index e6a304af3..be2d2bf9e 100644 --- a/Mac/Resources/Info.plist +++ b/Mac/Resources/Info.plist @@ -31,6 +31,7 @@ RSS Feed CFBundleURLSchemes + netnewswire feed feeds x-netnewswire-feed @@ -74,5 +75,46 @@ https://ranchero.com/downloads/netnewswire-release.xml UserAgent NetNewsWire (RSS Reader; https://netnewswire.com/) + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + nnwtheme + + CFBundleTypeName + NetNewsWire Theme + CFBundleTypeRole + Viewer + LSItemContentTypes + + com.ranchero.netnewswire.theme + + LSTypeIsPackage + + + + UTImportedTypeDeclarations + + + UTTypeConformsTo + + com.apple.package + + UTTypeDescription + NetNewsWire Theme + UTTypeIconFiles + + UTTypeIdentifier + com.ranchero.netnewswire.theme + UTTypeTagSpecification + + public.filename-extension + + nnwtheme + + + + diff --git a/Mac/Scriptability/AppDelegate+Scriptability.swift b/Mac/Scriptability/AppDelegate+Scriptability.swift index 2b6b6f5db..132fe26be 100644 --- a/Mac/Scriptability/AppDelegate+Scriptability.swift +++ b/Mac/Scriptability/AppDelegate+Scriptability.swift @@ -18,6 +18,7 @@ import Foundation import Articles +import Zip protocol AppDelegateAppleEvents { func installAppleEventHandlers() @@ -44,6 +45,34 @@ extension AppDelegate : AppDelegateAppleEvents { return } + // Handle themes + if urlString.hasPrefix("netnewswire://theme") { + guard let comps = URLComponents(string: urlString), + let queryItems = comps.queryItems, + let themeURLString = queryItems.first(where: { $0.name == "url" })?.value else { + return + } + + if let themeURL = URL(string: themeURLString) { + let request = URLRequest(url: themeURL) + let task = URLSession.shared.downloadTask(with: request) { location, response, error in + guard let location = location else { + return + } + + do { + try ArticleThemeDownloader.shared.handleFile(at: location) + } catch { + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) + } + } + task.resume() + } + return + + } + + // 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 let nnwScheme = "x-netnewswire-feed:" diff --git a/Mac/Scriptability/Article+Scriptability.swift b/Mac/Scriptability/Article+Scriptability.swift index 053afc316..6189d35c1 100644 --- a/Mac/Scriptability/Article+Scriptability.swift +++ b/Mac/Scriptability/Article+Scriptability.swift @@ -57,17 +57,17 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta @objc(url) var url:String? { - return article.url ?? article.externalURL + return article.preferredLink } @objc(permalink) var permalink:String? { - return article.url + return article.link } @objc(externalUrl) var externalUrl:String? { - return article.externalURL + return article.externalLink } @objc(title) @@ -132,7 +132,7 @@ class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectConta @objc(imageURL) var imageURL:String { - return article.imageURL ?? "" + return article.imageLink ?? "" } @objc(authors) diff --git a/Multiplatform/Shared/Account Management/FixAccountCredentialView.swift b/Multiplatform/Shared/Account Management/FixAccountCredentialView.swift deleted file mode 100644 index ecb585d70..000000000 --- a/Multiplatform/Shared/Account Management/FixAccountCredentialView.swift +++ /dev/null @@ -1,167 +0,0 @@ -// -// FixAccountCredentialView.swift -// NetNewsWire -// -// Created by Stuart Breckenridge on 24/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct FixAccountCredentialView: View { - - let accountSyncError: AccountSyncError - @Environment(\.presentationMode) var presentationMode - @StateObject private var editModel = EditAccountCredentialsModel() - - - var body: some View { - #if os(macOS) - MacForm - .onAppear { - editModel.retrieveCredentials(accountSyncError.account) - } - .onChange(of: editModel.accountCredentialsWereUpdated) { value in - if value == true { - presentationMode.wrappedValue.dismiss() - } - } - .alert(isPresented: $editModel.showError) { - Alert(title: Text("Error Adding Account"), - message: Text(editModel.error.description), - dismissButton: .default(Text("Dismiss"), - action: { - editModel.error = .none - })) - } - .frame(idealWidth: 300, idealHeight: 200, alignment: .top) - .padding() - #else - iOSForm - .onAppear { - editModel.retrieveCredentials(accountSyncError.account) - } - .onChange(of: editModel.accountCredentialsWereUpdated) { value in - if value == true { - presentationMode.wrappedValue.dismiss() - } - } - .alert(isPresented: $editModel.showError) { - Alert(title: Text("Error Adding Account"), - message: Text(editModel.error.description), - dismissButton: .default(Text("Dismiss"), - action: { - editModel.error = .none - })) - } - #endif - - - } - - var MacForm: some View { - Form { - header - HStack(alignment: .center) { - VStack(alignment: .trailing, spacing: 12) { - Text("Username: ") - Text("Password: ") - if accountSyncError.account.type == .freshRSS { - Text("API URL: ") - } - }.frame(width: 75) - - VStack(alignment: .leading, spacing: 12) { - accountFields - } - } - .textFieldStyle(RoundedBorderTextFieldStyle()) - - Spacer() - HStack{ - if editModel.accountIsUpdatingCredentials { - ProgressView("Updating") - } - Spacer() - cancelButton - updateButton - } - }.frame(height: 220) - } - - #if os(iOS) - var iOSForm: some View { - - NavigationView { - List { - Section(header: header, content: { - accountFields - }) - } - .listStyle(InsetGroupedListStyle()) - .navigationBarItems( - leading: - cancelButton - , trailing: - HStack { - if editModel.accountIsUpdatingCredentials { - ProgressView() - .frame(width: 20 , height: 20) - .padding(.horizontal, 4) - } - updateButton - } - - ) - } - } - #endif - - var header: some View { - HStack { - Spacer() - VStack { - Image(rsImage: accountSyncError.account.smallIcon!.image) - .resizable() - .frame(width: 30, height: 30) - Text(accountSyncError.account.nameForDisplay) - Text(accountSyncError.error.localizedDescription) - .multilineTextAlignment(.center) - .lineLimit(3) - .padding(.top, 4) - } - Spacer() - }.padding() - } - - @ViewBuilder - var accountFields: some View { - TextField("Username", text: $editModel.userName) - SecureField("Password", text: $editModel.password) - if accountSyncError.account.type == .freshRSS { - TextField("API URL", text: $editModel.apiUrl) - } - } - - @ViewBuilder - var updateButton: some View { - if accountSyncError.account.type != .freshRSS { - Button("Update", action: { - editModel.updateAccountCredentials(accountSyncError.account) - }).disabled(editModel.userName.count == 0 || editModel.password.count == 0) - } else { - Button("Update", action: { - editModel.updateAccountCredentials(accountSyncError.account) - }).disabled(editModel.userName.count == 0 || editModel.password.count == 0 || editModel.apiUrl.count == 0) - } - } - - var cancelButton: some View { - Button("Cancel", action: { - presentationMode.wrappedValue.dismiss() - }) - } - -} - diff --git a/Multiplatform/Shared/Add/Add Account Models/AddAccountSignUp.swift b/Multiplatform/Shared/Add/Add Account Models/AddAccountSignUp.swift deleted file mode 100644 index d2c11d5cd..000000000 --- a/Multiplatform/Shared/Add/Add Account Models/AddAccountSignUp.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// AddAccountSignUp.swift -// NetNewsWire -// -// Created by Stuart Breckenridge on 06/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import Account -#if os(iOS) -import UIKit -#endif - - -/// Helper functions common to most account services. -protocol AddAccountSignUp { - func presentSignUpOption(_ accountType: AccountType) -} - - -extension AddAccountSignUp { - func presentSignUpOption(_ accountType: AccountType) { - #if os(macOS) - switch accountType { - case .bazQux: - NSWorkspace.shared.open(URL(string: "https://bazqux.com")!) - case .feedbin: - NSWorkspace.shared.open(URL(string: "https://feedbin.com/signup")!) - case .feedly: - NSWorkspace.shared.open(URL(string: "https://feedly.com")!) - case .feedWrangler: - NSWorkspace.shared.open(URL(string: "https://feedwrangler.net/users/new")!) - case .freshRSS: - NSWorkspace.shared.open(URL(string: "https://freshrss.org")!) - case .inoreader: - NSWorkspace.shared.open(URL(string: "https://www.inoreader.com")!) - case .newsBlur: - NSWorkspace.shared.open(URL(string: "https://newsblur.com")!) - case .theOldReader: - NSWorkspace.shared.open(URL(string: "https://theoldreader.com")!) - default: - return - } - #else - switch accountType { - case .bazQux: - UIApplication.shared.open(URL(string: "https://bazqux.com")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil) - case .feedbin: - UIApplication.shared.open(URL(string: "https://feedbin.com/signup")!, options: [:], completionHandler: nil) - case .feedly: - UIApplication.shared.open(URL(string: "https://feedly.com")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil) - case .feedWrangler: - UIApplication.shared.open(URL(string: "https://feedwrangler.net/users/new")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil) - case .freshRSS: - UIApplication.shared.open(URL(string: "https://freshrss.org")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil) - case .inoreader: - UIApplication.shared.open(URL(string: "https://www.inoreader.com")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil) - case .newsBlur: - UIApplication.shared.open(URL(string: "https://newsblur.com")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil) - case .theOldReader: - UIApplication.shared.open(URL(string: "https://theoldreader.com")!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil) - default: - return - } - #endif - } -} diff --git a/Multiplatform/Shared/Add/Add Account Models/AddFeedWranglerViewModel.swift b/Multiplatform/Shared/Add/Add Account Models/AddFeedWranglerViewModel.swift deleted file mode 100644 index f05ac68d1..000000000 --- a/Multiplatform/Shared/Add/Add Account Models/AddFeedWranglerViewModel.swift +++ /dev/null @@ -1,70 +0,0 @@ -// -// AddFeedWranglerViewModel.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 05/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore -import RSWeb -import Secrets - -class AddFeedWranglerViewModel: ObservableObject, AddAccountSignUp { - @Published var isAuthenticating: Bool = false - @Published var accountUpdateError: AccountUpdateErrors = .none - @Published var showError: Bool = false - @Published var username: String = "" - @Published var password: String = "" - @Published var canDismiss: Bool = false - @Published var showPassword: Bool = false - - func authenticateFeedWrangler() { - - isAuthenticating = true - let credentials = Credentials(type: .feedWranglerBasic, username: username, secret: password) - - Account.validateCredentials(type: .feedWrangler, credentials: credentials) { result in - - - self.isAuthenticating = false - - switch result { - case .success(let validatedCredentials): - - guard let validatedCredentials = validatedCredentials else { - self.accountUpdateError = .invalidUsernamePassword - self.showError = true - return - } - - let account = AccountManager.shared.createAccount(type: .feedWrangler) - - do { - try account.removeCredentials(type: .feedWranglerBasic) - try account.removeCredentials(type: .feedWranglerToken) - try account.storeCredentials(credentials) - try account.storeCredentials(validatedCredentials) - self.canDismiss = true - account.refreshAll(completion: { result in - switch result { - case .success: - break - case .failure(let error): - self.accountUpdateError = .other(error: error) - self.showError = true - } - }) - } catch { - self.accountUpdateError = .keyChainError - self.showError = true - } - case .failure: - self.accountUpdateError = .networkError - self.showError = true - } - } - } -} diff --git a/Multiplatform/Shared/Add/Add Account Models/AddFeedbinViewModel.swift b/Multiplatform/Shared/Add/Add Account Models/AddFeedbinViewModel.swift deleted file mode 100644 index a043fec28..000000000 --- a/Multiplatform/Shared/Add/Add Account Models/AddFeedbinViewModel.swift +++ /dev/null @@ -1,68 +0,0 @@ -// -// AddFeedbinViewModel.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 05/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore -import RSWeb -import Secrets - -class AddFeedbinViewModel: ObservableObject, AddAccountSignUp { - @Published var isAuthenticating: Bool = false - @Published var accountUpdateError: AccountUpdateErrors = .none - @Published var showError: Bool = false - @Published var username: String = "" - @Published var password: String = "" - @Published var canDismiss: Bool = false - @Published var showPassword: Bool = false - - func authenticateFeedbin() { - isAuthenticating = true - let credentials = Credentials(type: .basic, username: username, secret: password) - - Account.validateCredentials(type: .feedbin, credentials: credentials) { result in - self.isAuthenticating = false - - switch result { - case .success(let validatedCredentials): - - guard let validatedCredentials = validatedCredentials else { - self.accountUpdateError = .invalidUsernamePassword - self.showError = true - return - } - - let account = AccountManager.shared.createAccount(type: .feedbin) - - do { - try account.removeCredentials(type: .basic) - try account.storeCredentials(validatedCredentials) - self.isAuthenticating = false - self.canDismiss = true - account.refreshAll(completion: { result in - switch result { - case .success: - break - case .failure(let error): - self.accountUpdateError = .other(error: error) - self.showError = true - } - }) - - } catch { - self.accountUpdateError = .keyChainError - self.showError = true - } - - case .failure: - self.accountUpdateError = .networkError - self.showError = true - } - } - } -} diff --git a/Multiplatform/Shared/Add/Add Account Models/AddFeedlyViewModel.swift b/Multiplatform/Shared/Add/Add Account Models/AddFeedlyViewModel.swift deleted file mode 100644 index 7312b2f61..000000000 --- a/Multiplatform/Shared/Add/Add Account Models/AddFeedlyViewModel.swift +++ /dev/null @@ -1,67 +0,0 @@ -// -// AddFeedlyViewModel.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 05/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore -import RSWeb -import Secrets - -class AddFeedlyViewModel: ObservableObject, OAuthAccountAuthorizationOperationDelegate, AddAccountSignUp { - @Published var isAuthenticating: Bool = false - @Published var accountUpdateError: AccountUpdateErrors = .none - @Published var showError: Bool = false - @Published var username: String = "" - @Published var password: String = "" - - func authenticateFeedly() { - isAuthenticating = true - let addAccount = OAuthAccountAuthorizationOperation(accountType: .feedly) - addAccount.delegate = self - #if os(macOS) - addAccount.presentationAnchor = NSApplication.shared.windows.last - #else - addAccount.presentationAnchor = UIApplication.shared.windows.last - #endif - MainThreadOperationQueue.shared.add(addAccount) - } - - func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) { - - isAuthenticating = false - - // macOS only: `ASWebAuthenticationSession` leaves the browser in the foreground. - // Ensure the app is in the foreground so the user can see their Feedly account load. - #if os(macOS) - NSApplication.shared.activate(ignoringOtherApps: true) - #endif - - account.refreshAll { [weak self] result in - switch result { - case .success: - break - case .failure(let error): - self?.accountUpdateError = .other(error: error) - self?.showError = true - } - } - } - - func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) { - isAuthenticating = false - - // macOS only: `ASWebAuthenticationSession` leaves the browser in the foreground. - // Ensure the app is in the foreground so the user can see the error. - #if os(macOS) - NSApplication.shared.activate(ignoringOtherApps: true) - #endif - - accountUpdateError = .other(error: error) - showError = true - } -} diff --git a/Multiplatform/Shared/Add/Add Account Models/AddNewsBlurViewModel.swift b/Multiplatform/Shared/Add/Add Account Models/AddNewsBlurViewModel.swift deleted file mode 100644 index 96f58b78f..000000000 --- a/Multiplatform/Shared/Add/Add Account Models/AddNewsBlurViewModel.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// AddNewsBlurViewModel.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 05/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore -import RSWeb -import Secrets - -class AddNewsBlurViewModel: ObservableObject, AddAccountSignUp { - @Published var isAuthenticating: Bool = false - @Published var accountUpdateError: AccountUpdateErrors = .none - @Published var showError: Bool = false - @Published var username: String = "" - @Published var password: String = "" - @Published var canDismiss: Bool = false - @Published var showPassword: Bool = false - - func authenticateNewsBlur() { - isAuthenticating = true - let credentials = Credentials(type: .newsBlurBasic, username: username, secret: password) - - Account.validateCredentials(type: .newsBlur, credentials: credentials) { result in - - self.isAuthenticating = false - - switch result { - case .success(let validatedCredentials): - - guard let validatedCredentials = validatedCredentials else { - self.accountUpdateError = .invalidUsernamePassword - self.showError = true - return - } - - let account = AccountManager.shared.createAccount(type: .newsBlur) - - do { - try account.removeCredentials(type: .newsBlurBasic) - try account.removeCredentials(type: .newsBlurSessionId) - try account.storeCredentials(credentials) - try account.storeCredentials(validatedCredentials) - self.canDismiss = true - account.refreshAll(completion: { result in - switch result { - case .success: - break - case .failure(let error): - self.accountUpdateError = .other(error: error) - self.showError = true - } - }) - - } catch { - self.accountUpdateError = .keyChainError - self.showError = true - } - - case .failure: - self.accountUpdateError = .networkError - self.showError = true - } - } - } - -} diff --git a/Multiplatform/Shared/Add/Add Account Models/AddReaderAPIViewModel.swift b/Multiplatform/Shared/Add/Add Account Models/AddReaderAPIViewModel.swift deleted file mode 100644 index c6e67897f..000000000 --- a/Multiplatform/Shared/Add/Add Account Models/AddReaderAPIViewModel.swift +++ /dev/null @@ -1,122 +0,0 @@ -// -// AddReaderAPIViewModel.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 05/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore -import RSWeb -import Secrets - -class AddReaderAPIViewModel: ObservableObject, AddAccountSignUp { - @Published var isAuthenticating: Bool = false - @Published var accountUpdateError: AccountUpdateErrors = .none - @Published var showError: Bool = false - @Published var username: String = "" - @Published var password: String = "" - @Published var apiUrl: String = "" - @Published var canDismiss: Bool = false - @Published var showPassword: Bool = false - - func authenticateReaderAccount(_ accountType: AccountType) { - isAuthenticating = true - - let credentials = Credentials(type: .readerBasic, username: username, secret: password) - - if accountType == .freshRSS { - Account.validateCredentials(type: accountType, credentials: credentials, endpoint: URL(string: apiUrl)!) { result in - - self.isAuthenticating = false - - switch result { - case .success(let validatedCredentials): - - guard let validatedCredentials = validatedCredentials else { - self.accountUpdateError = .invalidUsernamePassword - self.showError = true - return - } - - let account = AccountManager.shared.createAccount(type: .freshRSS) - - do { - try account.removeCredentials(type: .readerBasic) - try account.removeCredentials(type: .readerAPIKey) - try account.storeCredentials(credentials) - try account.storeCredentials(validatedCredentials) - self.canDismiss = true - account.refreshAll(completion: { result in - switch result { - case .success: - break - case .failure(let error): - self.accountUpdateError = .other(error: error) - self.showError = true - } - }) - - } catch { - self.accountUpdateError = .keyChainError - self.showError = true - } - - case .failure: - self.accountUpdateError = .networkError - self.showError = true - } - } - } - - else { - - Account.validateCredentials(type: accountType, credentials: credentials) { result in - - self.isAuthenticating = false - - switch result { - case .success(let validatedCredentials): - - guard let validatedCredentials = validatedCredentials else { - self.accountUpdateError = .invalidUsernamePassword - self.showError = true - return - } - - let account = AccountManager.shared.createAccount(type: .freshRSS) - - do { - try account.removeCredentials(type: .readerBasic) - try account.removeCredentials(type: .readerAPIKey) - try account.storeCredentials(credentials) - try account.storeCredentials(validatedCredentials) - self.canDismiss = true - account.refreshAll(completion: { result in - switch result { - case .success: - break - case .failure(let error): - self.accountUpdateError = .other(error: error) - self.showError = true - } - }) - - } catch { - self.accountUpdateError = .keyChainError - self.showError = true - } - - case .failure: - self.accountUpdateError = .networkError - self.showError = true - } - } - - } - - } - -} diff --git a/Multiplatform/Shared/Add/Add Account Sheets/AddCloudKitAccountView.swift b/Multiplatform/Shared/Add/Add Account Sheets/AddCloudKitAccountView.swift deleted file mode 100644 index b7ee03318..000000000 --- a/Multiplatform/Shared/Add/Add Account Sheets/AddCloudKitAccountView.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// AddCloudKitAccountView.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 03/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct AddCloudKitAccountView: View { - - @Environment (\.presentationMode) var presentationMode - - var body: some View { - - #if os(macOS) - macBody - #else - NavigationView { - iosBody - } - #endif - - } - - #if os(iOS) - var iosBody: some View { - List { - Section(header: formHeader, footer: formFooter, content: { - Button(action: { - _ = AccountManager.shared.createAccount(type: .cloudKit) - presentationMode.wrappedValue.dismiss() - }, label: { - HStack { - Spacer() - Text("Add Account") - Spacer() - } - }).disabled(AccountManager.shared.activeAccounts.filter({ $0.type == .cloudKit }).count > 0) - }) - }.navigationBarItems(leading: - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - }) - ) - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(Text(AccountType.cloudKit.localizedAccountName())) - .listStyle(InsetGroupedListStyle()) - } - #endif - - #if os(macOS) - var macBody: some View { - VStack { - HStack(spacing: 16) { - VStack(alignment: .leading) { - AccountType.cloudKit.image() - .resizable() - .frame(width: 50, height: 50) - Spacer() - } - VStack(alignment: .leading, spacing: 8) { - Text("Sign in to your iCloud account.") - .font(.headline) - - Text("This account syncs across your Mac and iOS devices using your iCloud account.") - .foregroundColor(.secondary) - .font(.callout) - .lineLimit(2) - .padding(.top, 4) - - Spacer() - HStack(spacing: 8) { - Spacer() - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - .frame(width: 60) - }).keyboardShortcut(.cancelAction) - - Button(action: { - _ = AccountManager.shared.createAccount(type: .cloudKit) - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Create") - .frame(width: 60) - }) - .keyboardShortcut(.defaultAction) - .disabled(AccountManager.shared.activeAccounts.filter({ $0.type == .cloudKit }).count > 0) - } - } - } - } - .padding() - .frame(minWidth: 400, maxWidth: 400, maxHeight: 150) - } - #endif - - var formHeader: some View { - HStack { - Spacer() - VStack(alignment: .center) { - AccountType.cloudKit.image() - .resizable() - .frame(width: 50, height: 50) - } - Spacer() - }.padding(.vertical) - } - - var formFooter: some View { - HStack { - Spacer() - VStack(spacing: 8) { - Text("This account syncs across your Mac and iOS devices using your iCloud account.").foregroundColor(.secondary) - } - .multilineTextAlignment(.center) - .font(.caption) - Spacer() - - }.padding(.vertical) - } -} - -struct AddCloudKitAccountView_Previews: PreviewProvider { - static var previews: some View { - AddCloudKitAccountView() - } -} diff --git a/Multiplatform/Shared/Add/Add Account Sheets/AddFeedWranglerAccountView.swift b/Multiplatform/Shared/Add/Add Account Sheets/AddFeedWranglerAccountView.swift deleted file mode 100644 index 5fe5d27c7..000000000 --- a/Multiplatform/Shared/Add/Add Account Sheets/AddFeedWranglerAccountView.swift +++ /dev/null @@ -1,216 +0,0 @@ -// -// AddFeedWranglerAccountView.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 03/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore -import RSWeb -import Secrets - - -struct AddFeedWranglerAccountView: View { - - @Environment (\.presentationMode) var presentationMode - @StateObject private var model = AddFeedWranglerViewModel() - - var body: some View { - #if os(macOS) - macBody - #else - NavigationView { - iosBody - } - #endif - } - - - #if os(iOS) - var iosBody: some View { - List { - Section(header: formHeader, content: { - TextField("Email", text: $model.username) - if model.showPassword == false { - ZStack { - HStack { - SecureField("Password", text: $model.password) - Spacer() - Image(systemName: "eye.fill") - .foregroundColor(.accentColor) - .onTapGesture { - model.showPassword = true - } - } - } - } - else { - ZStack { - HStack { - TextField("Password", text: $model.password) - Spacer() - Image(systemName: "eye.slash.fill") - .foregroundColor(.accentColor) - .onTapGesture { - model.showPassword = false - } - } - } - } - - }) - - Section(footer: formFooter, content: { - Button(action: { - model.authenticateFeedWrangler() - }, label: { - HStack { - Spacer() - Text("Add Account") - Spacer() - } - }).disabled(model.username.isEmpty || model.password.isEmpty) - }) - - } - .navigationBarItems(leading: - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - })) - .listStyle(InsetGroupedListStyle()) - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(Text("Feed Wrangler")) - .alert(isPresented: $model.showError, content: { - Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel(Text("Dismiss"))) - }) - .onReceive(model.$canDismiss, perform: { value in - if value == true { - presentationMode.wrappedValue.dismiss() - } - }) - } - #endif - - #if os(macOS) - var macBody: some View { - VStack { - HStack(spacing: 16) { - VStack(alignment: .leading) { - AccountType.feedWrangler.image() - .resizable() - .frame(width: 50, height: 50) - Spacer() - } - VStack(alignment: .leading, spacing: 8) { - Text("Sign in to your Feed Wrangler account.") - .font(.headline) - HStack { - Text("Don’t have a Feed Wrangler account?") - .font(.callout) - Button(action: { - model.presentSignUpOption(.feedWrangler) - }, label: { - Text("Sign up here.").font(.callout) - }).buttonStyle(LinkButtonStyle()) - } - - HStack { - VStack(alignment: .trailing, spacing: 14) { - Text("Email") - Text("Password") - } - VStack(spacing: 8) { - TextField("me@email.com", text: $model.username) - SecureField("•••••••••••", text: $model.password) - } - } - - Text("Your username and password will be encrypted and stored in Keychain.") - .foregroundColor(.secondary) - .font(.callout) - .lineLimit(2) - .padding(.top, 4) - - Spacer() - HStack(spacing: 8) { - Spacer() - ProgressView() - .scaleEffect(CGSize(width: 0.5, height: 0.5)) - .hidden(!model.isAuthenticating) - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - .frame(width: 60) - }).keyboardShortcut(.cancelAction) - - Button(action: { - model.authenticateFeedWrangler() - }, label: { - Text("Sign In") - .frame(width: 60) - }) - .keyboardShortcut(.defaultAction) - .disabled(model.username.isEmpty || model.password.isEmpty) - } - } - } - } - .padding() - .frame(minWidth: 400, maxWidth: 400, minHeight: 230, maxHeight: 260) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .alert(isPresented: $model.showError, content: { - Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel()) - }) - .onReceive(model.$canDismiss, perform: { value in - if value == true { - presentationMode.wrappedValue.dismiss() - } - }) - } - #endif - - var formHeader: some View { - HStack { - Spacer() - VStack(alignment: .center) { - AccountType.feedWrangler.image() - .resizable() - .frame(width: 50, height: 50) - } - Spacer() - }.padding(.vertical) - } - - var formFooter: some View { - HStack { - Spacer() - VStack(spacing: 8) { - Text("Sign in to your Feed Wrangler account and sync your feeds across your devices. Your username and password and password will be encrypted and stored in Keychain.").foregroundColor(.secondary) - Text("Don’t have a Feed Wrangler account?").foregroundColor(.secondary) - Button(action: { - model.presentSignUpOption(.feedWrangler) - }, label: { - Text("Sign Up Here").foregroundColor(.blue).multilineTextAlignment(.center) - }) - ProgressView().hidden(!model.isAuthenticating) - } - .multilineTextAlignment(.center) - .font(.caption2) - Spacer() - - }.padding(.vertical) - } - -} - -struct AddFeedWranglerAccountView_Previews: PreviewProvider { - static var previews: some View { - AddFeedWranglerAccountView() - } -} diff --git a/Multiplatform/Shared/Add/Add Account Sheets/AddFeedbinAccountView.swift b/Multiplatform/Shared/Add/Add Account Sheets/AddFeedbinAccountView.swift deleted file mode 100644 index dd33b6434..000000000 --- a/Multiplatform/Shared/Add/Add Account Sheets/AddFeedbinAccountView.swift +++ /dev/null @@ -1,214 +0,0 @@ -// -// AddFeedbinAccountView.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 02/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore -import RSWeb -import Secrets - -struct AddFeedbinAccountView: View { - - @Environment (\.presentationMode) var presentationMode - @StateObject private var model = AddFeedbinViewModel() - - var body: some View { - #if os(macOS) - macBody - #else - NavigationView { - iosBody - } - #endif - } - - #if os(iOS) - var iosBody: some View { - List { - Section(header: formHeader, content: { - TextField("Email", text: $model.username) - if model.showPassword == false { - ZStack { - HStack { - SecureField("Password", text: $model.password) - Spacer() - Image(systemName: "eye.fill") - .foregroundColor(.accentColor) - .onTapGesture { - model.showPassword = true - } - } - } - } - else { - ZStack { - HStack { - TextField("Password", text: $model.password) - Spacer() - Image(systemName: "eye.slash.fill") - .foregroundColor(.accentColor) - .onTapGesture { - model.showPassword = false - } - } - } - } - - }) - - Section(footer: formFooter, content: { - Button(action: { - model.authenticateFeedbin() - }, label: { - HStack { - Spacer() - Text("Add Account") - Spacer() - } - }).disabled(model.username.isEmpty || model.password.isEmpty) - }) - - } - .navigationBarItems(leading: - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - })) - .listStyle(InsetGroupedListStyle()) - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(Text("Feedbin")) - .alert(isPresented: $model.showError, content: { - Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel(Text("Dismiss"))) - }) - .onReceive(model.$canDismiss, perform: { value in - if value == true { - presentationMode.wrappedValue.dismiss() - } - }) - } - #endif - - #if os(macOS) - var macBody: some View { - VStack { - HStack(spacing: 16) { - VStack(alignment: .leading) { - AccountType.feedbin.image() - .frame(width: 50, height: 50) - Spacer() - } - VStack(alignment: .leading, spacing: 8) { - Text("Sign in to your Feedbin account.") - .font(.headline) - HStack { - Text("Don’t have a Feedbin account?") - .font(.callout) - Button(action: { - model.presentSignUpOption(.feedbin) - }, label: { - Text("Sign up here.").font(.callout) - }).buttonStyle(LinkButtonStyle()) - } - - HStack { - VStack(alignment: .trailing, spacing: 14) { - Text("Email") - Text("Password") - } - VStack(spacing: 8) { - TextField("me@email.com", text: $model.username) - SecureField("•••••••••••", text: $model.password) - } - } - - Text("Your username and password will be encrypted and stored in Keychain.") - .foregroundColor(.secondary) - .font(.callout) - .lineLimit(2) - .padding(.top, 4) - - Spacer() - HStack(spacing: 8) { - Spacer() - ProgressView() - .scaleEffect(CGSize(width: 0.5, height: 0.5)) - .hidden(!model.isAuthenticating) - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - .frame(width: 60) - }).keyboardShortcut(.cancelAction) - - Button(action: { - model.authenticateFeedbin() - }, label: { - Text("Sign In") - .frame(width: 60) - }) - .keyboardShortcut(.defaultAction) - .disabled(model.username.isEmpty || model.password.isEmpty) - } - } - } - } - .padding() - .frame(minWidth: 400, maxWidth: 400, minHeight: 230, maxHeight: 260) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .alert(isPresented: $model.showError, content: { - Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel()) - }) - .onReceive(model.$canDismiss, perform: { value in - if value == true { - presentationMode.wrappedValue.dismiss() - } - }) - } - #endif - - var formHeader: some View { - HStack { - Spacer() - VStack(alignment: .center) { - AccountType.feedbin.image() - .resizable() - .frame(width: 50, height: 50) - } - Spacer() - }.padding(.vertical) - } - - var formFooter: some View { - HStack { - Spacer() - VStack(spacing: 8) { - Text("Sign in to your Feedbin account and sync your feeds across your devices. Your username and password and password will be encrypted and stored in Keychain.").foregroundColor(.secondary) - Text("Don’t have a Feedbin account?").foregroundColor(.secondary) - Button(action: { - model.presentSignUpOption(.feedbin) - }, label: { - Text("Sign Up Here").foregroundColor(.blue).multilineTextAlignment(.center) - }) - ProgressView().hidden(!model.isAuthenticating) - } - .multilineTextAlignment(.center) - .font(.caption2) - Spacer() - - }.padding(.vertical) - } - - -} - -struct AddFeedbinAccountView_Previews: PreviewProvider { - static var previews: some View { - AddFeedbinAccountView() - } -} diff --git a/Multiplatform/Shared/Add/Add Account Sheets/AddFeedlyAccountView.swift b/Multiplatform/Shared/Add/Add Account Sheets/AddFeedlyAccountView.swift deleted file mode 100644 index 32f14553a..000000000 --- a/Multiplatform/Shared/Add/Add Account Sheets/AddFeedlyAccountView.swift +++ /dev/null @@ -1,149 +0,0 @@ -// -// AddFeedlyAccountView.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 05/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore -import RSWeb -import Secrets - -struct AddFeedlyAccountView: View { - - @Environment (\.presentationMode) var presentationMode - @StateObject private var model = AddFeedlyViewModel() - - var body: some View { - #if os(macOS) - macBody - #else - NavigationView { - iosBody - } - #endif - } - - - #if os(iOS) - var iosBody: some View { - List { - Section(header: formHeader, footer: formFooter, content: { - Button(action: { - model.authenticateFeedly() - }, label: { - HStack { - Spacer() - Text("Add Account") - Spacer() - } - }) - }) - }.navigationBarItems(leading: - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - }) - ) - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(Text(AccountType.feedly.localizedAccountName())) - .listStyle(InsetGroupedListStyle()) - - } - #endif - - #if os(macOS) - var macBody: some View { - VStack { - HStack(spacing: 16) { - VStack(alignment: .leading) { - AccountType.feedly.image() - .resizable() - .frame(width: 50, height: 50) - Spacer() - } - VStack(alignment: .leading, spacing: 8) { - Text("Sign in to your Feedly account.") - .font(.headline) - HStack { - Text("Don’t have a Feedly account?") - .font(.callout) - Button(action: { - model.presentSignUpOption(.feedly) - }, label: { - Text("Sign up here.").font(.callout) - }).buttonStyle(LinkButtonStyle()) - } - - Spacer() - HStack(spacing: 8) { - Spacer() - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - .frame(width: 60) - }).keyboardShortcut(.cancelAction) - - Button(action: { - model.authenticateFeedly() - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Sign In") - .frame(width: 60) - }) - .keyboardShortcut(.defaultAction) - .disabled(AccountManager.shared.activeAccounts.filter({ $0.type == .cloudKit }).count > 0) - } - } - } - } - .padding() - .frame(minWidth: 400, maxWidth: 400, maxHeight: 150) - .alert(isPresented: $model.showError, content: { - Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel()) - }) - } - #endif - - var formHeader: some View { - HStack { - Spacer() - VStack(alignment: .center) { - AccountType.feedly.image() - .resizable() - .frame(width: 50, height: 50) - } - Spacer() - }.padding(.vertical) - } - - var formFooter: some View { - HStack { - Spacer() - VStack(spacing: 8) { - Text("Sign in to your Feedly account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have an Feedly account?").foregroundColor(.secondary) - Button(action: { - model.presentSignUpOption(.feedly) - }, label: { - Text("Sign Up Here").foregroundColor(.blue).multilineTextAlignment(.center) - }) - } - .multilineTextAlignment(.center) - .font(.caption) - Spacer() - - }.padding(.vertical) - } - -} - -struct AddFeedlyAccountView_Previews: PreviewProvider { - static var previews: some View { - AddFeedlyAccountView() - } -} diff --git a/Multiplatform/Shared/Add/Add Account Sheets/AddLocalAccountView.swift b/Multiplatform/Shared/Add/Add Account Sheets/AddLocalAccountView.swift deleted file mode 100644 index e24923273..000000000 --- a/Multiplatform/Shared/Add/Add Account Sheets/AddLocalAccountView.swift +++ /dev/null @@ -1,140 +0,0 @@ -// -// AddLocalAccountView.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 02/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore - -struct AddLocalAccountView: View { - - @State private var newAccountName: String = "" - @Environment (\.presentationMode) var presentationMode - - var body: some View { - #if os(macOS) - macBody - #else - NavigationView { - iosBody - } - #endif - } - - #if os(iOS) - var iosBody: some View { - List { - Section(header: formHeader, content: { - TextField("Account Name", text: $newAccountName) - }) - - Section(footer: formFooter, content: { - Button(action: { - let newAccount = AccountManager.shared.createAccount(type: .onMyMac) - newAccount.name = newAccountName - presentationMode.wrappedValue.dismiss() - }, label: { - HStack { - Spacer() - Text("Add Account") - Spacer() - } - }) - }) - }.navigationBarItems(leading: - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - }) - ) - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(Text(AccountType.onMyMac.localizedAccountName())) - .listStyle(InsetGroupedListStyle()) - } - #endif - - #if os(macOS) - var macBody: some View { - VStack { - HStack(spacing: 16) { - VStack(alignment: .leading) { - AccountType.onMyMac.image() - .resizable() - .frame(width: 50, height: 50) - Spacer() - } - VStack(alignment: .leading, spacing: 8) { - Text("Create a local account on your Mac.") - .font(.headline) - Text("Local accounts store their data on your Mac. They do not sync across your devices.") - .font(.callout) - .foregroundColor(.secondary) - HStack { - Text("Name: ") - TextField("Account Name", text: $newAccountName) - }.padding(.top, 8) - Spacer() - HStack(spacing: 8) { - Spacer() - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - .frame(width: 60) - }).keyboardShortcut(.cancelAction) - - Button(action: { - let newAccount = AccountManager.shared.createAccount(type: .onMyMac) - newAccount.name = newAccountName - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Create") - .frame(width: 60) - }).keyboardShortcut(.defaultAction) - } - } - } - } - .padding() - .frame(minWidth: 400, maxWidth: 400, minHeight: 230, maxHeight: 260) - .textFieldStyle(RoundedBorderTextFieldStyle()) - } - #endif - - var formHeader: some View { - HStack { - Spacer() - VStack(alignment: .center) { - AccountType.onMyMac.image() - .resizable() - .frame(width: 50, height: 50) - } - Spacer() - }.padding(.vertical) - } - - var formFooter: some View { - HStack { - Spacer() - VStack(spacing: 8) { - Text("Local accounts do not sync your feeds across devices.").foregroundColor(.secondary) - } - .multilineTextAlignment(.center) - .font(.caption) - Spacer() - - }.padding(.vertical) - } - -} - -struct AddLocalAccount_Previews: PreviewProvider { - static var previews: some View { - AddLocalAccountView() - } -} diff --git a/Multiplatform/Shared/Add/Add Account Sheets/AddNewsBlurAccountView.swift b/Multiplatform/Shared/Add/Add Account Sheets/AddNewsBlurAccountView.swift deleted file mode 100644 index 8f6c35b1c..000000000 --- a/Multiplatform/Shared/Add/Add Account Sheets/AddNewsBlurAccountView.swift +++ /dev/null @@ -1,212 +0,0 @@ -// -// AddNewsBlurAccountView.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 03/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore -import RSWeb -import Secrets - -struct AddNewsBlurAccountView: View { - - @Environment (\.presentationMode) var presentationMode - @StateObject private var model = AddNewsBlurViewModel() - - var body: some View { - #if os(macOS) - macBody - #else - NavigationView { - iosBody - } - #endif - } - - #if os(iOS) - var iosBody: some View { - List { - Section(header: formHeader, content: { - TextField("Email", text: $model.username) - if model.showPassword == false { - ZStack { - HStack { - SecureField("Password", text: $model.password) - Spacer() - Image(systemName: "eye.fill") - .foregroundColor(.accentColor) - .onTapGesture { - model.showPassword = true - } - } - } - } - else { - ZStack { - HStack { - TextField("Password", text: $model.password) - Spacer() - Image(systemName: "eye.slash.fill") - .foregroundColor(.accentColor) - .onTapGesture { - model.showPassword = false - } - } - } - } - - }) - - Section(footer: formFooter, content: { - Button(action: { - model.authenticateNewsBlur() - }, label: { - HStack { - Spacer() - Text("Add Account") - Spacer() - } - }).disabled(model.username.isEmpty || model.password.isEmpty) - }) - - } - .navigationBarItems(leading: - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - })) - .listStyle(InsetGroupedListStyle()) - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(Text("NewsBlur")) - .alert(isPresented: $model.showError, content: { - Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel(Text("Dismiss"))) - }) - .onReceive(model.$canDismiss, perform: { value in - if value == true { - presentationMode.wrappedValue.dismiss() - } - }) - } - #endif - - #if os(macOS) - var macBody: some View { - VStack { - HStack(spacing: 16) { - VStack(alignment: .leading) { - AccountType.newsBlur.image() - .frame(width: 50, height: 50) - Spacer() - } - VStack(alignment: .leading, spacing: 8) { - Text("Sign in to your NewsBlur account.") - .font(.headline) - HStack { - Text("Don’t have a NewsBlur account?") - .font(.callout) - Button(action: { - model.presentSignUpOption(.newsBlur) - }, label: { - Text("Sign up here.").font(.callout) - }).buttonStyle(LinkButtonStyle()) - } - - HStack { - VStack(alignment: .trailing, spacing: 14) { - Text("Email") - Text("Password") - } - VStack(spacing: 8) { - TextField("me@email.com", text: $model.username) - SecureField("•••••••••••", text: $model.password) - } - } - - Text("Your username and password will be encrypted and stored in Keychain.") - .foregroundColor(.secondary) - .font(.callout) - .lineLimit(2) - .padding(.top, 4) - - Spacer() - HStack(spacing: 8) { - Spacer() - ProgressView() - .scaleEffect(CGSize(width: 0.5, height: 0.5)) - .hidden(!model.isAuthenticating) - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - .frame(width: 60) - }).keyboardShortcut(.cancelAction) - - Button(action: { - model.authenticateNewsBlur() - }, label: { - Text("Sign In") - .frame(width: 60) - }) - .keyboardShortcut(.defaultAction) - .disabled(model.username.isEmpty || model.password.isEmpty) - } - } - } - } - .padding() - .frame(minWidth: 400, maxWidth: 400, minHeight: 230, maxHeight: 260) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .alert(isPresented: $model.showError, content: { - Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel()) - }) - .onReceive(model.$canDismiss, perform: { value in - if value == true { - presentationMode.wrappedValue.dismiss() - } - }) - } - #endif - - var formHeader: some View { - HStack { - Spacer() - VStack(alignment: .center) { - AccountType.newsBlur.image() - .resizable() - .frame(width: 50, height: 50) - } - Spacer() - }.padding(.vertical) - } - - var formFooter: some View { - HStack { - Spacer() - VStack(spacing: 8) { - Text("Sign in to your NewsBlur account and sync your feeds across your devices. Your username and password and password will be encrypted and stored in Keychain.").foregroundColor(.secondary) - Text("Don’t have a NewsBlur account?").foregroundColor(.secondary) - Button(action: { - model.presentSignUpOption(.newsBlur) - }, label: { - Text("Sign Up Here").foregroundColor(.blue).multilineTextAlignment(.center) - }) - ProgressView().hidden(!model.isAuthenticating) - } - .multilineTextAlignment(.center) - .font(.caption2) - Spacer() - - }.padding(.vertical) - } -} - -struct AddNewsBlurAccountView_Previews: PreviewProvider { - static var previews: some View { - AddNewsBlurAccountView() - } -} diff --git a/Multiplatform/Shared/Add/Add Account Sheets/AddReaderAPIAccountView.swift b/Multiplatform/Shared/Add/Add Account Sheets/AddReaderAPIAccountView.swift deleted file mode 100644 index 0d2f70c75..000000000 --- a/Multiplatform/Shared/Add/Add Account Sheets/AddReaderAPIAccountView.swift +++ /dev/null @@ -1,247 +0,0 @@ -// -// AddReaderAPIAccountView.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 03/12/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore -import RSWeb -import Secrets - -struct AddReaderAPIAccountView: View { - - @Environment (\.presentationMode) var presentationMode - @StateObject private var model = AddReaderAPIViewModel() - public var accountType: AccountType - - var body: some View { - #if os(macOS) - macBody - #else - NavigationView { - iosBody - } - #endif - } - - #if os(iOS) - var iosBody: some View { - List { - Section(header: formHeader, content: { - TextField("Email", text: $model.username) - if model.showPassword == false { - ZStack { - HStack { - SecureField("Password", text: $model.password) - Spacer() - Image(systemName: "eye.fill") - .foregroundColor(.accentColor) - .onTapGesture { - model.showPassword = true - } - } - } - } - else { - ZStack { - HStack { - TextField("Password", text: $model.password) - Spacer() - Image(systemName: "eye.slash.fill") - .foregroundColor(.accentColor) - .onTapGesture { - model.showPassword = false - } - } - } - } - if accountType == .freshRSS { - TextField("API URL", text: $model.apiUrl) - } - - }) - - Section(footer: formFooter, content: { - Button(action: { - model.authenticateReaderAccount(accountType) - }, label: { - HStack { - Spacer() - Text("Add Account") - Spacer() - } - }).disabled(createDisabled()) - }) - - } - .navigationBarItems(leading: - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - })) - .listStyle(InsetGroupedListStyle()) - .navigationBarTitleDisplayMode(.inline) - .navigationTitle(Text(accountType.localizedAccountName())) - .alert(isPresented: $model.showError, content: { - Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel(Text("Dismiss"))) - }) - .onReceive(model.$canDismiss, perform: { value in - if value == true { - presentationMode.wrappedValue.dismiss() - } - }) - } - #endif - - #if os(macOS) - var macBody: some View { - VStack { - HStack(spacing: 16) { - VStack(alignment: .leading) { - accountType.image() - .resizable() - .frame(width: 50, height: 50) - Spacer() - } - VStack(alignment: .leading, spacing: 8) { - Text("Sign in to your \(accountType.localizedAccountName()) account.") - .font(.headline) - HStack { - if accountType == .freshRSS { - Text("Don’t have a \(accountType.localizedAccountName()) instance?") - .font(.callout) - } else { - Text("Don’t have an \(accountType.localizedAccountName()) account?") - .font(.callout) - } - Button(action: { - model.presentSignUpOption(accountType) - }, label: { - Text(accountType == .freshRSS ? "Find out more." : "Sign up here.").font(.callout) - }).buttonStyle(LinkButtonStyle()) - } - - HStack { - VStack(alignment: .trailing, spacing: 14) { - Text("Email") - Text("Password") - if accountType == .freshRSS { - Text("API URL") - } - } - VStack(spacing: 8) { - TextField("me@email.com", text: $model.username) - SecureField("•••••••••••", text: $model.password) - if accountType == .freshRSS { - TextField("https://myfreshrss.rocks", text: $model.apiUrl) - } - } - } - - Text("Your username and password will be encrypted and stored in Keychain.") - .foregroundColor(.secondary) - .font(.callout) - .lineLimit(2) - .padding(.top, 4) - - Spacer() - HStack(spacing: 8) { - Spacer() - ProgressView() - .scaleEffect(CGSize(width: 0.5, height: 0.5)) - .hidden(!model.isAuthenticating) - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - .frame(width: 60) - }).keyboardShortcut(.cancelAction) - - Button(action: { - model.authenticateReaderAccount(accountType) - }, label: { - Text("Sign In") - .frame(width: 60) - }) - .keyboardShortcut(.defaultAction) - .disabled(createDisabled()) - } - } - } - } - .padding() - .frame(width: 400, height: height()) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .alert(isPresented: $model.showError, content: { - Alert(title: Text("Sign In Error"), message: Text(model.accountUpdateError.description), dismissButton: .cancel()) - }) - .onReceive(model.$canDismiss, perform: { value in - if value == true { - presentationMode.wrappedValue.dismiss() - } - }) - } - #endif - - - - - func createDisabled() -> Bool { - if accountType == .freshRSS { - return model.username.isEmpty || model.password.isEmpty || !model.apiUrl.mayBeURL - } - return model.username.isEmpty || model.password.isEmpty - } - - func height() -> CGFloat { - if accountType == .freshRSS { - return 260 - } - return 230 - } - - var formHeader: some View { - HStack { - Spacer() - VStack(alignment: .center) { - accountType.image() - .resizable() - .frame(width: 50, height: 50) - } - Spacer() - }.padding(.vertical) - } - - var formFooter: some View { - HStack { - Spacer() - VStack(spacing: 8) { - Text("Sign in to your \(accountType.localizedAccountName()) account and sync your feeds across your devices. Your username and password and password will be encrypted and stored in Keychain.").foregroundColor(.secondary) - Text("Don’t have a \(accountType.localizedAccountName()) instance?").foregroundColor(.secondary) - Button(action: { - model.presentSignUpOption(accountType) - }, label: { - Text("Sign Up Here").foregroundColor(.blue).multilineTextAlignment(.center) - }) - ProgressView().hidden(!model.isAuthenticating) - } - .multilineTextAlignment(.center) - .font(.caption2) - Spacer() - - }.padding(.vertical) - } - -} - -struct AddReaderAPIAccountView_Previews: PreviewProvider { - static var previews: some View { - AddReaderAPIAccountView(accountType: .freshRSS) - //AddReaderAPIAccountView(accountType: .inoreader) - } -} diff --git a/Multiplatform/Shared/Add/AddFolderModel.swift b/Multiplatform/Shared/Add/AddFolderModel.swift deleted file mode 100644 index 57da82f7e..000000000 --- a/Multiplatform/Shared/Add/AddFolderModel.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// AddFolderModel.swift -// NetNewsWire -// -// Created by Alex Faber on 04/07/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import Account -import RSCore -import SwiftUI - - -class AddFolderModel: ObservableObject { - - @Published var shouldDismiss: Bool = false - @Published var folderName: String = "" - @Published var selectedAccountIndex: Int = 0 - @Published var accounts: [Account] = [] - - @Published var showError: Bool = false - @Published var showProgressIndicator: Bool = false - - init() { - for account in - AccountManager.shared.sortedActiveAccounts{ - accounts.append(account) - } - } - - func addFolder() { - let account = accounts[selectedAccountIndex] - - showProgressIndicator = true - - account.addFolder(folderName){ result in - self.showProgressIndicator = false - - switch result { - case .success(_): - self.shouldDismiss = true - - case .failure(let error): - print("Error") - print(error) - } - - } - } -} diff --git a/Multiplatform/Shared/Add/AddFolderView.swift b/Multiplatform/Shared/Add/AddFolderView.swift deleted file mode 100644 index 1167096c5..000000000 --- a/Multiplatform/Shared/Add/AddFolderView.swift +++ /dev/null @@ -1,125 +0,0 @@ -// -// AddFolderView.swift -// NetNewsWire -// -// Created by Alex Faber on 04/07/2020. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore - -struct AddFolderView: View { - - @ObservedObject private var viewModel = AddFolderModel() - @Binding var isPresented: Bool - - var body: some View { - #if os(iOS) - iosForm - .onReceive(viewModel.$shouldDismiss, perform: { - dismiss in - if dismiss == true { - isPresented = false - } - }) - #else - macForm - .onReceive(viewModel.$shouldDismiss, perform: { dismiss in - if dismiss == true { - isPresented = false - } - }) - #endif - } - #if os(iOS) - var iosForm: some View { - NavigationView { - Form { - Section { - TextField("Name", text: $viewModel.folderName) - } - Section { - accountPicker - } - } - .navigationTitle("Add Folder") - .navigationBarTitleDisplayMode(.inline) - .navigationBarItems( - leading:Button("Cancel", action: { - isPresented = false - } - ) - .help("Cancel Adding Folder"), - trailing:Button("Add", action: { - viewModel.addFolder() - } - ) - .disabled(viewModel.folderName.isEmpty) - .help("Save Adding Folder") - ) - - } - } - #endif - - #if os(macOS) - var macForm: some View { - Form { - HStack { - Spacer() - Image(rsImage: AppAssets.faviconTemplateImage) - .resizable() - .renderingMode(.template) - .frame(width: 30, height: 30) - Text("Add a Folder") - .font(.title) - Spacer() - } - - LazyVGrid(columns: [GridItem(.fixed(75), spacing: 10, alignment: .trailing),GridItem(.fixed(400), spacing: 0, alignment: .leading) ], alignment: .leading, spacing: 10, pinnedViews: [], content:{ - Text("Name:").bold() - TextField("Name", text: $viewModel.folderName) - .textFieldStyle(RoundedBorderTextFieldStyle()) - .help("The name of the folder you want to create") - Text("Account:").bold() - accountPicker - .help("Pick the account you want to create a folder in.") - }) - buttonStack - } - .frame(maxWidth: 485) - .padding(12) - } - #endif - - var accountPicker: some View { - Picker("Account:", selection: $viewModel.selectedAccountIndex, content: { - ForEach(0.. AccountAndFolderSpecifier? { - if let account = container as? Account { - return AccountAndFolderSpecifier(account: account, folder: nil) - } - if let folder = container as? Folder, let account = folder.account { - return AccountAndFolderSpecifier(account: account, folder: folder) - } - return nil - } - - func addWebFeed() { - if let account = accountAndFolderFromContainer(containers[selectedFolderIndex])?.account { - - showProgressIndicator = true - - let normalizedURLString = providedURL.normalizedURL - - guard !normalizedURLString.isEmpty, let url = URL(string: normalizedURLString) else { - showProgressIndicator = false - return - } - - let container = containers[selectedFolderIndex] - - if account.hasWebFeed(withURL: normalizedURLString) { - addFeedError = .alreadySubscribed - showProgressIndicator = false - return - } - - account.createWebFeed(url: url.absoluteString, name: providedName, container: container, validateFeed: true, completion: { [weak self] result in - self?.showProgressIndicator = false - switch result { - case .success(let feed): - NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.webFeed: feed]) - self?.shouldDismiss = true - case .failure(let error): - switch error { - case AccountError.createErrorAlreadySubscribed: - self?.addFeedError = .alreadySubscribed - return - case AccountError.createErrorNotFound: - self?.addFeedError = .noFeeds - return - default: - print("Error") - } - } - }) - } - } - - func smallIconImage(for container: Container) -> RSImage? { - if let smallIconProvider = container as? SmallIconProvider { - return smallIconProvider.smallIcon?.image - } - return nil - } - -} diff --git a/Multiplatform/Shared/Add/AddWebFeedView.swift b/Multiplatform/Shared/Add/AddWebFeedView.swift deleted file mode 100644 index e898312e0..000000000 --- a/Multiplatform/Shared/Add/AddWebFeedView.swift +++ /dev/null @@ -1,209 +0,0 @@ -// -// AddWebFeedView.swift -// NetNewsWire -// -// Created by Stuart Breckenridge on 3/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore - - -struct AddWebFeedView: View { - - @StateObject private var viewModel = AddWebFeedModel() - @Binding var isPresented: Bool - - var body: some View { - #if os(iOS) - iosForm - .onAppear { - viewModel.pasteUrlFromPasteboard() - } - .onReceive(viewModel.$shouldDismiss, perform: { dismiss in - if dismiss == true { - isPresented = false - } - }) - #else - macForm - .onAppear { - viewModel.pasteUrlFromPasteboard() - }.alert(isPresented: $viewModel.showError) { - Alert(title: Text("Oops"), - message: Text(viewModel.addFeedError!.localizedDescription), - dismissButton: Alert.Button.cancel({ - viewModel.addFeedError = AddWebFeedError.none - })) - } - .onChange(of: viewModel.shouldDismiss, perform: { dismiss in - if dismiss == true { - isPresented = false - } - }) - #endif - } - - #if os(macOS) - var macForm: some View { - Form { - HStack { - Spacer() - Image(rsImage: AppAssets.faviconTemplateImage) - .resizable() - .renderingMode(.template) - .frame(width: 30, height: 30) - Text("Add a Web Feed") - .font(.title) - Spacer() - }.padding() - - LazyVGrid(columns: [GridItem(.fixed(75), spacing: 10, alignment: .trailing),GridItem(.fixed(400), spacing: 0, alignment: .leading) ], alignment: .leading, spacing: 10, pinnedViews: [], content:{ - Text("URL:").bold() - urlTextField - .textFieldStyle(RoundedBorderTextFieldStyle()) - .help("The URL of the feed you want to add.") - Text("Name:").bold() - providedNameTextField - .textFieldStyle(RoundedBorderTextFieldStyle()) - .help("The name of the feed. (Optional.)") - Text("Folder:").bold() - folderPicker - .help("Pick the folder you want to add the feed to.") - }) - buttonStack - } - .frame(maxWidth: 485) - .padding(12) - } - #endif - - #if os(iOS) - var iosForm: some View { - NavigationView { - List { - urlTextField - providedNameTextField - folderPicker - } - .listStyle(InsetGroupedListStyle()) - .navigationBarTitle("Add Web Feed") - .navigationBarTitleDisplayMode(.inline) - .navigationBarItems(leading: - Button("Cancel", action: { - isPresented = false - }) - .help("Cancel Add Feed") - , trailing: - HStack(spacing: 12) { - if viewModel.showProgressIndicator == true { - ProgressView() - } - Button("Add", action: { - viewModel.addWebFeed() - }) - .disabled(!viewModel.providedURL.mayBeURL) - .help("Add Feed") - } - ) - } - } - #endif - - @ViewBuilder var urlTextField: some View { - #if os(iOS) - TextField("URL", text: $viewModel.providedURL) - .disableAutocorrection(true) - .autocapitalization(UITextAutocapitalizationType.none) - #else - TextField("URL", text: $viewModel.providedURL) - .disableAutocorrection(true) - #endif - } - - var providedNameTextField: some View { - TextField("Title (Optional)", text: $viewModel.providedName) - } - - @ViewBuilder var folderPicker: some View { - #if os(iOS) - Picker("Folder", selection: $viewModel.selectedFolderIndex, content: { - ForEach(0.. RSImage? { - switch accountType { - case .onMyMac: - #if os(macOS) - return AppAssets.accountLocalMacImage - #endif - #if os(iOS) - if UIDevice.current.userInterfaceIdiom == .pad { - return AppAssets.accountLocalPadImage - } else { - return AppAssets.accountLocalPhoneImage - } - #endif - case .bazQux: - return AppAssets.accountBazQux - case .cloudKit: - return AppAssets.accountCloudKitImage - case .feedbin: - return AppAssets.accountFeedbinImage - case .feedly: - return AppAssets.accountFeedlyImage - case .feedWrangler: - return AppAssets.accountFeedWranglerImage - case .freshRSS: - return AppAssets.accountFreshRSSImage - case .newsBlur: - return AppAssets.accountNewsBlurImage - case .inoreader: - return AppAssets.accountInoreader - case .theOldReader: - return AppAssets.accountTheOldReader - - } - } - -} diff --git a/Multiplatform/Shared/AppDefaults.swift b/Multiplatform/Shared/AppDefaults.swift deleted file mode 100644 index 861036520..000000000 --- a/Multiplatform/Shared/AppDefaults.swift +++ /dev/null @@ -1,353 +0,0 @@ -// -// AppDefaults.swift -// NetNewsWire -// -// Created by Stuart Breckenridge on 1/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import SwiftUI - -enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable { - case automatic = 0 - case light = 1 - case dark = 2 - - var description: String { - switch self { - case .automatic: - return NSLocalizedString("Automatic", comment: "Automatic") - case .light: - return NSLocalizedString("Light", comment: "Light") - case .dark: - return NSLocalizedString("Dark", comment: "Dark") - } - } -} - -final class AppDefaults: ObservableObject { - - #if os(macOS) - static let store: UserDefaults = UserDefaults.standard - #endif - - #if os(iOS) - static let store: UserDefaults = { - let appIdentifierPrefix = Bundle.main.object(forInfoDictionaryKey: "AppIdentifierPrefix") as! String - let suiteName = "\(appIdentifierPrefix)group.\(Bundle.main.bundleIdentifier!)" - return UserDefaults.init(suiteName: suiteName)! - }() - #endif - - public static let shared = AppDefaults() - private init() {} - - struct Key { - - // Shared Defaults - static let refreshInterval = "refreshInterval" - static let hideDockUnreadCount = "JustinMillerHideDockUnreadCount" - static let activeExtensionPointIDs = "activeExtensionPointIDs" - static let lastImageCacheFlushDate = "lastImageCacheFlushDate" - static let firstRunDate = "firstRunDate" - static let lastRefresh = "lastRefresh" - static let addWebFeedAccountID = "addWebFeedAccountID" - static let addWebFeedFolderName = "addWebFeedFolderName" - static let addFolderAccountID = "addFolderAccountID" - - static let userInterfaceColorPalette = "userInterfaceColorPalette" - static let timelineSortDirection = "timelineSortDirection" - static let timelineGroupByFeed = "timelineGroupByFeed" - static let timelineIconDimensions = "timelineIconDimensions" - static let timelineNumberOfLines = "timelineNumberOfLines" - - // Sidebar Defaults - static let sidebarConfirmDelete = "sidebarConfirmDelete" - - // iOS Defaults - static let refreshClearsReadArticles = "refreshClearsReadArticles" - static let articleFullscreenAvailable = "articleFullscreenAvailable" - static let articleFullscreenEnabled = "articleFullscreenEnabled" - static let confirmMarkAllAsRead = "confirmMarkAllAsRead" - - // macOS Defaults - static let articleTextSize = "articleTextSize" - static let openInBrowserInBackground = "openInBrowserInBackground" - static let defaultBrowserID = "defaultBrowserID" - static let subscribeToFeedsInDefaultBrowser = "subscribeToFeedsInDefaultBrowser" - static let checkForUpdatesAutomatically = "checkForUpdatesAutomatically" - static let downloadTestBuilds = "downloadTestBuild" - static let sendCrashLogs = "sendCrashLogs" - - // Hidden macOS Defaults - static let showDebugMenu = "ShowDebugMenu" - static let timelineShowsSeparators = "CorreiaSeparators" - static let showTitleOnMainWindow = "KafasisTitleMode" - - #if !MAC_APP_STORE - static let webInspectorEnabled = "WebInspectorEnabled" - static let webInspectorStartsAttached = "__WebInspectorPageGroupLevel1__.WebKit2InspectorStartsAttached" - #endif - - } - - // MARK: Development Builds - let isDeveloperBuild: Bool = { - if let dev = Bundle.main.object(forInfoDictionaryKey: "DeveloperEntitlements") as? String, dev == "-dev" { - return true - } - return false - }() - - // MARK: First Run Details - var firstRunDate: Date? { - set { - AppDefaults.store.setValue(newValue, forKey: Key.firstRunDate) - objectWillChange.send() - } - get { - AppDefaults.store.object(forKey: Key.firstRunDate) as? Date - } - } - - // MARK: Refresh Interval - @AppStorage(wrappedValue: 4, Key.refreshInterval, store: store) var interval: Int { - didSet { - objectWillChange.send() - } - } - - var refreshInterval: RefreshInterval { - RefreshInterval(rawValue: interval) ?? RefreshInterval.everyHour - } - - // MARK: Dock Badge - @AppStorage(wrappedValue: false, Key.hideDockUnreadCount, store: store) var hideDockUnreadCount { - didSet { - objectWillChange.send() - } - } - - // MARK: Color Palette - var userInterfaceColorPalette: UserInterfaceColorPalette { - get { - if let palette = UserInterfaceColorPalette(rawValue: AppDefaults.store.integer(forKey: Key.userInterfaceColorPalette)) { - return palette - } - return .automatic - } - set { - AppDefaults.store.set(newValue.rawValue, forKey: Key.userInterfaceColorPalette) - #if os(macOS) - self.objectWillChange.send() - #else - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { - self.objectWillChange.send() - }) - #endif - } - } - - static var userInterfaceColorScheme: ColorScheme? { - switch AppDefaults.shared.userInterfaceColorPalette { - case .light: - return ColorScheme.light - case .dark: - return ColorScheme.dark - default: - return nil - } - } - - // MARK: Feeds & Folders - @AppStorage(Key.addWebFeedAccountID, store: store) var addWebFeedAccountID: String? - - @AppStorage(Key.addWebFeedFolderName, store: store) var addWebFeedFolderName: String? - - @AppStorage(Key.addFolderAccountID, store: store) var addFolderAccountID: String? - - @AppStorage(wrappedValue: false, Key.confirmMarkAllAsRead, store: store) var confirmMarkAllAsRead: Bool - - // MARK: Extension Points - var activeExtensionPointIDs: [[AnyHashable : AnyHashable]]? { - get { - return AppDefaults.store.object(forKey: Key.activeExtensionPointIDs) as? [[AnyHashable : AnyHashable]] - } - set { - UserDefaults.standard.set(newValue, forKey: Key.activeExtensionPointIDs) - objectWillChange.send() - } - } - - // MARK: Image Cache - var lastImageCacheFlushDate: Date? { - set { - AppDefaults.store.setValue(newValue, forKey: Key.lastImageCacheFlushDate) - objectWillChange.send() - } - get { - AppDefaults.store.object(forKey: Key.lastImageCacheFlushDate) as? Date - } - } - - // MARK: Timeline - @AppStorage(wrappedValue: false, Key.timelineGroupByFeed, store: store) var timelineGroupByFeed: Bool { - didSet { - objectWillChange.send() - } - } - - @AppStorage(wrappedValue: 2.0, Key.timelineNumberOfLines, store: store) var timelineNumberOfLines: Double { - didSet { - objectWillChange.send() - } - } - - @AppStorage(wrappedValue: 40.0, Key.timelineIconDimensions, store: store) var timelineIconDimensions: Double { - didSet { - objectWillChange.send() - } - } - - /// Set to `true` to sort oldest to newest, `false` for newest to oldest. Default is `false`. - @AppStorage(wrappedValue: false, Key.timelineSortDirection, store: store) var timelineSortDirection: Bool { - didSet { - objectWillChange.send() - } - } - - // MARK: Sidebar - @AppStorage(wrappedValue: true, Key.sidebarConfirmDelete, store: store) var sidebarConfirmDelete: Bool { - didSet { - objectWillChange.send() - } - } - - - // MARK: Refresh - @AppStorage(wrappedValue: false, Key.refreshClearsReadArticles, store: store) var refreshClearsReadArticles: Bool - - // MARK: Articles - @AppStorage(wrappedValue: false, Key.articleFullscreenAvailable, store: store) var articleFullscreenAvailable: Bool - - @AppStorage(wrappedValue: false, Key.articleFullscreenEnabled, store: store) var articleFullscreenEnabled: Bool - - @AppStorage(wrappedValue: 3, Key.articleTextSize, store: store) var articleTextSizeTag: Int { - didSet { - objectWillChange.send() - } - } - - var articleTextSize: ArticleTextSize { - ArticleTextSize(rawValue: articleTextSizeTag) ?? ArticleTextSize.large - } - - // MARK: Refresh - var lastRefresh: Date? { - set { - AppDefaults.store.setValue(newValue, forKey: Key.lastRefresh) - objectWillChange.send() - } - get { - AppDefaults.store.object(forKey: Key.lastRefresh) as? Date - } - } - - // MARK: Window State - @AppStorage(wrappedValue: false, Key.openInBrowserInBackground, store: store) var openInBrowserInBackground: Bool { - didSet { - objectWillChange.send() - } - } - - @AppStorage(Key.defaultBrowserID, store: store) var defaultBrowserID: String? { - didSet { - objectWillChange.send() - } - } - - @AppStorage(wrappedValue: false, Key.subscribeToFeedsInDefaultBrowser, store: store) var subscribeToFeedsInDefaultBrowser: Bool { - didSet { - objectWillChange.send() - } - } - - @AppStorage(Key.showTitleOnMainWindow, store: store) var showTitleOnMainWindow: Bool? { - didSet { - objectWillChange.send() - } - } - - @AppStorage(wrappedValue: false, Key.showDebugMenu, store: store) var showDebugMenu: Bool { - didSet { - objectWillChange.send() - } - } - - @AppStorage(wrappedValue: false, Key.timelineShowsSeparators, store: store) var timelineShowsSeparators: Bool { - didSet { - objectWillChange.send() - } - } - - #if !MAC_APP_STORE - @AppStorage(wrappedValue: false, Key.webInspectorEnabled, store: store) var webInspectorEnabled: Bool { - didSet { - objectWillChange.send() - } - } - - @AppStorage(wrappedValue: false, Key.webInspectorStartsAttached, store: store) var webInspectorStartsAttached: Bool { - didSet { - objectWillChange.send() - } - } - #endif - - @AppStorage(wrappedValue: true, Key.checkForUpdatesAutomatically, store: store) var checkForUpdatesAutomatically: Bool { - didSet { - objectWillChange.send() - } - } - - @AppStorage(wrappedValue: false, Key.downloadTestBuilds, store: store) var downloadTestBuilds: Bool { - didSet { - objectWillChange.send() - } - } - - @AppStorage(wrappedValue: true, Key.sendCrashLogs, store: store) var sendCrashLogs: Bool { - didSet { - objectWillChange.send() - } - } - - static func registerDefaults() { - let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue, - Key.timelineGroupByFeed: false, - Key.refreshClearsReadArticles: false, - Key.timelineNumberOfLines: 2, - Key.timelineIconDimensions: 40, - Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, - Key.articleFullscreenAvailable: false, - Key.articleFullscreenEnabled: false, - Key.confirmMarkAllAsRead: true, - "NSScrollViewShouldScrollUnderTitlebar": false, - Key.refreshInterval: RefreshInterval.everyHour.rawValue] - AppDefaults.store.register(defaults: defaults) - } - -} - -extension AppDefaults { - - func isFirstRun() -> Bool { - if let _ = AppDefaults.store.object(forKey: Key.firstRunDate) as? Date { - return false - } - firstRunDate = Date() - return true - } - -} diff --git a/Multiplatform/Shared/Article/ArticleContainerView.swift b/Multiplatform/Shared/Article/ArticleContainerView.swift deleted file mode 100644 index 07fa53b72..000000000 --- a/Multiplatform/Shared/Article/ArticleContainerView.swift +++ /dev/null @@ -1,19 +0,0 @@ -// -// ArticleContainerView.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/2/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Articles - -struct ArticleContainerView: View { - - var body: some View { - ArticleView() - .modifier(ArticleToolbarModifier()) - } - -} diff --git a/Multiplatform/Shared/Article/ArticleExtractorButtonState.swift b/Multiplatform/Shared/Article/ArticleExtractorButtonState.swift deleted file mode 100644 index 6c9f6d04a..000000000 --- a/Multiplatform/Shared/Article/ArticleExtractorButtonState.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// ArticleExtractorButtonState.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation - -enum ArticleExtractorButtonState { - case error - case animated - case on - case off -} diff --git a/Multiplatform/Shared/Article/ArticleIconSchemeHandler.swift b/Multiplatform/Shared/Article/ArticleIconSchemeHandler.swift deleted file mode 100644 index 4e18cccb7..000000000 --- a/Multiplatform/Shared/Article/ArticleIconSchemeHandler.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// ArticleIconSchemeHandler.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import WebKit -import Articles - -class ArticleIconSchemeHandler: NSObject, WKURLSchemeHandler { - - weak var sceneModel: SceneModel? - - init(sceneModel: SceneModel) { - self.sceneModel = sceneModel - } - - func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) { - - guard let url = urlSchemeTask.request.url, let sceneModel = sceneModel else { - urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) - return - } - - guard let components = URLComponents(url: url, resolvingAgainstBaseURL: false) else { - return - } - let articleID = components.path - guard let iconImage = sceneModel.articleFor(articleID)?.iconImage() else { - urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) - return - } - - let iconView = IconView(frame: CGRect(x: 0, y: 0, width: 48, height: 48)) - iconView.iconImage = iconImage - let renderedImage = iconView.asImage() - - guard let data = renderedImage.dataRepresentation() else { - urlSchemeTask.didFailWithError(URLError(.fileDoesNotExist)) - return - } - - let headerFields = ["Cache-Control": "no-cache"] - if let response = HTTPURLResponse(url: url, statusCode: 200, httpVersion: nil, headerFields: headerFields) { - urlSchemeTask.didReceive(response) - urlSchemeTask.didReceive(data) - urlSchemeTask.didFinish() - } - - } - - func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) { - urlSchemeTask.didFailWithError(URLError(.unknown)) - } - -} - diff --git a/Multiplatform/Shared/Article/ArticleToolbarModifier.swift b/Multiplatform/Shared/Article/ArticleToolbarModifier.swift deleted file mode 100644 index 380cba519..000000000 --- a/Multiplatform/Shared/Article/ArticleToolbarModifier.swift +++ /dev/null @@ -1,120 +0,0 @@ -// -// ArticleToolbarModifier.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/5/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct ArticleToolbarModifier: ViewModifier { - - @EnvironmentObject private var sceneModel: SceneModel - @State private var showActivityView = false - - func body(content: Content) -> some View { - content - .toolbar { - #if os(iOS) - - ToolbarItem(placement: .primaryAction) { - HStack(spacing: 20) { - Button { - } label: { - AppAssets.prevArticleImage - .font(.title3) - } - .help("Previouse Unread") - Button { - } label: { - AppAssets.nextArticleImage.font(.title3) - } - .help("Next Unread") - } - } - - ToolbarItem(placement: .bottomBar) { - Button { - sceneModel.toggleReadStatusForSelectedArticles() - } label: { - if sceneModel.readButtonState == true { - AppAssets.readClosedImage - } else { - AppAssets.readOpenImage - } - } - .disabled(sceneModel.readButtonState == nil) - .help(sceneModel.readButtonState ?? false ? "Mark as Unread" : "Mark as Read") - } - - ToolbarItem(placement: .bottomBar) { - Spacer() - } - - ToolbarItem(placement: .bottomBar) { - Button { - sceneModel.toggleStarredStatusForSelectedArticles() - } label: { - if sceneModel.starButtonState ?? false { - AppAssets.starClosedImage - } else { - AppAssets.starOpenImage - } - } - .disabled(sceneModel.starButtonState == nil) - .help(sceneModel.starButtonState ?? false ? "Mark as Unstarred" : "Mark as Starred") - } - - ToolbarItem(placement: .bottomBar) { - Spacer() - } - - ToolbarItem(placement: .bottomBar) { - Button { - sceneModel.goToNextUnread() - } label: { - AppAssets.nextUnreadArticleImage.font(.title3) - } - .disabled(sceneModel.nextUnreadButtonState == nil) - .help("Next Unread") - } - - ToolbarItem(placement: .bottomBar) { - Spacer() - } - - ToolbarItem(placement: .bottomBar) { - Button { - } label: { - AppAssets.articleExtractorOff - .font(.title3) - } - .disabled(sceneModel.extractorButtonState == nil) - .help("Reader View") - } - - ToolbarItem(placement: .bottomBar) { - Spacer() - } - - ToolbarItem(placement: .bottomBar) { - Button { - showActivityView.toggle() - } label: { - AppAssets.shareImage.font(.title3) - } - .disabled(sceneModel.shareButtonState == nil) - .help("Share") - .sheet(isPresented: $showActivityView) { - if let article = sceneModel.selectedArticles.first, let url = article.preferredURL { - ActivityViewController(title: article.title, url: url) - } - } - } - - #endif - } - } - -} diff --git a/Multiplatform/Shared/Article/PreloadedWebView.swift b/Multiplatform/Shared/Article/PreloadedWebView.swift deleted file mode 100644 index 2baca9e73..000000000 --- a/Multiplatform/Shared/Article/PreloadedWebView.swift +++ /dev/null @@ -1,132 +0,0 @@ -// -// PreloadedWebView.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import WebKit -import RSWeb - -class PreloadedWebView: WKWebView { - - private var isReady: Bool = false - private var readyCompletion: (() -> Void)? - - init(articleIconSchemeHandler: ArticleIconSchemeHandler) { - let preferences = WKPreferences() - preferences.javaScriptCanOpenWindowsAutomatically = false - - let configuration = WKWebViewConfiguration() - configuration.preferences = preferences - configuration.setValue(true, forKey: "allowUniversalAccessFromFileURLs") - #if os(iOS) - configuration.allowsInlineMediaPlayback = true - #endif - configuration.mediaTypesRequiringUserActionForPlayback = .audio - configuration.setURLSchemeHandler(articleIconSchemeHandler, forURLScheme: ArticleRenderer.imageIconScheme) - - super.init(frame: .zero, configuration: configuration) - - if let userAgent = UserAgent.fromInfoPlist() { - customUserAgent = userAgent - } - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - } - - func preload() { - navigationDelegate = self - loadFileURL(ArticleRenderer.blank.url, allowingReadAccessTo: ArticleRenderer.blank.baseURL) - } - - func ready(completion: @escaping () -> Void) { - if isReady { - completeRequest(completion: completion) - } else { - readyCompletion = completion - } - } - - #if os(macOS) - override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) { - // There’s no API for affecting a WKWebView’s contextual menu. - // (WebView had API for this.) - // - // This a minor hack. It hides unwanted menu items. - // The menu item identifiers are not documented anywhere; - // they could change, and this code would need updating. - for menuItem in menu.items { - if shouldHideMenuItem(menuItem) { - menuItem.isHidden = true - } - } - - super.willOpenMenu(menu, with: event) - } - #endif -} - -// MARK: WKScriptMessageHandler - -extension PreloadedWebView: WKNavigationDelegate { - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - isReady = true - if let completion = readyCompletion { - completeRequest(completion: completion) - readyCompletion = nil - } - } - -} - -// MARK: Private - -private extension PreloadedWebView { - - func completeRequest(completion: @escaping () -> Void) { - isReady = false - navigationDelegate = nil - completion() - } - -} - -#if os(macOS) -private extension NSUserInterfaceItemIdentifier { - - static let DetailMenuItemIdentifierReload = NSUserInterfaceItemIdentifier(rawValue: "WKMenuItemIdentifierReload") - static let DetailMenuItemIdentifierOpenLink = NSUserInterfaceItemIdentifier(rawValue: "WKMenuItemIdentifierOpenLink") -} - -private extension PreloadedWebView { - - static let menuItemIdentifiersToHide: [NSUserInterfaceItemIdentifier] = [.DetailMenuItemIdentifierReload, .DetailMenuItemIdentifierOpenLink] - static let menuItemIdentifierMatchStrings = ["newwindow", "download"] - - func shouldHideMenuItem(_ menuItem: NSMenuItem) -> Bool { - - guard let identifier = menuItem.identifier else { - return false - } - - if PreloadedWebView.menuItemIdentifiersToHide.contains(identifier) { - return true - } - - let lowerIdentifier = identifier.rawValue.lowercased() - for matchString in PreloadedWebView.menuItemIdentifierMatchStrings { - if lowerIdentifier.contains(matchString) { - return true - } - } - - return false - } -} -#endif diff --git a/Multiplatform/Shared/Article/WebViewProvider.swift b/Multiplatform/Shared/Article/WebViewProvider.swift deleted file mode 100644 index 64d834c0c..000000000 --- a/Multiplatform/Shared/Article/WebViewProvider.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// WebViewProvider.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import RSCore -import WebKit - -/// WKWebView has an awful behavior of a flash to white on first load when in dark mode. -/// Keep a queue of WebViews where we've already done a trivial load so that by the time we need them in the UI, they're past the flash-to-shite part of their lifecycle. -class WebViewProvider: NSObject { - - private let articleIconSchemeHandler: ArticleIconSchemeHandler - private let operationQueue = MainThreadOperationQueue() - private var queue = NSMutableArray() - - init(articleIconSchemeHandler: ArticleIconSchemeHandler) { - self.articleIconSchemeHandler = articleIconSchemeHandler - super.init() - replenishQueueIfNeeded() - } - - func replenishQueueIfNeeded() { - operationQueue.add(WebViewProviderReplenishQueueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler)) - } - - func dequeueWebView(completion: @escaping (PreloadedWebView) -> ()) { - operationQueue.add(WebViewProviderDequeueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler, completion: completion)) - operationQueue.add(WebViewProviderReplenishQueueOperation(queue: queue, articleIconSchemeHandler: articleIconSchemeHandler)) - } - -} - -class WebViewProviderReplenishQueueOperation: MainThreadOperation { - - // MainThreadOperation - public var isCanceled = false - public var id: Int? - public weak var operationDelegate: MainThreadOperationDelegate? - public var name: String? = "WebViewProviderReplenishQueueOperation" - public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? - - private let minimumQueueDepth = 3 - - private var queue: NSMutableArray - private var articleIconSchemeHandler: ArticleIconSchemeHandler - - init(queue: NSMutableArray, articleIconSchemeHandler: ArticleIconSchemeHandler) { - self.queue = queue - self.articleIconSchemeHandler = articleIconSchemeHandler - } - - func run() { - while queue.count < minimumQueueDepth { - let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler) - webView.preload() - queue.insert(webView, at: 0) - } - self.operationDelegate?.operationDidComplete(self) - } - -} - -class WebViewProviderDequeueOperation: MainThreadOperation { - - // MainThreadOperation - public var isCanceled = false - public var id: Int? - public weak var operationDelegate: MainThreadOperationDelegate? - public var name: String? = "WebViewProviderFlushQueueOperation" - public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? - - private var queue: NSMutableArray - private var articleIconSchemeHandler: ArticleIconSchemeHandler - private var completion: (PreloadedWebView) -> () - - init(queue: NSMutableArray, articleIconSchemeHandler: ArticleIconSchemeHandler, completion: @escaping (PreloadedWebView) -> ()) { - self.queue = queue - self.articleIconSchemeHandler = articleIconSchemeHandler - self.completion = completion - } - - func run() { - if let webView = queue.lastObject as? PreloadedWebView { - self.completion(webView) - self.queue.remove(webView) - self.operationDelegate?.operationDidComplete(self) - return - } - - assertionFailure("Creating PreloadedWebView in \(#function); queue has run dry.") - - let webView = PreloadedWebView(articleIconSchemeHandler: articleIconSchemeHandler) - webView.preload() - self.completion(webView) - self.operationDelegate?.operationDidComplete(self) - } - -} diff --git a/Multiplatform/Shared/Article/WrapperScriptMessageHandler.swift b/Multiplatform/Shared/Article/WrapperScriptMessageHandler.swift deleted file mode 100644 index e3a58e19d..000000000 --- a/Multiplatform/Shared/Article/WrapperScriptMessageHandler.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// WrapperScriptMessageHandler.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import WebKit - -class WrapperScriptMessageHandler: NSObject, WKScriptMessageHandler { - - // We need to wrap a message handler to prevent a circlular reference - private weak var handler: WKScriptMessageHandler? - - init(_ handler: WKScriptMessageHandler) { - self.handler = handler - } - - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - handler?.userContentController(userContentController, didReceive: message) - } - -} diff --git a/Multiplatform/Shared/Article/blank.html b/Multiplatform/Shared/Article/blank.html deleted file mode 100644 index 6e02cf3a6..000000000 --- a/Multiplatform/Shared/Article/blank.html +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - diff --git a/Multiplatform/Shared/Article/main_multiplatform.js b/Multiplatform/Shared/Article/main_multiplatform.js deleted file mode 100644 index e1f0f2d0a..000000000 --- a/Multiplatform/Shared/Article/main_multiplatform.js +++ /dev/null @@ -1,498 +0,0 @@ -var activeImageViewer = null; - -class ImageViewer { - constructor(img) { - this.img = img; - this.loadingInterval = null; - this.activityIndicator = "data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiIHN0YW5kYWxvbmU9Im5vIj8+PHN2ZyB4bWxuczpzdmc9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiB2ZXJzaW9uPSIxLjAiIHdpZHRoPSI2NHB4IiBoZWlnaHQ9IjY0cHgiIHZpZXdCb3g9IjAgMCAxMjggMTI4IiB4bWw6c3BhY2U9InByZXNlcnZlIj48Zz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMwMDAwMDAiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDMwIDY0IDY0KSIvPjxwYXRoIGQ9Ik01OS42IDBoOHY0MGgtOFYweiIgZmlsbD0iI2NjY2NjYyIgdHJhbnNmb3JtPSJyb3RhdGUoNjAgNjQgNjQpIi8+PHBhdGggZD0iTTU5LjYgMGg4djQwaC04VjB6IiBmaWxsPSIjY2NjY2NjIiB0cmFuc2Zvcm09InJvdGF0ZSg5MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNjY2NjY2MiIHRyYW5zZm9ybT0icm90YXRlKDEyMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiNiMmIyYjIiIHRyYW5zZm9ybT0icm90YXRlKDE1MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM5OTk5OTkiIHRyYW5zZm9ybT0icm90YXRlKDE4MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM3ZjdmN2YiIHRyYW5zZm9ybT0icm90YXRlKDIxMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM2NjY2NjYiIHRyYW5zZm9ybT0icm90YXRlKDI0MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiM0YzRjNGMiIHRyYW5zZm9ybT0icm90YXRlKDI3MCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMzMzMzMzMiIHRyYW5zZm9ybT0icm90YXRlKDMwMCA2NCA2NCkiLz48cGF0aCBkPSJNNTkuNiAwaDh2NDBoLThWMHoiIGZpbGw9IiMxOTE5MTkiIHRyYW5zZm9ybT0icm90YXRlKDMzMCA2NCA2NCkiLz48YW5pbWF0ZVRyYW5zZm9ybSBhdHRyaWJ1dGVOYW1lPSJ0cmFuc2Zvcm0iIHR5cGU9InJvdGF0ZSIgdmFsdWVzPSIwIDY0IDY0OzMwIDY0IDY0OzYwIDY0IDY0OzkwIDY0IDY0OzEyMCA2NCA2NDsxNTAgNjQgNjQ7MTgwIDY0IDY0OzIxMCA2NCA2NDsyNDAgNjQgNjQ7MjcwIDY0IDY0OzMwMCA2NCA2NDszMzAgNjQgNjQiIGNhbGNNb2RlPSJkaXNjcmV0ZSIgZHVyPSIxMDgwbXMiIHJlcGVhdENvdW50PSJpbmRlZmluaXRlIj48L2FuaW1hdGVUcmFuc2Zvcm0+PC9nPjwvc3ZnPg=="; - } - - isLoaded() { - return this.img.classList.contains("nnwLoaded"); - } - - clicked() { - this.showLoadingIndicator(); - if (this.isLoaded()) { - this.showViewer(); - } else { - var callback = () => { - if (this.isLoaded()) { - clearInterval(this.loadingInterval); - this.showViewer(); - } - } - this.loadingInterval = setInterval(callback, 100); - } - } - cancel() { - clearInterval(this.loadingInterval); - this.hideLoadingIndicator(); - } - - showViewer() { - this.hideLoadingIndicator(); - - var canvas = document.createElement("canvas"); - var pixelRatio = window.devicePixelRatio; - do { - canvas.width = this.img.naturalWidth * pixelRatio; - canvas.height = this.img.naturalHeight * pixelRatio; - pixelRatio--; - } while (pixelRatio > 0 && canvas.width * canvas.height > 16777216) - canvas.getContext("2d").drawImage(this.img, 0, 0, canvas.width, canvas.height); - - const rect = this.img.getBoundingClientRect(); - const message = { - x: rect.x, - y: rect.y, - width: rect.width, - height: rect.height, - imageTitle: this.img.title, - imageURL: canvas.toDataURL(), - }; - - var jsonMessage = JSON.stringify(message); - window.webkit.messageHandlers.imageWasClicked.postMessage(jsonMessage); - } - - hideImage() { - this.img.style.opacity = 0; - } - - showImage() { - this.img.style.opacity = 1 - } - - showLoadingIndicator() { - var wrapper = document.createElement("div"); - wrapper.classList.add("activityIndicatorWrap"); - this.img.parentNode.insertBefore(wrapper, this.img); - wrapper.appendChild(this.img); - - var activityIndicatorImg = document.createElement("img"); - activityIndicatorImg.classList.add("activityIndicator"); - activityIndicatorImg.style.opacity = 0; - activityIndicatorImg.src = this.activityIndicator; - wrapper.appendChild(activityIndicatorImg); - - activityIndicatorImg.style.opacity = 1; - } - - hideLoadingIndicator() { - var wrapper = this.img.parentNode; - if (wrapper.classList.contains("activityIndicatorWrap")) { - var wrapperParent = wrapper.parentNode; - wrapperParent.insertBefore(this.img, wrapper); - wrapperParent.removeChild(wrapper); - } - } - - static init() { - cancelImageLoad(); - - // keep track of when an image has finished downloading for ImageViewer - document.querySelectorAll("img").forEach(element => { - element.onload = function() { - this.classList.add("nnwLoaded"); - } - }); - - // Add the click listener for images - window.onclick = function(event) { - if (event.target.matches("img") && !event.target.classList.contains("nnw-nozoom")) { - if (activeImageViewer && activeImageViewer.img === event.target) { - cancelImageLoad(); - } else { - cancelImageLoad(); - activeImageViewer = new ImageViewer(event.target); - activeImageViewer.clicked(); - } - } - } - } -} - -function cancelImageLoad() { - if (activeImageViewer) { - activeImageViewer.cancel(); - activeImageViewer = null; - } -} - -function hideClickedImage() { - if (activeImageViewer) { - activeImageViewer.hideImage(); - } -} - -// Used to animate the transition from a fullscreen image -function showClickedImage() { - if (activeImageViewer) { - activeImageViewer.showImage(); - } - window.webkit.messageHandlers.imageWasShown.postMessage(""); -} - -function showFeedInspectorSetup() { - document.getElementById("nnwImageIcon").onclick = function(event) { - window.webkit.messageHandlers.showFeedInspector.postMessage(""); - } -} - -function linkHover() { - window.onmouseover = function(event) { - var closestAnchor = event.target.closest('a') - if (closestAnchor) { - window.webkit.messageHandlers.mouseDidEnter.postMessage(closestAnchor.href); - } - } - window.onmouseout = function(event) { - var closestAnchor = event.target.closest('a') - if (closestAnchor) { - window.webkit.messageHandlers.mouseDidExit.postMessage(closestAnchor.href); - } - } -} - - -function postRenderProcessing() { - ImageViewer.init(); - showFeedInspectorSetup(); - linkHover(); -} - - -function makeHighlightRect({left, top, width, height}, offsetTop=0, offsetLeft=0) { - const overlay = document.createElement('a'); - - Object.assign(overlay.style, { - position: 'absolute', - left: `${Math.floor(left + offsetLeft)}px`, - top: `${Math.floor(top + offsetTop)}px`, - width: `${Math.ceil(width)}px`, - height: `${Math.ceil(height)}px`, - backgroundColor: 'rgba(200, 220, 10, 0.4)', - pointerEvents: 'none' - }); - - return overlay; -} - -function clearHighlightRects() { - let container = document.getElementById('nnw:highlightContainer') - if (container) container.remove(); -} - -function highlightRects(rects, clearOldRects=true, makeHighlightRect=makeHighlightRect) { - const article = document.querySelector('article'); - let container = document.getElementById('nnw:highlightContainer'); - - article.style.position = 'relative'; - - if (container && clearOldRects) - container.remove(); - - container = document.createElement('div'); - container.id = 'nnw:highlightContainer'; - article.appendChild(container); - - const {top, left} = article.getBoundingClientRect(); - return Array.from(rects, rect => - container.appendChild(makeHighlightRect(rect, -top, -left)) - ); -} - -FinderResult = class { - constructor(result) { - Object.assign(this, result); - } - - range() { - const range = document.createRange(); - range.setStart(this.node, this.offset); - range.setEnd(this.node, this.offsetEnd); - return range; - } - - bounds() { - return this.range().getBoundingClientRect(); - } - - rects() { - return this.range().getClientRects(); - } - - highlight({clearOldRects=true, fn=makeHighlightRect} = {}) { - highlightRects(this.rects(), clearOldRects, fn); - } - - scrollTo() { - scrollToRect(this.bounds(), this.node); - } - - toJSON() { - return { - rects: Array.from(this.rects()), - bounds: this.bounds(), - index: this.index, - matchGroups: this.match - }; - } - - toJSONString() { - return JSON.stringify(this.toJSON()); - } -} - -Finder = class { - constructor(pattern, options) { - if (!pattern.global) { - pattern = new RegExp(pattern, 'g'); - } - - this.pattern = pattern; - this.lastResult = null; - this._nodeMatches = []; - this.options = { - rootSelector: '.articleBody', - startNode: null, - startOffset: null, - } - - this.resultIndex = -1 - - Object.assign(this.options, options); - - this.walker = document.createTreeWalker(this.root, NodeFilter.SHOW_TEXT); - } - - get root() { - return document.querySelector(this.options.rootSelector) - } - - get count() { - const node = this.walker.currentNode; - const index = this.resultIndex; - this.reset(); - - let result, count = 0; - while ((result = this.next())) ++count; - - this.resultIndex = index; - this.walker.currentNode = node; - - return count; - } - - reset() { - this.walker.currentNode = this.options.startNode || this.root; - this.resultIndex = -1; - } - - [Symbol.iterator]() { - return this; - } - - next({wrap = false} = {}) { - const { startNode } = this.options; - const { pattern, walker } = this; - - let { node, matchIndex = -1 } = this.lastResult || { node: startNode }; - - while (true) { - if (!node) - node = walker.nextNode(); - - if (!node) { - if (!wrap || this.resultIndex < 0) break; - - this.reset(); - - continue; - } - - let nextIndex = matchIndex + 1; - let matches = this._nodeMatches; - - if (!matches.length) { - matches = Array.from(node.textContent.matchAll(pattern)); - nextIndex = 0; - } - - if (matches[nextIndex]) { - this._nodeMatches = matches; - const m = matches[nextIndex]; - - this.lastResult = new FinderResult({ - node, - offset: m.index, - offsetEnd: m.index + m[0].length, - text: m[0], - match: m, - matchIndex: nextIndex, - index: ++this.resultIndex, - }); - - return { value: this.lastResult, done: false }; - } - - this._nodeMatches = []; - node = null; - } - - return { value: undefined, done: true }; - } - - /// TODO Call when the search text changes - retry() { - if (this.lastResult) { - this.lastResult.offsetEnd = this.lastResult.offset; - } - - } - - toJSON() { - const results = Array.from(this); - } -} - -function scrollParent(node) { - let elt = node.nodeType === Node.ELEMENT_NODE ? node : node.parentElement; - - while (elt) { - if (elt.scrollHeight > elt.clientHeight) - return elt; - elt = elt.parentElement; - } -} - -function scrollToRect({top, height}, node, pad=20, padBottom=60) { - const scrollToTop = top - pad; - - let scrollBy = scrollToTop; - - if (scrollToTop >= 0) { - const visible = window.visualViewport; - const scrollToBottom = top + height + padBottom - visible.height; - // The top of the rect is already in the viewport - if (scrollToBottom <= 0 || scrollToTop === 0) - // Don't need to scroll up--or can't - return; - - scrollBy = Math.min(scrollToBottom, scrollBy); - } - - scrollParent(node).scrollBy({ top: scrollBy }); -} - -function withEncodedArg(fn) { - return function(encodedData, ...rest) { - const data = encodedData && JSON.parse(atob(encodedData)); - return fn(data, ...rest); - } -} - -function escapeRegex(s) { - return s.replace(/[.?*+^$\\()[\]{}]/g, '\\$&'); -} - -class FindState { - constructor(options) { - let { text, caseSensitive, regex } = options; - - if (!regex) - text = escapeRegex(text); - - const finder = new Finder(new RegExp(text, caseSensitive ? 'g' : 'ig')); - this.results = Array.from(finder); - this.index = -1; - this.options = options; - } - - get selected() { - return this.index > -1 ? this.results[this.index] : null; - } - - toJSON() { - return { - index: this.index > -1 ? this.index : null, - results: this.results, - count: this.results.length - }; - } - - selectNext(step=1) { - const index = this.index + step; - const result = this.results[index]; - if (result) { - this.index = index; - result.highlight(); - result.scrollTo(); - } - return result; - } - - selectPrevious() { - return this.selectNext(-1); - } -} - -CurrentFindState = null; - -const ExcludeKeys = new Set(['top', 'right', 'bottom', 'left']); -updateFind = withEncodedArg(options => { - // TODO Start at the current result position - // TODO Introduce slight delay, cap the number of results, and report results asynchronously - - let newFindState; - if (!options || !options.text) { - clearHighlightRects(); - return - } - - try { - newFindState = new FindState(options); - } catch (err) { - clearHighlightRects(); - throw err; - } - - if (newFindState.results.length) { - let selected = CurrentFindState && CurrentFindState.selected; - let selectIndex = 0; - if (selected) { - let {node: currentNode, offset: currentOffset} = selected; - selectIndex = newFindState.results.findIndex(r => { - if (r.node === currentNode) { - return r.offset >= currentOffset; - } - - let relation = currentNode.compareDocumentPosition(r.node); - return Boolean(relation & Node.DOCUMENT_POSITION_FOLLOWING); - }); - } - - newFindState.selectNext(selectIndex+1); - } else { - clearHighlightRects(); - } - - CurrentFindState = newFindState; - return btoa(JSON.stringify(CurrentFindState, (k, v) => (ExcludeKeys.has(k) ? undefined : v))); -}); - -selectNextResult = withEncodedArg(options => { - if (CurrentFindState) - CurrentFindState.selectNext(); -}); - -selectPreviousResult = withEncodedArg(options => { - if (CurrentFindState) - CurrentFindState.selectPrevious(); -}); - -function endFind() { - clearHighlightRects() - CurrentFindState = null; -} diff --git a/Multiplatform/Shared/Article/page.html b/Multiplatform/Shared/Article/page.html deleted file mode 100644 index 9d38c005e..000000000 --- a/Multiplatform/Shared/Article/page.html +++ /dev/null @@ -1,21 +0,0 @@ - - - [[title]] - - - - - - - - - - [[body]] - - diff --git a/Multiplatform/Shared/Article/styleSheet.css b/Multiplatform/Shared/Article/styleSheet.css deleted file mode 100644 index 3b48a9164..000000000 --- a/Multiplatform/Shared/Article/styleSheet.css +++ /dev/null @@ -1,65 +0,0 @@ -body { - margin-top: 3px; - margin-bottom: 20px; - padding-left: 20px; - padding-right: 20px; - - word-break: break-word; - -webkit-hyphens: auto; - -webkit-text-size-adjust: none; -} - -:root { - color-scheme: light dark; - font: -apple-system-body; - font-family: -apple-system; - font-size: [[font-size]]px; - --accent-color: rgba([[accent-r]], [[accent-g]], [[accent-b]], .75); - --block-quote-border-color: rgba([[accent-r]], [[accent-g]], [[accent-b]], .50); -} - -@media(prefers-color-scheme: dark) { - :root { - --accent-color: rgba([[accent-r]], [[accent-g]], [[accent-b]], .75); - --block-quote-border-color: rgba([[accent-r]], [[accent-g]], [[accent-b]], .50); - --header-table-border-color: rgba(255, 255, 255, 0.2); - } -} - -body a, body a:link, body a:visited { - color: var(--accent-color); -} -body .header { - font: -apple-system-body; - font-size: [[font-size]]px; -} -body .header a:link, body .header a:visited { - color: var(--accent-color); -} - -.avatar img { - border-radius: 4px; -} - -pre { - border: 1px solid var(--accent-color); - padding: 5px; -} - -.nnw-overflow table { - border: 1px solid var(--accent-color); -} - -.activityIndicatorWrap { - position: relative; -} - -.activityIndicator { - z-index: 1; - width: 64px; - height: 64px; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} diff --git a/Multiplatform/Shared/Assets.xcassets/AccentColor.colorset/Contents.json b/Multiplatform/Shared/Assets.xcassets/AccentColor.colorset/Contents.json deleted file mode 100644 index 4f1440c52..000000000 --- a/Multiplatform/Shared/Assets.xcassets/AccentColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.933", - "green" : "0.416", - "red" : "0.031" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.957", - "green" : "0.620", - "red" : "0.369" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index 8ba7ca9ea..000000000 --- a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,176 +0,0 @@ -{ - "images" : [ - { - "filename" : "icon-40.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "icon-60.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "20x20" - }, - { - "filename" : "icon-58.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "icon-87.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "29x29" - }, - { - "filename" : "icon-80.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "icon-120.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "40x40" - }, - { - "filename" : "icon-121.png", - "idiom" : "iphone", - "scale" : "2x", - "size" : "60x60" - }, - { - "filename" : "icon-180.png", - "idiom" : "iphone", - "scale" : "3x", - "size" : "60x60" - }, - { - "filename" : "icon-20.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "20x20" - }, - { - "filename" : "icon-41.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "20x20" - }, - { - "filename" : "icon-29.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "29x29" - }, - { - "filename" : "icon-59.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "29x29" - }, - { - "filename" : "icon-42.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "40x40" - }, - { - "filename" : "icon-81.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "40x40" - }, - { - "filename" : "icon-76.png", - "idiom" : "ipad", - "scale" : "1x", - "size" : "76x76" - }, - { - "filename" : "icon-152.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "76x76" - }, - { - "filename" : "icon-167.png", - "idiom" : "ipad", - "scale" : "2x", - "size" : "83.5x83.5" - }, - { - "filename" : "icon-1024.png", - "idiom" : "ios-marketing", - "scale" : "1x", - "size" : "1024x1024" - }, - { - "filename" : "Icon-MacOS-16x16@1x.png.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "16x16" - }, - { - "filename" : "Icon-MacOS-16x16@2x.png.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "16x16" - }, - { - "filename" : "Icon-MacOS-32x32@1x.png.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "32x32" - }, - { - "filename" : "Icon-MacOS-32x32@2x.png.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "32x32" - }, - { - "filename" : "Icon-MacOS-128x128@1x.png.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "128x128" - }, - { - "filename" : "Icon-MacOS-128x128@2x.png.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "128x128" - }, - { - "filename" : "Icon-MacOS-256x256@1x.png.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "256x256" - }, - { - "filename" : "Icon-MacOS-256x256@2x.png.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "256x256" - }, - { - "filename" : "Icon-MacOS-512x512@1x.png.png", - "idiom" : "mac", - "scale" : "1x", - "size" : "512x512" - }, - { - "filename" : "Icon-MacOS-512x512@2x.png.png", - "idiom" : "mac", - "scale" : "2x", - "size" : "512x512" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@1x.png.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@1x.png.png deleted file mode 100644 index e8c6573fd..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@1x.png.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@2x.png.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@2x.png.png deleted file mode 100644 index 176ad77e6..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-128x128@2x.png.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@1x.png.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@1x.png.png deleted file mode 100644 index 4477733ee..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@1x.png.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@2x.png.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@2x.png.png deleted file mode 100644 index 5a536a436..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-16x16@2x.png.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@1x.png.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@1x.png.png deleted file mode 100644 index 176ad77e6..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@1x.png.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@2x.png.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@2x.png.png deleted file mode 100644 index 943c3a89e..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-256x256@2x.png.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@1x.png.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@1x.png.png deleted file mode 100644 index 084985da6..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@1x.png.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@2x.png.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@2x.png.png deleted file mode 100644 index 0c1d6e3b4..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-32x32@2x.png.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@1x.png.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@1x.png.png deleted file mode 100644 index 943c3a89e..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@1x.png.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@2x.png.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@2x.png.png deleted file mode 100644 index 61f85ee9d..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/Icon-MacOS-512x512@2x.png.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-1024.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-1024.png deleted file mode 100644 index c2bb5d540..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-1024.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-120.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-120.png deleted file mode 100644 index 6d1a94fd9..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-120.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-121.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-121.png deleted file mode 100644 index 6d1a94fd9..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-121.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-152.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-152.png deleted file mode 100644 index b217d09c4..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-152.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-167.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-167.png deleted file mode 100644 index 4cd8fa6c0..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-167.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-180.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-180.png deleted file mode 100644 index 8c5c93b8c..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-180.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-20.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-20.png deleted file mode 100644 index 6be295367..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-20.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-29.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-29.png deleted file mode 100644 index c9c8ffb32..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-29.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-40.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-40.png deleted file mode 100644 index 180a98b25..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-40.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-41.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-41.png deleted file mode 100644 index 180a98b25..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-41.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-42.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-42.png deleted file mode 100644 index 180a98b25..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-42.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-58.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-58.png deleted file mode 100644 index a53d44864..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-58.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-59.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-59.png deleted file mode 100644 index a53d44864..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-59.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-60.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-60.png deleted file mode 100644 index 7a01bc978..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-60.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-76.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-76.png deleted file mode 100644 index 4aea101ae..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-76.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-80.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-80.png deleted file mode 100644 index 85289428d..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-80.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-81.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-81.png deleted file mode 100644 index 85289428d..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-81.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-87.png b/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-87.png deleted file mode 100644 index dd27b3ca3..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/AppIcon.appiconset/icon-87.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/ArticleExtractorError.imageset/ArticleExtractorError.pdf b/Multiplatform/Shared/Assets.xcassets/ArticleExtractorError.imageset/ArticleExtractorError.pdf deleted file mode 100644 index 7d1450ec6..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/ArticleExtractorError.imageset/ArticleExtractorError.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/ArticleExtractorError.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/ArticleExtractorError.imageset/Contents.json deleted file mode 100644 index 0f8e6142c..000000000 --- a/Multiplatform/Shared/Assets.xcassets/ArticleExtractorError.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "ArticleExtractorError.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/ArticleExtractorOn.symbolset/Contents.json b/Multiplatform/Shared/Assets.xcassets/ArticleExtractorOn.symbolset/Contents.json deleted file mode 100644 index 842391314..000000000 --- a/Multiplatform/Shared/Assets.xcassets/ArticleExtractorOn.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "doc.plaintext.on.svg", - "idiom" : "universal" - } - ] -} diff --git a/Multiplatform/Shared/Assets.xcassets/ArticleExtractorOn.symbolset/doc.plaintext.on.svg b/Multiplatform/Shared/Assets.xcassets/ArticleExtractorOn.symbolset/doc.plaintext.on.svg deleted file mode 100644 index f0a3af753..000000000 --- a/Multiplatform/Shared/Assets.xcassets/ArticleExtractorOn.symbolset/doc.plaintext.on.svg +++ /dev/null @@ -1,218 +0,0 @@ - - - - Untitled - Created with Sketch. - - - - - - - Weight/Scale Variations - - - Ultralight - - - Thin - - - Light - - - Regular - - - Medium - - - Semibold - - - Bold - - - Heavy - - - Black - - - - - - - - - - - - - Design Variations - - - Symbols are supported in up to nine weights and three scales. - - - For optimal layout with text and other symbols, vertically align - - - symbols with the adjacent text. - - - - - - - - Margins - - - Leading and trailing margins on the left and right side of each symbol - - - can be adjusted by modifying the width of the blue rectangles. - - - Modifications are automatically applied proportionally to all - - - scales and weights. - - - - - - Exporting - - - Symbols should be outlined when exporting to ensure the - - - design is preserved when submitting to Xcode. - - - Template v.1.0 - - - Generated from doc.plaintext - - - Typeset at 100 points - - - Small - - - Medium - - - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Multiplatform/Shared/Assets.xcassets/Contents.json b/Multiplatform/Shared/Assets.xcassets/Contents.json deleted file mode 100644 index 73c00596a..000000000 --- a/Multiplatform/Shared/Assets.xcassets/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/ExtensionPointMarsEdit.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/ExtensionPointMarsEdit.imageset/Contents.json deleted file mode 100644 index 432281179..000000000 --- a/Multiplatform/Shared/Assets.xcassets/ExtensionPointMarsEdit.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "MarsEditOfficial.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/ExtensionPointMarsEdit.imageset/MarsEditOfficial.pdf b/Multiplatform/Shared/Assets.xcassets/ExtensionPointMarsEdit.imageset/MarsEditOfficial.pdf deleted file mode 100644 index 82396a369..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/ExtensionPointMarsEdit.imageset/MarsEditOfficial.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/ExtensionPointMicroblog.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/ExtensionPointMicroblog.imageset/Contents.json deleted file mode 100644 index 4db95b5b7..000000000 --- a/Multiplatform/Shared/Assets.xcassets/ExtensionPointMicroblog.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "micro-dot-blog.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/ExtensionPointMicroblog.imageset/micro-dot-blog.pdf b/Multiplatform/Shared/Assets.xcassets/ExtensionPointMicroblog.imageset/micro-dot-blog.pdf deleted file mode 100644 index ad66dd377..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/ExtensionPointMicroblog.imageset/micro-dot-blog.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/ExtensionPointReddit.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/ExtensionPointReddit.imageset/Contents.json deleted file mode 100644 index 1a49be3b3..000000000 --- a/Multiplatform/Shared/Assets.xcassets/ExtensionPointReddit.imageset/Contents.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "images" : [ - { - "filename" : "reddit-light.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "reddit-dark.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/ExtensionPointReddit.imageset/reddit-dark.pdf b/Multiplatform/Shared/Assets.xcassets/ExtensionPointReddit.imageset/reddit-dark.pdf deleted file mode 100644 index b4b6d7419..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/ExtensionPointReddit.imageset/reddit-dark.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/ExtensionPointReddit.imageset/reddit-light.pdf b/Multiplatform/Shared/Assets.xcassets/ExtensionPointReddit.imageset/reddit-light.pdf deleted file mode 100644 index 4d6267b63..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/ExtensionPointReddit.imageset/reddit-light.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/ExtensionPointTwitter.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/ExtensionPointTwitter.imageset/Contents.json deleted file mode 100644 index 834100cda..000000000 --- a/Multiplatform/Shared/Assets.xcassets/ExtensionPointTwitter.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "twitter.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/ExtensionPointTwitter.imageset/twitter.pdf b/Multiplatform/Shared/Assets.xcassets/ExtensionPointTwitter.imageset/twitter.pdf deleted file mode 100644 index e50de4443..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/ExtensionPointTwitter.imageset/twitter.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/FaviconTemplateImage.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/FaviconTemplateImage.imageset/Contents.json deleted file mode 100644 index bd3a2fbbe..000000000 --- a/Multiplatform/Shared/Assets.xcassets/FaviconTemplateImage.imageset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "images" : [ - { - "filename" : "faviconTemplateImage.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/FaviconTemplateImage.imageset/faviconTemplateImage.pdf b/Multiplatform/Shared/Assets.xcassets/FaviconTemplateImage.imageset/faviconTemplateImage.pdf deleted file mode 100644 index d6bcb5b69..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/FaviconTemplateImage.imageset/faviconTemplateImage.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/IconBackgroundColor.colorset/Contents.json b/Multiplatform/Shared/Assets.xcassets/IconBackgroundColor.colorset/Contents.json deleted file mode 100644 index 49db4ebbe..000000000 --- a/Multiplatform/Shared/Assets.xcassets/IconBackgroundColor.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.929", - "green" : "0.922", - "red" : "0.922" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.220", - "green" : "0.220", - "red" : "0.220" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/MarkAllAsRead.symbolset/Contents.json b/Multiplatform/Shared/Assets.xcassets/MarkAllAsRead.symbolset/Contents.json deleted file mode 100644 index 81428a2de..000000000 --- a/Multiplatform/Shared/Assets.xcassets/MarkAllAsRead.symbolset/Contents.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - }, - "symbols" : [ - { - "filename" : "markAllAsRead.svg", - "idiom" : "universal" - } - ] -} diff --git a/Multiplatform/Shared/Assets.xcassets/MarkAllAsRead.symbolset/markAllAsRead.svg b/Multiplatform/Shared/Assets.xcassets/MarkAllAsRead.symbolset/markAllAsRead.svg deleted file mode 100644 index 10e23abd9..000000000 --- a/Multiplatform/Shared/Assets.xcassets/MarkAllAsRead.symbolset/markAllAsRead.svg +++ /dev/null @@ -1,147 +0,0 @@ - - - - markAllAsRead - Created with Sketch. - - - - - - - Weight/Scale Variations - - - Ultralight - - - Thin - - - Light - - - Regular - - - Medium - - - Semibold - - - Bold - - - Heavy - - - Black - - - - - - - - - - - - - Design Variations - - - Symbols are supported in up to nine weights and three scales. - - - For optimal layout with text and other symbols, vertically align - - - symbols with the adjacent text. - - - - - - - - Margins - - - Leading and trailing margins on the left and right side of each symbol - - - can be adjusted by modifying the width of the blue rectangles. - - - Modifications are automatically applied proportionally to all - - - scales and weights. - - - - - - Exporting - - - Symbols should be outlined when exporting to ensure the - - - design is preserved when submitting to Xcode. - - - Template v.1.0 - - - Generated from circle - - - Typeset at 100 points - - - Small - - - Medium - - - Large - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/Multiplatform/Shared/Assets.xcassets/MarkAllAsReadPNG.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/MarkAllAsReadPNG.imageset/Contents.json deleted file mode 100644 index b494681a3..000000000 --- a/Multiplatform/Shared/Assets.xcassets/MarkAllAsReadPNG.imageset/Contents.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "mark-all-as-read.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "template" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/MarkAllAsReadPNG.imageset/mark-all-as-read.png b/Multiplatform/Shared/Assets.xcassets/MarkAllAsReadPNG.imageset/mark-all-as-read.png deleted file mode 100644 index c78a44c1b..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/MarkAllAsReadPNG.imageset/mark-all-as-read.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/SidebarUnreadCountBackground.colorset/Contents.json b/Multiplatform/Shared/Assets.xcassets/SidebarUnreadCountBackground.colorset/Contents.json deleted file mode 100644 index 39632c80d..000000000 --- a/Multiplatform/Shared/Assets.xcassets/SidebarUnreadCountBackground.colorset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.248", - "blue" : "0.118", - "green" : "0.118", - "red" : "0.118" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.250", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - }, - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.500", - "blue" : "0.000", - "green" : "0.000", - "red" : "0.000" - } - }, - "idiom" : "mac" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.500", - "blue" : "0.000", - "green" : "0.000", - "red" : "0.000" - } - }, - "idiom" : "mac" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/SidebarUnreadCountForeground.colorset/Contents.json b/Multiplatform/Shared/Assets.xcassets/SidebarUnreadCountForeground.colorset/Contents.json deleted file mode 100644 index 91ae93460..000000000 --- a/Multiplatform/Shared/Assets.xcassets/SidebarUnreadCountForeground.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "0.900", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/StarColor.colorset/Contents.json b/Multiplatform/Shared/Assets.xcassets/StarColor.colorset/Contents.json deleted file mode 100644 index c464941a7..000000000 --- a/Multiplatform/Shared/Assets.xcassets/StarColor.colorset/Contents.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.204", - "green" : "0.776", - "red" : "0.976" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/WebStatusBarBackground.colorset/Contents.json b/Multiplatform/Shared/Assets.xcassets/WebStatusBarBackground.colorset/Contents.json deleted file mode 100644 index c947def89..000000000 --- a/Multiplatform/Shared/Assets.xcassets/WebStatusBarBackground.colorset/Contents.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "colors" : [ - { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.940", - "green" : "0.940", - "red" : "0.940" - } - }, - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.196", - "green" : "0.196", - "red" : "0.196" - } - }, - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountBazQux.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountBazQux.imageset/Contents.json deleted file mode 100644 index 25d8387f8..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountBazQux.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "bazqux-any.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountBazQux.imageset/bazqux-any.pdf b/Multiplatform/Shared/Assets.xcassets/accountBazQux.imageset/bazqux-any.pdf deleted file mode 100644 index d13a0defd..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountBazQux.imageset/bazqux-any.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/Contents.json deleted file mode 100644 index b2f51e691..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/Contents.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "images" : [ - { - "filename" : "icloud-any.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "icloud-dark.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/icloud-any.pdf b/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/icloud-any.pdf deleted file mode 100644 index 79ba7e3eb..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/icloud-any.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/icloud-dark.pdf b/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/icloud-dark.pdf deleted file mode 100644 index e876337ac..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountCloudKit.imageset/icloud-dark.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/Contents.json deleted file mode 100644 index f7b68151c..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/Contents.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - "images" : [ - { - "filename" : "feedwranger-any-slice.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "feedwranger-dark-slice.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "feedwranger-any-slice@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "feedwranger-dark-slice@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "feedwranger-any-slice@3x.png", - "idiom" : "universal", - "scale" : "3x" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "feedwranger-dark-slice@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-any-slice.png b/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-any-slice.png deleted file mode 100644 index a04e07f9a..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-any-slice.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-any-slice@2x.png b/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-any-slice@2x.png deleted file mode 100644 index dd25a60ae..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-any-slice@2x.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-any-slice@3x.png b/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-any-slice@3x.png deleted file mode 100644 index 1fceca03d..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-any-slice@3x.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-dark-slice.png b/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-dark-slice.png deleted file mode 100644 index ff1990102..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-dark-slice.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-dark-slice@2x.png b/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-dark-slice@2x.png deleted file mode 100644 index e2e52edb5..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-dark-slice@2x.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-dark-slice@3x.png b/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-dark-slice@3x.png deleted file mode 100644 index e1640465a..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountFeedWrangler.imageset/feedwranger-dark-slice@3x.png and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedbin.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountFeedbin.imageset/Contents.json deleted file mode 100644 index 1b0780896..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountFeedbin.imageset/Contents.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "images" : [ - { - "filename" : "feedbin.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedbin.imageset/feedbin.pdf b/Multiplatform/Shared/Assets.xcassets/accountFeedbin.imageset/feedbin.pdf deleted file mode 100644 index 8892e9db6..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountFeedbin.imageset/feedbin.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/Contents.json deleted file mode 100644 index 236ba8fa6..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/Contents.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "images" : [ - { - "filename" : "feedly-logo-any.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "feedly-logo-dark.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/feedly-logo-any.pdf b/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/feedly-logo-any.pdf deleted file mode 100644 index e1ccaab94..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/feedly-logo-any.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/feedly-logo-dark.pdf b/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/feedly-logo-dark.pdf deleted file mode 100644 index f287b271b..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountFeedly.imageset/feedly-logo-dark.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountFreshRSS.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountFreshRSS.imageset/Contents.json deleted file mode 100644 index f71a580b8..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountFreshRSS.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "FreshRSS.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountFreshRSS.imageset/FreshRSS.pdf b/Multiplatform/Shared/Assets.xcassets/accountFreshRSS.imageset/FreshRSS.pdf deleted file mode 100644 index d9ba3f3ea..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountFreshRSS.imageset/FreshRSS.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountInoreader.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountInoreader.imageset/Contents.json deleted file mode 100644 index 8711b150a..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountInoreader.imageset/Contents.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "images" : [ - { - "filename" : "inoreader_logo-any.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "inoreader_logo-dark.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountInoreader.imageset/inoreader_logo-any.pdf b/Multiplatform/Shared/Assets.xcassets/accountInoreader.imageset/inoreader_logo-any.pdf deleted file mode 100644 index 4c5befe74..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountInoreader.imageset/inoreader_logo-any.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountInoreader.imageset/inoreader_logo-dark.pdf b/Multiplatform/Shared/Assets.xcassets/accountInoreader.imageset/inoreader_logo-dark.pdf deleted file mode 100644 index 2ab5d34dd..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountInoreader.imageset/inoreader_logo-dark.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocal.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountLocal.imageset/Contents.json deleted file mode 100644 index 63f3b392e..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountLocal.imageset/Contents.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "images" : [ - { - "filename" : "localAccountLight.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "localAccountDark-1.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocal.imageset/localAccountDark-1.pdf b/Multiplatform/Shared/Assets.xcassets/accountLocal.imageset/localAccountDark-1.pdf deleted file mode 100644 index 584b84f4a..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountLocal.imageset/localAccountDark-1.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocal.imageset/localAccountLight.pdf b/Multiplatform/Shared/Assets.xcassets/accountLocal.imageset/localAccountLight.pdf deleted file mode 100644 index d3d3d40e2..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountLocal.imageset/localAccountLight.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/Contents.json deleted file mode 100644 index 63f3b392e..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/Contents.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "images" : [ - { - "filename" : "localAccountLight.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "localAccountDark-1.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/localAccountDark-1.pdf b/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/localAccountDark-1.pdf deleted file mode 100644 index 584b84f4a..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/localAccountDark-1.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/localAccountLight.pdf b/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/localAccountLight.pdf deleted file mode 100644 index d3d3d40e2..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountLocalMac.imageset/localAccountLight.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/Contents.json deleted file mode 100644 index e072222a1..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/Contents.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "images" : [ - { - "filename" : "ipad-any-slice.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "ipad-dark-slice.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/ipad-any-slice.pdf b/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/ipad-any-slice.pdf deleted file mode 100644 index 91665fa1b..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/ipad-any-slice.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/ipad-dark-slice.pdf b/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/ipad-dark-slice.pdf deleted file mode 100644 index 9091a096b..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountLocalPad.imageset/ipad-dark-slice.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/Contents.json deleted file mode 100644 index 04d21c05f..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/Contents.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "images" : [ - { - "filename" : "iphone-any-slice.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "iphone-dark-slice.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/iphone-any-slice.pdf b/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/iphone-any-slice.pdf deleted file mode 100644 index f36056970..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/iphone-any-slice.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/iphone-dark-slice.pdf b/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/iphone-dark-slice.pdf deleted file mode 100644 index cf2247077..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountLocalPhone.imageset/iphone-dark-slice.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountNewsBlur.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountNewsBlur.imageset/Contents.json deleted file mode 100644 index a73c591f6..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountNewsBlur.imageset/Contents.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "images" : [ - { - "filename" : "Newsblur-any.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountNewsBlur.imageset/Newsblur-any.pdf b/Multiplatform/Shared/Assets.xcassets/accountNewsBlur.imageset/Newsblur-any.pdf deleted file mode 100644 index 216dc4f8a..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountNewsBlur.imageset/Newsblur-any.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountTheOldReader.imageset/Contents.json b/Multiplatform/Shared/Assets.xcassets/accountTheOldReader.imageset/Contents.json deleted file mode 100644 index 231c33ab0..000000000 --- a/Multiplatform/Shared/Assets.xcassets/accountTheOldReader.imageset/Contents.json +++ /dev/null @@ -1,26 +0,0 @@ -{ - "images" : [ - { - "filename" : "oldreader-icon-any.pdf", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "oldreader-icon-dark.pdf", - "idiom" : "universal" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - }, - "properties" : { - "preserves-vector-representation" : true, - "template-rendering-intent" : "original" - } -} diff --git a/Multiplatform/Shared/Assets.xcassets/accountTheOldReader.imageset/oldreader-icon-any.pdf b/Multiplatform/Shared/Assets.xcassets/accountTheOldReader.imageset/oldreader-icon-any.pdf deleted file mode 100644 index 05b0003b1..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountTheOldReader.imageset/oldreader-icon-any.pdf and /dev/null differ diff --git a/Multiplatform/Shared/Assets.xcassets/accountTheOldReader.imageset/oldreader-icon-dark.pdf b/Multiplatform/Shared/Assets.xcassets/accountTheOldReader.imageset/oldreader-icon-dark.pdf deleted file mode 100644 index dfe4ce8b4..000000000 Binary files a/Multiplatform/Shared/Assets.xcassets/accountTheOldReader.imageset/oldreader-icon-dark.pdf and /dev/null differ diff --git a/Multiplatform/Shared/CombineExt/DemandBuffer.swift b/Multiplatform/Shared/CombineExt/DemandBuffer.swift deleted file mode 100644 index 7b02ac377..000000000 --- a/Multiplatform/Shared/CombineExt/DemandBuffer.swift +++ /dev/null @@ -1,151 +0,0 @@ -// -// DemandBuffer.swift -// CombineExt -// -// Created by Shai Mishali on 21/02/2020. -// Copyright © 2020 Combine Community. All rights reserved. -// - -#if canImport(Combine) -import Combine -import class Foundation.NSRecursiveLock - -/// A buffer responsible for managing the demand of a downstream -/// subscriber for an upstream publisher -/// -/// It buffers values and completion events and forwards them dynamically -/// according to the demand requested by the downstream -/// -/// In a sense, the subscription only relays the requests for demand, as well -/// the events emitted by the upstream — to this buffer, which manages -/// the entire behavior and backpressure contract -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -class DemandBuffer { - private let lock = NSRecursiveLock() - private var buffer = [S.Input]() - private let subscriber: S - private var completion: Subscribers.Completion? - private var demandState = Demand() - - /// Initialize a new demand buffer for a provided downstream subscriber - /// - /// - parameter subscriber: The downstream subscriber demanding events - init(subscriber: S) { - self.subscriber = subscriber - } - - /// Buffer an upstream value to later be forwarded to - /// the downstream subscriber, once it demands it - /// - /// - parameter value: Upstream value to buffer - /// - /// - returns: The demand fulfilled by the bufferr - func buffer(value: S.Input) -> Subscribers.Demand { - precondition(self.completion == nil, - "How could a completed publisher sent values?! Beats me 🤷‍♂️") - - switch demandState.requested { - case .unlimited: - return subscriber.receive(value) - default: - buffer.append(value) - return flush() - } - } - - /// Complete the demand buffer with an upstream completion event - /// - /// This method will deplete the buffer immediately, - /// based on the currently accumulated demand, and relay the - /// completion event down as soon as demand is fulfilled - /// - /// - parameter completion: Completion event - func complete(completion: Subscribers.Completion) { - precondition(self.completion == nil, - "Completion have already occured, which is quite awkward 🥺") - - self.completion = completion - _ = flush() - } - - /// Signal to the buffer that the downstream requested new demand - /// - /// - note: The buffer will attempt to flush as many events rqeuested - /// by the downstream at this point - func demand(_ demand: Subscribers.Demand) -> Subscribers.Demand { - flush(adding: demand) - } - - /// Flush buffered events to the downstream based on the current - /// state of the downstream's demand - /// - /// - parameter newDemand: The new demand to add. If `nil`, the flush isn't the - /// result of an explicit demand change - /// - /// - note: After fulfilling the downstream's request, if completion - /// has already occured, the buffer will be cleared and the - /// completion event will be sent to the downstream subscriber - private func flush(adding newDemand: Subscribers.Demand? = nil) -> Subscribers.Demand { - lock.lock() - defer { lock.unlock() } - - if let newDemand = newDemand { - demandState.requested += newDemand - } - - // If buffer isn't ready for flushing, return immediately - guard demandState.requested > 0 || newDemand == Subscribers.Demand.none else { return .none } - - while !buffer.isEmpty && demandState.processed < demandState.requested { - demandState.requested += subscriber.receive(buffer.remove(at: 0)) - demandState.processed += 1 - } - - if let completion = completion { - // Completion event was already sent - buffer = [] - demandState = .init() - self.completion = nil - subscriber.receive(completion: completion) - return .none - } - - let sentDemand = demandState.requested - demandState.sent - demandState.sent += sentDemand - return sentDemand - } -} - -// MARK: - Private Helpers -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -private extension DemandBuffer { - /// A model that tracks the downstream's - /// accumulated demand state - struct Demand { - var processed: Subscribers.Demand = .none - var requested: Subscribers.Demand = .none - var sent: Subscribers.Demand = .none - } -} - -// MARK: - Internally-scoped helpers -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension Subscription { - /// Reqeust demand if it's not empty - /// - /// - parameter demand: Requested demand - func requestIfNeeded(_ demand: Subscribers.Demand) { - guard demand > .none else { return } - request(demand) - } -} - -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension Optional where Wrapped == Subscription { - /// Cancel the Optional subscription and nullify it - mutating func kill() { - self?.cancel() - self = nil - } -} -#endif diff --git a/Multiplatform/Shared/CombineExt/ReplaySubject.swift b/Multiplatform/Shared/CombineExt/ReplaySubject.swift deleted file mode 100644 index b639da4fd..000000000 --- a/Multiplatform/Shared/CombineExt/ReplaySubject.swift +++ /dev/null @@ -1,121 +0,0 @@ -// -// ReplaySubject.swift -// CombineExt -// -// Created by Jasdev Singh on 13/04/2020. -// Copyright © 2020 Combine Community. All rights reserved. -// - -#if canImport(Combine) -import Combine - -/// A `ReplaySubject` is a subject that can buffer one or more values. It stores value events, up to its `bufferSize` in a -/// first-in-first-out manner and then replays it to -/// future subscribers and also forwards completion events. -/// -/// The implementation borrows heavily from [Entwine’s](https://github.com/tcldr/Entwine/blob/b839c9fcc7466878d6a823677ce608da998b95b9/Sources/Entwine/Operators/ReplaySubject.swift). -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -public final class ReplaySubject: Subject { - public typealias Output = Output - public typealias Failure = Failure - - private let bufferSize: Int - private var buffer = [Output]() - - // Keeping track of all live subscriptions, so `send` events can be forwarded to them. - private var subscriptions = [Subscription>]() - - private var completion: Subscribers.Completion? - private var isActive: Bool { completion == nil } - - /// Create a `ReplaySubject`, buffering up to `bufferSize` values and replaying them to new subscribers - /// - Parameter bufferSize: The maximum number of value events to buffer and replay to all future subscribers. - public init(bufferSize: Int) { - self.bufferSize = bufferSize - } - - public func send(_ value: Output) { - guard isActive else { return } - - buffer.append(value) - - if buffer.count > bufferSize { - buffer.removeFirst() - } - - subscriptions.forEach { $0.forwardValueToBuffer(value) } - } - - public func send(completion: Subscribers.Completion) { - guard isActive else { return } - - self.completion = completion - - subscriptions.forEach { $0.forwardCompletionToBuffer(completion) } - } - - public func send(subscription: Combine.Subscription) { - subscription.request(.unlimited) - } - - public func receive(subscriber: Subscriber) where Failure == Subscriber.Failure, Output == Subscriber.Input { - let subscriberIdentifier = subscriber.combineIdentifier - - let subscription = Subscription(downstream: AnySubscriber(subscriber)) { [weak self] in - guard let self = self, - let subscriptionIndex = self.subscriptions - .firstIndex(where: { $0.innerSubscriberIdentifier == subscriberIdentifier }) else { return } - - self.subscriptions.remove(at: subscriptionIndex) - } - - subscriptions.append(subscription) - - subscriber.receive(subscription: subscription) - subscription.replay(buffer, completion: completion) - } -} - -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -extension ReplaySubject { - final class Subscription: Combine.Subscription where Output == Downstream.Input, Failure == Downstream.Failure { - private var demandBuffer: DemandBuffer? - private var cancellationHandler: (() -> Void)? - - fileprivate let innerSubscriberIdentifier: CombineIdentifier - - init(downstream: Downstream, cancellationHandler: (() -> Void)?) { - self.demandBuffer = DemandBuffer(subscriber: downstream) - self.innerSubscriberIdentifier = downstream.combineIdentifier - self.cancellationHandler = cancellationHandler - } - - func replay(_ buffer: [Output], completion: Subscribers.Completion?) { - buffer.forEach(forwardValueToBuffer) - - if let completion = completion { - forwardCompletionToBuffer(completion) - } - } - - func forwardValueToBuffer(_ value: Output) { - _ = demandBuffer?.buffer(value: value) - } - - func forwardCompletionToBuffer(_ completion: Subscribers.Completion) { - demandBuffer?.complete(completion: completion) - } - - func request(_ demand: Subscribers.Demand) { - _ = demandBuffer?.demand(demand) - } - - func cancel() { - cancellationHandler?() - cancellationHandler = nil - - demandBuffer = nil - } - } -} -#endif diff --git a/Multiplatform/Shared/CombineExt/ShareReplay.swift b/Multiplatform/Shared/CombineExt/ShareReplay.swift deleted file mode 100644 index d5c2b24a6..000000000 --- a/Multiplatform/Shared/CombineExt/ShareReplay.swift +++ /dev/null @@ -1,24 +0,0 @@ -// -// ShareReplay.swift -// CombineExt -// -// Created by Jasdev Singh on 13/04/2020. -// Copyright © 2020 Combine Community. All rights reserved. -// - -#if canImport(Combine) -import Combine - -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -public extension Publisher { - /// A variation on [share()](https://developer.apple.com/documentation/combine/publisher/3204754-share) - /// that allows for buffering and replaying a `replay` amount of value events to future subscribers. - /// - /// - Parameter count: The number of value events to buffer in a first-in-first-out manner. - /// - Returns: A publisher that replays the specified number of value events to future subscribers. - func share(replay count: Int) -> Publishers.Autoconnect>> { - multicast { ReplaySubject(bufferSize: count) } - .autoconnect() - } -} -#endif diff --git a/Multiplatform/Shared/CombineExt/Sink.swift b/Multiplatform/Shared/CombineExt/Sink.swift deleted file mode 100644 index c60008ac8..000000000 --- a/Multiplatform/Shared/CombineExt/Sink.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// Sink.swift -// CombineExt -// -// Created by Shai Mishali on 14/03/2020. -// Copyright © 2020 Combine Community. All rights reserved. -// - -#if canImport(Combine) -import Combine - -/// A generic sink using an underlying demand buffer to balance -/// the demand of a downstream subscriber for the events of an -/// upstream publisher -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -class Sink: Subscriber { - typealias TransformFailure = (Upstream.Failure) -> Downstream.Failure? - typealias TransformOutput = (Upstream.Output) -> Downstream.Input? - - private(set) var buffer: DemandBuffer - private var upstreamSubscription: Subscription? - private let transformOutput: TransformOutput? - private let transformFailure: TransformFailure? - - /// Initialize a new sink subscribing to the upstream publisher and - /// fulfilling the demand of the downstream subscriber using a backpresurre - /// demand-maintaining buffer. - /// - /// - parameter upstream: The upstream publisher - /// - parameter downstream: The downstream subscriber - /// - parameter transformOutput: Transform the upstream publisher's output type to the downstream's input type - /// - parameter transformFailure: Transform the upstream failure type to the downstream's failure type - /// - /// - note: You **must** provide the two transformation functions above if you're using - /// the default `Sink` implementation. Otherwise, you must subclass `Sink` with your own - /// publisher's sink and manage the buffer accordingly. - init(upstream: Upstream, - downstream: Downstream, - transformOutput: TransformOutput? = nil, - transformFailure: TransformFailure? = nil) { - self.buffer = DemandBuffer(subscriber: downstream) - self.transformOutput = transformOutput - self.transformFailure = transformFailure - upstream.subscribe(self) - } - - func demand(_ demand: Subscribers.Demand) { - let newDemand = buffer.demand(demand) - upstreamSubscription?.requestIfNeeded(newDemand) - } - - func receive(subscription: Subscription) { - upstreamSubscription = subscription - } - - func receive(_ input: Upstream.Output) -> Subscribers.Demand { - guard let transform = transformOutput else { - fatalError(""" - ❌ Missing output transformation - ========================= - - You must either: - - Provide a transformation function from the upstream's output to the downstream's input; or - - Subclass `Sink` with your own publisher's Sink and manage the buffer yourself - """) - } - - guard let input = transform(input) else { return .none } - return buffer.buffer(value: input) - } - - func receive(completion: Subscribers.Completion) { - switch completion { - case .finished: - buffer.complete(completion: .finished) - case .failure(let error): - guard let transform = transformFailure else { - fatalError(""" - ❌ Missing failure transformation - ========================= - - You must either: - - Provide a transformation function from the upstream's failure to the downstream's failuer; or - - Subclass `Sink` with your own publisher's Sink and manage the buffer yourself - """) - } - - guard let error = transform(error) else { return } - buffer.complete(completion: .failure(error)) - } - - cancelUpstream() - } - - func cancelUpstream() { - upstreamSubscription.kill() - } - - deinit { cancelUpstream() } -} -#endif diff --git a/Multiplatform/Shared/CombineExt/WIthLatestFrom.swift b/Multiplatform/Shared/CombineExt/WIthLatestFrom.swift deleted file mode 100644 index cda9b1c84..000000000 --- a/Multiplatform/Shared/CombineExt/WIthLatestFrom.swift +++ /dev/null @@ -1,238 +0,0 @@ -// -// WithLatestFrom.swift -// CombineExt -// -// Created by Shai Mishali on 29/08/2019. -// Copyright © 2020 Combine Community. All rights reserved. -// - -#if canImport(Combine) -import Combine - -// MARK: - Operator methods -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -public extension Publisher { - /// Merges two publishers into a single publisher by combining each value - /// from self with the latest value from the second publisher, if any. - /// - /// - parameter other: A second publisher source. - /// - parameter resultSelector: Function to invoke for each value from the self combined - /// with the latest value from the second source, if any. - /// - /// - returns: A publisher containing the result of combining each value of the self - /// with the latest value from the second publisher, if any, using the - /// specified result selector function. - func withLatestFrom(_ other: Other, - resultSelector: @escaping (Output, Other.Output) -> Result) - -> Publishers.WithLatestFrom { - return .init(upstream: self, second: other, resultSelector: resultSelector) - } - - /// Merges three publishers into a single publisher by combining each value - /// from self with the latest value from the second and third publisher, if any. - /// - /// - parameter other: A second publisher source. - /// - parameter other1: A third publisher source. - /// - parameter resultSelector: Function to invoke for each value from the self combined - /// with the latest value from the second and third source, if any. - /// - /// - returns: A publisher containing the result of combining each value of the self - /// with the latest value from the second and third publisher, if any, using the - /// specified result selector function. - func withLatestFrom(_ other: Other, - _ other1: Other1, - resultSelector: @escaping (Output, (Other.Output, Other1.Output)) -> Result) - -> Publishers.WithLatestFrom, Result> - where Other.Failure == Failure, Other1.Failure == Failure { - let combined = other.combineLatest(other1) - .eraseToAnyPublisher() - return .init(upstream: self, second: combined, resultSelector: resultSelector) - } - - /// Merges four publishers into a single publisher by combining each value - /// from self with the latest value from the second, third and fourth publisher, if any. - /// - /// - parameter other: A second publisher source. - /// - parameter other1: A third publisher source. - /// - parameter other2: A fourth publisher source. - /// - parameter resultSelector: Function to invoke for each value from the self combined - /// with the latest value from the second, third and fourth source, if any. - /// - /// - returns: A publisher containing the result of combining each value of the self - /// with the latest value from the second, third and fourth publisher, if any, using the - /// specified result selector function. - func withLatestFrom(_ other: Other, - _ other1: Other1, - _ other2: Other2, - resultSelector: @escaping (Output, (Other.Output, Other1.Output, Other2.Output)) -> Result) - -> Publishers.WithLatestFrom, Result> - where Other.Failure == Failure, Other1.Failure == Failure, Other2.Failure == Failure { - let combined = other.combineLatest(other1, other2) - .eraseToAnyPublisher() - return .init(upstream: self, second: combined, resultSelector: resultSelector) - } - - /// Upon an emission from self, emit the latest value from the - /// second publisher, if any exists. - /// - /// - parameter other: A second publisher source. - /// - /// - returns: A publisher containing the latest value from the second publisher, if any. - func withLatestFrom(_ other: Other) - -> Publishers.WithLatestFrom { - return .init(upstream: self, second: other) { $1 } - } - - /// Upon an emission from self, emit the latest value from the - /// second and third publisher, if any exists. - /// - /// - parameter other: A second publisher source. - /// - parameter other1: A third publisher source. - /// - /// - returns: A publisher containing the latest value from the second and third publisher, if any. - func withLatestFrom(_ other: Other, - _ other1: Other1) - -> Publishers.WithLatestFrom, (Other.Output, Other1.Output)> - where Other.Failure == Failure, Other1.Failure == Failure { - withLatestFrom(other, other1) { $1 } - } - - /// Upon an emission from self, emit the latest value from the - /// second, third and forth publisher, if any exists. - /// - /// - parameter other: A second publisher source. - /// - parameter other1: A third publisher source. - /// - parameter other2: A forth publisher source. - /// - /// - returns: A publisher containing the latest value from the second, third and forth publisher, if any. - func withLatestFrom(_ other: Other, - _ other1: Other1, - _ other2: Other2) - -> Publishers.WithLatestFrom, (Other.Output, Other1.Output, Other2.Output)> - where Other.Failure == Failure, Other1.Failure == Failure, Other2.Failure == Failure { - withLatestFrom(other, other1, other2) { $1 } - } -} - -// MARK: - Publisher -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -public extension Publishers { - struct WithLatestFrom: Publisher where Upstream.Failure == Other.Failure { - public typealias Failure = Upstream.Failure - public typealias ResultSelector = (Upstream.Output, Other.Output) -> Output - - private let upstream: Upstream - private let second: Other - private let resultSelector: ResultSelector - private var latestValue: Other.Output? - - init(upstream: Upstream, - second: Other, - resultSelector: @escaping ResultSelector) { - self.upstream = upstream - self.second = second - self.resultSelector = resultSelector - } - - public func receive(subscriber: S) where Failure == S.Failure, Output == S.Input { - subscriber.receive(subscription: Subscription(upstream: upstream, - downstream: subscriber, - second: second, - resultSelector: resultSelector)) - } - } -} - -// MARK: - Subscription -@available(OSX 10.15, iOS 13.0, tvOS 13.0, watchOS 6.0, *) -private extension Publishers.WithLatestFrom { - class Subscription: Combine.Subscription, CustomStringConvertible where Downstream.Input == Output, Downstream.Failure == Failure { - private let resultSelector: ResultSelector - private var sink: Sink? - - private let upstream: Upstream - private let downstream: Downstream - private let second: Other - - // Secondary (other) publisher - private var latestValue: Other.Output? - private var otherSubscription: Cancellable? - private var preInitialDemand = Subscribers.Demand.none - - init(upstream: Upstream, - downstream: Downstream, - second: Other, - resultSelector: @escaping ResultSelector) { - self.upstream = upstream - self.second = second - self.downstream = downstream - self.resultSelector = resultSelector - - trackLatestFromSecond { [weak self] in - guard let self = self else { return } - self.request(self.preInitialDemand) - self.preInitialDemand = .none - } - } - - func request(_ demand: Subscribers.Demand) { - guard latestValue != nil else { - preInitialDemand += demand - return - } - - self.sink?.demand(demand) - } - - // Create an internal subscription to the `Other` publisher, - // constantly tracking its latest value - private func trackLatestFromSecond(onInitialValue: @escaping () -> Void) { - var gotInitialValue = false - - let subscriber = AnySubscriber( - receiveSubscription: { [weak self] subscription in - self?.otherSubscription = subscription - subscription.request(.unlimited) - }, - receiveValue: { [weak self] value in - guard let self = self else { return .none } - self.latestValue = value - - if !gotInitialValue { - // When getting initial value, start pulling values - // from upstream in the main sink - self.sink = Sink(upstream: self.upstream, - downstream: self.downstream, - transformOutput: { [weak self] value in - guard let self = self, - let other = self.latestValue else { return nil } - - return self.resultSelector(value, other) - }, - transformFailure: { $0 }) - - // Signal initial value to start fulfilling downstream demand - gotInitialValue = true - onInitialValue() - } - - return .unlimited - }, - receiveCompletion: nil) - - self.second.subscribe(subscriber) - } - - var description: String { - return "WithLatestFrom.Subscription<\(Output.self), \(Failure.self)>" - } - - func cancel() { - sink = nil - otherSubscription?.cancel() - } - } -} -#endif diff --git a/Multiplatform/Shared/ErrorHandler.swift b/Multiplatform/Shared/ErrorHandler.swift deleted file mode 100644 index 63bcf26d6..000000000 --- a/Multiplatform/Shared/ErrorHandler.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// ErrorHandler.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/28/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import RSCore -import os.log - -struct ErrorHandler { - - private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") - - public static func log(_ error: Error) { - os_log(.error, log: self.log, "%@", error.localizedDescription) - } - -} diff --git a/Multiplatform/Shared/Images/ArticleIconImageLoader.swift b/Multiplatform/Shared/Images/ArticleIconImageLoader.swift deleted file mode 100644 index cc94abe57..000000000 --- a/Multiplatform/Shared/Images/ArticleIconImageLoader.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// ArticleIconImageLoader.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/1/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import Combine -import Account -import Articles - -final class ArticleIconImageLoader: ObservableObject { - - @Published var image: IconImage? - private var article: Article? - private var cancellables = Set() - - init() { - NotificationCenter.default.publisher(for: .FaviconDidBecomeAvailable).sink { [weak self] _ in - guard let self = self, let article = self.article else { return } - self.image = article.iconImage() - }.store(in: &cancellables) - - NotificationCenter.default.publisher(for: .WebFeedIconDidBecomeAvailable).sink { [weak self] note in - guard let self = self, let article = self.article, let noteFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed, noteFeed == article.webFeed else { - return - } - self.image = article.iconImage() - }.store(in: &cancellables) - - NotificationCenter.default.publisher(for: .AvatarDidBecomeAvailable).sink { [weak self] note in - guard let self = self, let article = self.article, let authors = article.authors, let avatarURL = note.userInfo?[UserInfoKey.url] as? String else { - return - } - for author in authors { - if author.avatarURL == avatarURL { - self.image = article.iconImage() - return - } - } - }.store(in: &cancellables) - } - - func loadImage(for article: Article) { - guard image == nil else { return } - self.article = article - image = article.iconImage() - } - -} diff --git a/Multiplatform/Shared/Images/FeedIconImageLoader.swift b/Multiplatform/Shared/Images/FeedIconImageLoader.swift deleted file mode 100644 index 833055310..000000000 --- a/Multiplatform/Shared/Images/FeedIconImageLoader.swift +++ /dev/null @@ -1,47 +0,0 @@ -// -// FeedIconImageLoader.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/29/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Combine -import Account - -final class FeedIconImageLoader: ObservableObject { - - @Published var image: IconImage? - private var feed: Feed? - private var cancellables = Set() - - init() { - NotificationCenter.default.publisher(for: .FaviconDidBecomeAvailable).sink { [weak self] _ in - self?.fetchImage() - }.store(in: &cancellables) - - - NotificationCenter.default.publisher(for: .WebFeedIconDidBecomeAvailable).sink { [weak self] note in - guard let feed = self?.feed as? WebFeed, let noteFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed, feed == noteFeed else { - return - } - self?.fetchImage() - }.store(in: &cancellables) - } - - func loadImage(for feed: Feed) { - guard image == nil else { return } - self.feed = feed - fetchImage() - } - -} - -private extension FeedIconImageLoader { - - func fetchImage() { - guard let feed = feed else { return } - image = IconImageCache.shared.imageForFeed(feed) - } -} diff --git a/Multiplatform/Shared/Images/IconImageView.swift b/Multiplatform/Shared/Images/IconImageView.swift deleted file mode 100644 index 822b3a7ad..000000000 --- a/Multiplatform/Shared/Images/IconImageView.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// IconImageView.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/29/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct IconImageView: View { - - @Environment(\.colorScheme) var colorScheme - var iconImage: IconImage - - var body: some View { - GeometryReader { proxy in - - let newSize = newImageSize(viewSize: proxy.size) - let tooShort = newSize.height < proxy.size.height - let indistinguishable = colorScheme == .dark ? iconImage.isDark : iconImage.isBright - let showBackground = (tooShort && !iconImage.isSymbol) || indistinguishable - - Group { - Image(rsImage: iconImage.image) - .resizable() - .scaledToFit() - .frame(width: newSize.width, height: newSize.height, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) - } - .frame(width: proxy.size.width, height: proxy.size.height, alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) - .background(showBackground ? AppAssets.iconBackgroundColor : nil) - .cornerRadius(4) - - } - } - - func newImageSize(viewSize: CGSize) -> CGSize { - let imageSize = iconImage.image.size - let newSize: CGSize - - if imageSize.height == imageSize.width { - if imageSize.height >= viewSize.height { - newSize = CGSize(width: viewSize.width, height: viewSize.height) - } else { - newSize = CGSize(width: imageSize.width, height: imageSize.height) - } - } else if imageSize.height > imageSize.width { - let factor = viewSize.height / imageSize.height - let width = imageSize.width * factor - newSize = CGSize(width: width, height: viewSize.height) - } else { - let factor = viewSize.width / imageSize.width - let height = imageSize.height * factor - newSize = CGSize(width: viewSize.width, height: height) - } - - return newSize - } - -} diff --git a/Multiplatform/Shared/Inspector/InspectorModel.swift b/Multiplatform/Shared/Inspector/InspectorModel.swift deleted file mode 100644 index 63d774cef..000000000 --- a/Multiplatform/Shared/Inspector/InspectorModel.swift +++ /dev/null @@ -1,119 +0,0 @@ -// -// InspectorModel.swift -// NetNewsWire -// -// Created by Stuart Breckenridge on 18/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import UserNotifications -import RSCore -import Account -#if os(macOS) -import AppKit -#else -import UIKit -#endif - - -class InspectorModel: ObservableObject { - - // Global Inspector Variables - @Published var editedName: String = "" - @Published var shouldUpdate: Bool = false - - // Account Inspector Variables - @Published var notificationSettings: UNNotificationSettings? - @Published var notifyAboutNewArticles: Bool = false { - didSet { - updateNotificationSettings() - } - } - @Published var alwaysShowReaderView: Bool = false { - didSet { - selectedWebFeed?.isArticleExtractorAlwaysOn = alwaysShowReaderView - } - } - @Published var accountIsActive: Bool = false { - didSet { - selectedAccount?.isActive = accountIsActive - } - } - @Published var showHomePage: Bool = false // iOS only - - // Private Variables - private let centre = UNUserNotificationCenter.current() - private var selectedWebFeed: WebFeed? - private var selectedFolder: Folder? - private var selectedAccount: Account? - - init() { - getNotificationSettings() - } - - func getNotificationSettings() { - centre.getNotificationSettings { (settings) in - DispatchQueue.main.async { - self.notificationSettings = settings - if settings.authorizationStatus == .authorized { - #if os(macOS) - NSApplication.shared.registerForRemoteNotifications() - #else - UIApplication.shared.registerForRemoteNotifications() - #endif - } - } - } - } - - func configure(with feed: WebFeed) { - selectedWebFeed = feed - notifyAboutNewArticles = selectedWebFeed?.isNotifyAboutNewArticles ?? false - alwaysShowReaderView = selectedWebFeed?.isArticleExtractorAlwaysOn ?? false - editedName = feed.nameForDisplay - } - - func configure(with folder: Folder) { - selectedFolder = folder - editedName = folder.nameForDisplay - } - - func configure(with account: Account) { - selectedAccount = account - editedName = account.nameForDisplay - accountIsActive = account.isActive - } - - func updateNotificationSettings() { - guard let feed = selectedWebFeed, - let settings = notificationSettings - else { return } - if settings.authorizationStatus == .denied { - notifyAboutNewArticles = false - } else if settings.authorizationStatus == .authorized { - feed.isNotifyAboutNewArticles = notifyAboutNewArticles - } else { - UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { [weak self] (granted, error) in - self?.updateNotificationSettings() - if granted { - DispatchQueue.main.async { - self?.selectedWebFeed!.isNotifyAboutNewArticles = self?.notifyAboutNewArticles - #if os(macOS) - NSApplication.shared.registerForRemoteNotifications() - #else - UIApplication.shared.registerForRemoteNotifications() - #endif - } - } else { - DispatchQueue.main.async { - self?.notifyAboutNewArticles = false - } - } - } - } - } - - -} - diff --git a/Multiplatform/Shared/Inspector/InspectorPlatformModifier.swift b/Multiplatform/Shared/Inspector/InspectorPlatformModifier.swift deleted file mode 100644 index f78c6f4fb..000000000 --- a/Multiplatform/Shared/Inspector/InspectorPlatformModifier.swift +++ /dev/null @@ -1,43 +0,0 @@ -// -// InspectorPlatformModifier.swift -// NetNewsWire -// -// Created by Stuart Breckenridge on 18/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct InspectorPlatformModifier: ViewModifier { - - @Environment(\.presentationMode) var presentationMode - @Binding var shouldUpdate: Bool - - @ViewBuilder func body(content: Content) -> some View { - - #if os(macOS) - content - .textFieldStyle(RoundedBorderTextFieldStyle()) - .frame(width: 300) - .padding() - #else - NavigationView { - content - .listStyle(InsetGroupedListStyle()) - .navigationBarTitle("Inspector", displayMode: .inline) - .navigationBarItems( - leading: - Button("Cancel", action: { - presentationMode.wrappedValue.dismiss() - }), - trailing: - Button("Done", action: { - shouldUpdate = true - }) - ) - } - #endif - } - - -} diff --git a/Multiplatform/Shared/Inspector/InspectorView.swift b/Multiplatform/Shared/Inspector/InspectorView.swift deleted file mode 100644 index 3111cd3ab..000000000 --- a/Multiplatform/Shared/Inspector/InspectorView.swift +++ /dev/null @@ -1,259 +0,0 @@ -// -// InspectorView.swift -// NetNewsWire -// -// Created by Stuart Breckenridge on 18/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import RSCore -import Account - -struct InspectorView: View { - - @Environment(\.presentationMode) var presentationMode - @StateObject private var feedIconImageLoader = FeedIconImageLoader() - @StateObject private var inspectorModel = InspectorModel() - var sidebarItem: SidebarItem - - var body: some View { - switch sidebarItem.representedType { - case .webFeed: - WebFeedInspectorView - .modifier(InspectorPlatformModifier(shouldUpdate: $inspectorModel.shouldUpdate)) - case .folder: - FolderInspectorView - .modifier(InspectorPlatformModifier(shouldUpdate: $inspectorModel.shouldUpdate)) - case .account: - AccountInspectorView - .modifier(InspectorPlatformModifier(shouldUpdate: $inspectorModel.shouldUpdate)) - default: - EmptyView() - } - } - - // MARK: WebFeed Inspector - - - var WebFeedInspectorView: some View { - Form { - Section(header: webFeedHeader) { - TextField("", text: $inspectorModel.editedName) - } - - #if os(macOS) - Divider() - #endif - - Section(content: { - Toggle("Notify About New Articles", isOn: $inspectorModel.notifyAboutNewArticles) - Toggle("Always Show Reader View", isOn: $inspectorModel.alwaysShowReaderView) - }) - - if let homePageURL = (sidebarItem.feed as? WebFeed)?.homePageURL { - #if os(macOS) - Divider() - #endif - - Section(header: Text("Home Page URL")) { - HStack { - Text(verbatim: homePageURL) - .fixedSize(horizontal: false, vertical: true) - Spacer() - AppAssets.openInBrowserImage - .foregroundColor(.accentColor) - } - .onTapGesture { - if let url = URL(string: homePageURL) { - #if os(macOS) - NSWorkspace.shared.open(url) - #else - inspectorModel.showHomePage = true - #endif - } - } - .contextMenu(ContextMenu(menuItems: { - Button(action: { - #if os(macOS) - URLPasteboardWriter.write(urlString: homePageURL, to: NSPasteboard.general) - #else - UIPasteboard.general.string = homePageURL - #endif - }, label: { - Text("Copy Home Page URL") - }) - })) - .sheet(isPresented: $inspectorModel.showHomePage, onDismiss: { inspectorModel.showHomePage = false }) { - #if os(macOS) - EmptyView() - #else - SafariView(url: URL(string: (sidebarItem.feed as! WebFeed).homePageURL!)!) - #endif - } - } - } - - #if os(macOS) - Divider() - #endif - - Section(header: Text("Feed URL")) { - VStack { -// #if os(macOS) -// Spacer() // This shouldn't be necessary, but for some reason macOS doesn't put the space in itself -// #endif - Text(verbatim: (sidebarItem.feed as? WebFeed)?.url ?? "") - .fixedSize(horizontal: false, vertical: true) - .contextMenu(ContextMenu(menuItems: { - Button(action: { - if let urlString = (sidebarItem.feed as? WebFeed)?.url { - #if os(macOS) - URLPasteboardWriter.write(urlString: urlString, to: NSPasteboard.general) - #else - UIPasteboard.general.string = urlString - #endif - } - }, label: { - Text("Copy Feed URL") - }) - })) - } - } - - #if os(macOS) - HStack { - Spacer() - Button("Cancel", action: { - presentationMode.wrappedValue.dismiss() - }) - Button("Done", action: { - inspectorModel.shouldUpdate = true - }) - }.padding([.top, .bottom], 20) - #endif - } - .onAppear { - inspectorModel.configure(with: sidebarItem.feed as! WebFeed) - feedIconImageLoader.loadImage(for: sidebarItem.feed!) - }.onReceive(inspectorModel.$shouldUpdate) { value in - if value == true { - if inspectorModel.editedName.trimmingWhitespace.count > 0 { - (sidebarItem.feed as? WebFeed)?.rename(to: inspectorModel.editedName.trimmingWhitespace) { _ in } - } - presentationMode.wrappedValue.dismiss() - } - } - } - - var webFeedHeader: some View { - HStack(alignment: .center) { - Spacer() - if let image = feedIconImageLoader.image { - IconImageView(iconImage: image) - .frame(width: 50, height: 50) - } - Spacer() - }.padding(.top, 20) - } - - - // MARK: Folder Inspector - - var FolderInspectorView: some View { - Form { - Section(header: folderHeader) { - TextField("", text: $inspectorModel.editedName) - } - - #if os(macOS) - HStack { - Spacer() - Button("Cancel", action: { - presentationMode.wrappedValue.dismiss() - }) - Button("Done", action: { - inspectorModel.shouldUpdate = true - }) - }.padding([.top, .bottom]) - #endif - } - .onAppear { - inspectorModel.configure(with: sidebarItem.represented as! Folder) - feedIconImageLoader.loadImage(for: sidebarItem.feed!) - } - .onReceive(inspectorModel.$shouldUpdate) { value in - if value == true { - if inspectorModel.editedName.trimmingWhitespace.count > 0 { - (sidebarItem.feed as? Folder)?.rename(to: inspectorModel.editedName.trimmingWhitespace) { _ in } - } - presentationMode.wrappedValue.dismiss() - } - } - } - - var folderHeader: some View { - HStack(alignment: .center) { - Spacer() - if let image = feedIconImageLoader.image { - IconImageView(iconImage: image) - .frame(width: 50, height: 50) - } - Spacer() - }.padding(.top, 20) - } - - - // MARK: Account Inspector - - var AccountInspectorView: some View { - Form { - Section(header: accountHeader) { - TextField("", text: $inspectorModel.editedName) - Toggle("Active", isOn: $inspectorModel.accountIsActive) - } - - #if os(macOS) - HStack { - Spacer() - Button("Cancel", action: { - presentationMode.wrappedValue.dismiss() - }).keyboardShortcut(.cancelAction) - Button("Done", action: { - inspectorModel.shouldUpdate = true - }).keyboardShortcut(.defaultAction) - }.padding(.top) - #endif - } - .onAppear { - inspectorModel.configure(with: sidebarItem.represented as! Account) - } - .onReceive(inspectorModel.$shouldUpdate) { value in - if value == true { - if inspectorModel.editedName.trimmingWhitespace.count > 0 { - (sidebarItem.represented as? Account)?.name = inspectorModel.editedName - } else { - (sidebarItem.represented as? Account)?.name = nil - } - presentationMode.wrappedValue.dismiss() - } - } - } - - var accountHeader: some View { - HStack(alignment: .center) { - Spacer() - if let image = (sidebarItem.represented as? Account)?.smallIcon?.image { - Image(rsImage: image) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 50, height: 50) - } - Spacer() - }.padding() - } - - -} - - diff --git a/Multiplatform/Shared/MainApp.swift b/Multiplatform/Shared/MainApp.swift deleted file mode 100644 index e3b3dc473..000000000 --- a/Multiplatform/Shared/MainApp.swift +++ /dev/null @@ -1,135 +0,0 @@ -// -// MainApp.swift -// Shared -// -// Created by Maurice Parker on 6/27/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -@main -struct MainApp: App { - - #if os(macOS) - @NSApplicationDelegateAdaptor(AppDelegate.self) private var delegate - @State private var selectedPane: MacPreferencePane = .general - #endif - #if os(iOS) - @UIApplicationDelegateAdaptor(AppDelegate.self) private var delegate - #endif - - @StateObject private var refreshProgress = RefreshProgressModel() - @StateObject private var defaults = AppDefaults.shared - - @SceneBuilder var body: some Scene { - #if os(macOS) - WindowGroup { - SceneNavigationView() - .frame(minWidth: 600, idealWidth: 1000, maxWidth: .infinity, minHeight: 600, idealHeight: 700, maxHeight: .infinity) - .onAppear { refreshProgress.startup() } - .environmentObject(refreshProgress) - .environmentObject(defaults) - .preferredColorScheme(AppDefaults.userInterfaceColorScheme) - } - .windowToolbarStyle(UnifiedWindowToolbarStyle()) - .commands { - SidebarCommands() - CommandGroup(after: .newItem, addition: { - Button("New Feed", action: {}) - .keyboardShortcut("N") - Button("New Folder", action: {}) - .keyboardShortcut("N", modifiers: [.shift, .command]) - Button("Refresh", action: {}) - .keyboardShortcut("R") - }) - CommandMenu("Subscriptions", content: { - Button("Import Subscriptions", action: {}) - .keyboardShortcut("I", modifiers: [.shift, .command]) - Button("Import NNW 3 Subscriptions", action: {}) - .keyboardShortcut("O", modifiers: [.shift, .command]) - Button("Export Subscriptions", action: {}) - .keyboardShortcut("E", modifiers: [.shift, .command]) - }) - CommandMenu("Go", content: { - Button("Next Unread", action: {}) - .keyboardShortcut("/", modifiers: [.command]) - Button("Today", action: {}) - .keyboardShortcut("1", modifiers: [.command]) - Button("All Unread", action: {}) - .keyboardShortcut("2", modifiers: [.command]) - Button("Starred", action: {}) - .keyboardShortcut("3", modifiers: [.command]) - }) - CommandMenu("Article", content: { - Button("Mark as Read", action: {}) - .keyboardShortcut("U", modifiers: [.shift, .command]) - Button("Mark All as Read", action: {}) - .keyboardShortcut("K", modifiers: [.command]) - Button("Mark Older as Read", action: {}) - .keyboardShortcut("K", modifiers: [.shift, .command]) - Button("Mark as Starred", action: {}) - .keyboardShortcut("L", modifiers: [.shift, .command]) - Button("Open in Browser", action: {}) - .keyboardShortcut(.rightArrow, modifiers: [.command]) - }) - CommandGroup(after: .help, addition: { - Button("Release Notes", action: { - NSWorkspace.shared.open(URL.releaseNotes) - }) - .keyboardShortcut("V", modifiers: [.shift, .command]) - }) - } - - // Mac Preferences - Settings { - TabView(selection: $selectedPane) { - GeneralPreferencesView() - .tabItem { - Image(systemName: "gearshape") - .font(.title2) - Text("General") - } - .tag(MacPreferencePane.general) - - AccountsPreferencesView() - .tabItem { - Image(systemName: "at") - .font(.title2) - Text("Accounts") - } - .tag(MacPreferencePane.accounts) - - LayoutPreferencesView() - .tabItem { - Image(systemName: "eyeglasses") - .font(.title2) - Text("Viewing") - } - .tag(MacPreferencePane.viewing) - - AdvancedPreferencesView() - .tabItem { - Image(systemName: "scale.3d") - .font(.title2) - Text("Advanced") - } - .tag(MacPreferencePane.advanced) - } - .preferredColorScheme(AppDefaults.userInterfaceColorScheme) - .frame(width: 500) - .padding() - } - #endif - - #if os(iOS) - WindowGroup { - SceneNavigationView() - .onAppear { refreshProgress.startup() } - .environmentObject(refreshProgress) - .environmentObject(defaults) - .preferredColorScheme(AppDefaults.userInterfaceColorScheme) - } - #endif - } -} diff --git a/Multiplatform/Shared/Previews/PreviewArticles.swift b/Multiplatform/Shared/Previews/PreviewArticles.swift deleted file mode 100644 index 75cc895d9..000000000 --- a/Multiplatform/Shared/Previews/PreviewArticles.swift +++ /dev/null @@ -1,58 +0,0 @@ -// -// PreviewArticles.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/1/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import Articles - -enum PreviewArticles { - - static var basicUnread: Article { - return makeBasicArticle(read: false, starred: false) - } - - static var basicRead: Article { - return makeBasicArticle(read: true, starred: false) - } - - static var basicStarred: Article { - return makeBasicArticle(read: false, starred: true) - } - -} - -private extension PreviewArticles { - - static var shortTitle: String { - return "Short article title" - } - - static var shortSummary: String { - return "Summary of article to be shown after title." - } - - static func makeBasicArticle(read: Bool, starred: Bool) -> Article { - let articleID = "prototype" - let status = ArticleStatus(articleID: articleID, read: read, starred: starred, dateArrived: Date()) - return Article(accountID: articleID, - articleID: articleID, - webFeedID: articleID, - uniqueID: articleID, - title: shortTitle, - contentHTML: nil, - contentText: nil, - url: nil, - externalURL: nil, - summary: shortSummary, - imageURL: nil, - datePublished: Date(), - dateModified: nil, - authors: nil, - status: status) - } - -} diff --git a/Multiplatform/Shared/RefreshProgressModel.swift b/Multiplatform/Shared/RefreshProgressModel.swift deleted file mode 100644 index 26e5c4a02..000000000 --- a/Multiplatform/Shared/RefreshProgressModel.swift +++ /dev/null @@ -1,86 +0,0 @@ -// -// RefreshProgressModel.swift -// NetNewsWire -// -// Created by Phil Viso on 7/2/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Combine -import RSCore -import Account - -class RefreshProgressModel: ObservableObject { - - enum State { - case refreshProgress(Float) - case lastRefreshDateText(String) - case none - } - - @Published var state = State.none - - private static var dateFormatter: RelativeDateTimeFormatter = { - let formatter = RelativeDateTimeFormatter() - formatter.dateTimeStyle = .named - - return formatter - }() - - private static let lastRefreshDateTextUpdateInterval = 60 - private static let lastRefreshDateTextRelativeDateFormattingThreshold = 60.0 - - func startup() { - updateState() - observeRefreshProgress() - scheduleLastRefreshDateTextUpdate() - } - - // MARK: Observing account changes - - private func observeRefreshProgress() { - NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshProgressDidChange), name: .AccountRefreshProgressDidChange, object: nil) - } - - // MARK: Refreshing state - - @objc private func accountRefreshProgressDidChange() { - CoalescingQueue.standard.add(self, #selector(updateState)) - } - - @objc private func updateState() { - let progress = AccountManager.shared.combinedRefreshProgress - - if !progress.isComplete { - let fractionCompleted = Float(progress.numberCompleted) / Float(progress.numberOfTasks) - self.state = .refreshProgress(fractionCompleted) - } else if let lastRefreshDate = AccountManager.shared.lastArticleFetchEndTime { - let text = localizedLastRefreshText(lastRefreshDate: lastRefreshDate) - self.state = .lastRefreshDateText(text) - } else { - self.state = .none - } - } - - private func scheduleLastRefreshDateTextUpdate() { - DispatchQueue.main.asyncAfter(deadline: .now() + .seconds(Self.lastRefreshDateTextUpdateInterval)) { - self.updateState() - self.scheduleLastRefreshDateTextUpdate() - } - } - - private func localizedLastRefreshText(lastRefreshDate: Date) -> String { - let now = Date() - - if now > lastRefreshDate.addingTimeInterval(Self.lastRefreshDateTextRelativeDateFormattingThreshold) { - let localizedDate = Self.dateFormatter.localizedString(for: lastRefreshDate, relativeTo: now) - let formatString = NSLocalizedString("Updated %@", comment: "Updated") as NSString - - return NSString.localizedStringWithFormat(formatString, localizedDate) as String - } else { - return NSLocalizedString("Updated Just Now", comment: "Updated Just Now") - } - } - -} diff --git a/Multiplatform/Shared/SceneModel.swift b/Multiplatform/Shared/SceneModel.swift deleted file mode 100644 index f644c391d..000000000 --- a/Multiplatform/Shared/SceneModel.swift +++ /dev/null @@ -1,215 +0,0 @@ -// -// SceneModel.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/28/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import Combine -import Account -import Articles -import RSCore - -final class SceneModel: ObservableObject { - - @Published var markAllAsReadButtonState: Bool? - @Published var nextUnreadButtonState: Bool? - @Published var readButtonState: Bool? - @Published var starButtonState: Bool? - @Published var extractorButtonState: ArticleExtractorButtonState? - @Published var openInBrowserButtonState: Bool? - @Published var shareButtonState: Bool? - @Published var accountSyncErrors: [AccountSyncError] = [] - - var selectedArticles: [Article] { - timelineModel.selectedArticles - } - - private var refreshProgressModel: RefreshProgressModel? = nil - private var articleIconSchemeHandler: ArticleIconSchemeHandler? = nil - - private(set) var webViewProvider: WebViewProvider? = nil - private(set) lazy var sidebarModel = SidebarModel(delegate: self) - private(set) lazy var timelineModel = TimelineModel(delegate: self) - - private var cancellables = Set() - - // MARK: Initialization API - - /// Prepares the SceneModel to be used in the views - func startup() { - self.articleIconSchemeHandler = ArticleIconSchemeHandler(sceneModel: self) - self.webViewProvider = WebViewProvider(articleIconSchemeHandler: self.articleIconSchemeHandler!) - - subscribeToAccountSyncErrors() - subscribeToToolbarChangeEvents() - } - - // MARK: Navigation API - - /// Goes to the next unread item found in Sidebar and Timeline order, top to bottom - func goToNextUnread() { - if !timelineModel.goToNextUnread() { - timelineModel.selectNextUnreadSubject.send(true) - sidebarModel.selectNextUnread.send() - } - } - - // MARK: Article Management API - - /// Marks all the articles in the Timeline as read - func markAllAsRead() { - timelineModel.markAllAsRead() - } - - /// Toggles the read status for the selected articles - func toggleReadStatusForSelectedArticles() { - timelineModel.toggleReadStatusForSelectedArticles() - } - - /// Toggles the star status for the selected articles - func toggleStarredStatusForSelectedArticles() { - timelineModel.toggleStarredStatusForSelectedArticles() - } - - /// Opens the selected article in an external browser - func openSelectedArticleInBrowser() { - timelineModel.openSelectedArticleInBrowser() - } - - /// Retrieves the article before the given article in the Timeline - func findPrevArticle(_ article: Article) -> Article? { - return timelineModel.findPrevArticle(article) - } - - /// Retrieves the article after the given article in the Timeline - func findNextArticle(_ article: Article) -> Article? { - return timelineModel.findNextArticle(article) - } - - /// Returns the article with the given articleID - func articleFor(_ articleID: String) -> Article? { - return timelineModel.articleFor(articleID) - } - -} - -// MARK: SidebarModelDelegate - -extension SceneModel: SidebarModelDelegate { - - func unreadCount(for feed: Feed) -> Int { - // TODO: Get the count from the timeline if Feed is the current timeline - return feed.unreadCount - } - -} - -// MARK: TimelineModelDelegate - -extension SceneModel: TimelineModelDelegate { - - var selectedFeedsPublisher: AnyPublisher<[Feed], Never>? { - return sidebarModel.selectedFeedsPublisher - } - - func timelineRequestedWebFeedSelection(_: TimelineModel, webFeed: WebFeed) { - } - -} - -// MARK: Private - -private extension SceneModel { - - // MARK: Subscriptions - func subscribeToToolbarChangeEvents() { - guard let selectedArticlesPublisher = timelineModel.selectedArticlesPublisher else { return } - - NotificationCenter.default.publisher(for: .UnreadCountDidChange) - .compactMap { $0.object as? AccountManager } - .sink { [weak self] accountManager in - self?.updateNextUnreadButtonState(accountManager: accountManager) - }.store(in: &cancellables) - - let blankNotification = Notification(name: .StatusesDidChange) - let statusesDidChangePublisher = NotificationCenter.default.publisher(for: .StatusesDidChange).prepend(blankNotification) - - statusesDidChangePublisher - .combineLatest(selectedArticlesPublisher) - .sink { [weak self] _, selectedArticles in - self?.updateArticleButtonsState(selectedArticles: selectedArticles) - } - .store(in: &cancellables) - - statusesDidChangePublisher - .combineLatest(timelineModel.articlesSubject) - .sink { [weak self] _, articles in - self?.updateMarkAllAsReadButtonsState(articles: articles) - } - .store(in: &cancellables) - } - - func subscribeToAccountSyncErrors() { - NotificationCenter.default.publisher(for: .AccountsDidFailToSyncWithErrors) - .sink { [weak self] notification in - guard let syncErrors = notification.userInfo?[Account.UserInfoKey.syncErrors] as? [AccountSyncError] else { - return - } - self?.accountSyncErrors = syncErrors - }.store(in: &cancellables) - } - - // MARK: Button State Updates - - func updateNextUnreadButtonState(accountManager: AccountManager) { - if accountManager.unreadCount > 0 { - self.nextUnreadButtonState = false - } else { - self.nextUnreadButtonState = nil - } - } - - func updateMarkAllAsReadButtonsState(articles: [Article]) { - if articles.canMarkAllAsRead() { - markAllAsReadButtonState = false - } else { - markAllAsReadButtonState = nil - } - } - - func updateArticleButtonsState(selectedArticles: [Article]) { - guard !selectedArticles.isEmpty else { - readButtonState = nil - starButtonState = nil - openInBrowserButtonState = nil - shareButtonState = nil - return - } - - if selectedArticles.anyArticleIsUnread() { - readButtonState = true - } else if selectedArticles.anyArticleIsReadAndCanMarkUnread() { - readButtonState = false - } else { - readButtonState = nil - } - - if selectedArticles.anyArticleIsUnstarred() { - starButtonState = false - } else { - starButtonState = true - } - - if selectedArticles.count == 1, selectedArticles.first?.preferredLink != nil { - openInBrowserButtonState = true - shareButtonState = true - } else { - openInBrowserButtonState = nil - shareButtonState = nil - } - } - -} diff --git a/Multiplatform/Shared/SceneNavigationModel.swift b/Multiplatform/Shared/SceneNavigationModel.swift deleted file mode 100644 index f613210f7..000000000 --- a/Multiplatform/Shared/SceneNavigationModel.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// SceneNavigationModel.swift -// NetNewsWire -// -// Created by Stuart Breckenridge on 13/8/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation - - -class SceneNavigationModel: ObservableObject { - @Published var sheetToShow: SidebarSheets = .none { - didSet { - sheetToShow != .none ? (showSheet = true) : (showSheet = false) - } - } - @Published var showSheet = false - @Published var showShareSheet = false - @Published var showAccountSyncErrorAlert = false -} diff --git a/Multiplatform/Shared/SceneNavigationView.swift b/Multiplatform/Shared/SceneNavigationView.swift deleted file mode 100644 index 190164638..000000000 --- a/Multiplatform/Shared/SceneNavigationView.swift +++ /dev/null @@ -1,219 +0,0 @@ -// -// SceneNavigationView.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/28/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -#if os(macOS) -import AppKit -#endif - - -struct SceneNavigationView: View { - - @StateObject private var sceneModel = SceneModel() - @StateObject private var sceneNavigationModel = SceneNavigationModel() - - #if os(iOS) - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - #endif - - var body: some View { - NavigationView { - #if os(macOS) - SidebarContainerView() - .frame(minWidth: 100, idealWidth: 150, maxHeight: .infinity) - #else - SidebarContainerView() - #endif - - #if os(iOS) - if horizontalSizeClass != .compact { - TimelineContainerView() - } - #else - TimelineContainerView() - #endif - - ArticleContainerView() - } - .environmentObject(sceneModel) - .onAppear { - sceneModel.startup() - } - .onReceive(sceneModel.$accountSyncErrors) { errors in - if errors.count == 0 { - sceneNavigationModel.showAccountSyncErrorAlert = false - } else { - if errors.count > 1 { - sceneNavigationModel.showAccountSyncErrorAlert = true - } else { - sceneNavigationModel.sheetToShow = .fixCredentials - } - } - } - .sheet(isPresented: $sceneNavigationModel.showSheet, - onDismiss: { - sceneNavigationModel.sheetToShow = .none - sceneModel.accountSyncErrors = [] - }) { - if sceneNavigationModel.sheetToShow == .web { - AddWebFeedView(isPresented: $sceneNavigationModel.showSheet) - } - if sceneNavigationModel.sheetToShow == .folder { - AddFolderView(isPresented: $sceneNavigationModel.showSheet) - } - #if os(iOS) - if sceneNavigationModel.sheetToShow == .settings { - SettingsView() - } - #endif - if sceneNavigationModel.sheetToShow == .fixCredentials { - FixAccountCredentialView(accountSyncError: sceneModel.accountSyncErrors[0]) - } - } - .alert(isPresented: $sceneNavigationModel.showAccountSyncErrorAlert, content: { - #if os(macOS) - return Alert(title: Text("Account Sync Error"), - message: Text("The following accounts failed to sync: ") + Text(sceneModel.accountSyncErrors.map({ $0.account.nameForDisplay }).joined(separator: ", ")) + Text(". You can update credentials in Preferences"), - dismissButton: .default(Text("Dismiss"))) - #else - return Alert(title: Text("Account Sync Error"), - message: Text("The following accounts failed to sync: ") + Text(sceneModel.accountSyncErrors.map({ $0.account.nameForDisplay }).joined(separator: ", ")) + Text(". You can update credentials in Settings"), - primaryButton: .default(Text("Show Settings"), action: { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { - sceneNavigationModel.sheetToShow = .settings - }) - - }), - secondaryButton: .cancel(Text("Dismiss"))) - - #endif - }) - .toolbar { - - #if os(macOS) - ToolbarItem(placement: .navigation) { - Button { - NSApp.keyWindow?.firstResponder?.tryToPerform(#selector(NSSplitViewController.toggleSidebar(_:)), with: nil) - } label: { - AppAssets.sidebarToggleImage - } - .help("Toggle Sidebar") - } - ToolbarItem() { - Menu { - Button("Add Web Feed", action: { sceneNavigationModel.sheetToShow = .web }) - Button("Add Reddit Feed", action: { }) - Button("Add Twitter Feed", action: { }) - Button("Add Folder", action: { sceneNavigationModel.sheetToShow = .folder}) - } label : { - AppAssets.addMenuImage - } - } - ToolbarItem { - Button { - AccountManager.shared.refreshAll(completion: nil) - - } label: { - AppAssets.refreshImage - } - .help("Refresh").padding(.trailing, 40) - } - ToolbarItem { - Button { - sceneModel.markAllAsRead() - } label: { - AppAssets.markAllAsReadImagePNG - .offset(y: 7) - } - .disabled(sceneModel.markAllAsReadButtonState == nil) - .help("Mark All as Read") - } -// ToolbarItem { -// MacSearchField() -// .frame(width: 200) -// } - ToolbarItem { - Button { - sceneModel.goToNextUnread() - } label: { - AppAssets.nextUnreadArticleImage - } - .disabled(sceneModel.nextUnreadButtonState == nil) - .help("Go to Next Unread").padding(.trailing, 40) - } - ToolbarItem { - Button { - sceneModel.toggleReadStatusForSelectedArticles() - } label: { - if sceneModel.readButtonState ?? false { - AppAssets.readClosedImage - } else { - AppAssets.readOpenImage - } - } - .disabled(sceneModel.readButtonState == nil) - .help(sceneModel.readButtonState ?? false ? "Mark as Unread" : "Mark as Read") - } - ToolbarItem { - Button { - sceneModel.toggleStarredStatusForSelectedArticles() - } label: { - if sceneModel.starButtonState ?? false { - AppAssets.starClosedImage - } else { - AppAssets.starOpenImage - } - } - .disabled(sceneModel.starButtonState == nil) - .help(sceneModel.starButtonState ?? false ? "Mark as Unstarred" : "Mark as Starred") - } - ToolbarItem { - Button { - } label: { - AppAssets.articleExtractorOff - } - .disabled(sceneModel.extractorButtonState == nil) - .help("Show Reader View") - } - ToolbarItem { - Button { - sceneModel.openSelectedArticleInBrowser() - } label: { - AppAssets.openInBrowserImage - } - .disabled(sceneModel.openInBrowserButtonState == nil) - .help("Open in Browser") - } - ToolbarItem { - ZStack { - if sceneNavigationModel.showShareSheet { - SharingServiceView(articles: sceneModel.selectedArticles, showing: $sceneNavigationModel.showShareSheet) - .frame(width: 20, height: 20) - } - Button { - sceneNavigationModel.showShareSheet = true - } label: { - AppAssets.shareImage - } - } - .disabled(sceneModel.shareButtonState == nil) - .help("Share") - } - #endif - } - } - -} - -struct NavigationView_Previews: PreviewProvider { - static var previews: some View { - SceneNavigationView() - .environmentObject(SceneModel()) - } -} diff --git a/Multiplatform/Shared/Sidebar/SidebarContainerView.swift b/Multiplatform/Shared/Sidebar/SidebarContainerView.swift deleted file mode 100644 index 4be6cdc71..000000000 --- a/Multiplatform/Shared/Sidebar/SidebarContainerView.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// SidebarContainerView.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/28/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct SidebarContainerView: View { - - @Environment(\.undoManager) var undoManager - @EnvironmentObject private var sceneModel: SceneModel - - @State var sidebarItems = [SidebarItem]() - - var body: some View { - SidebarView(sidebarItems: $sidebarItems) - .modifier(SidebarToolbarModifier()) - .modifier(SidebarListStyleModifier()) - .environmentObject(sceneModel.sidebarModel) - .onAppear { - sceneModel.sidebarModel.undoManager = undoManager - } - .onReceive(sceneModel.sidebarModel.sidebarItemsPublisher!) { newItems in - withAnimation { - sidebarItems = newItems - } - } - } - -} - -struct SidebarContainerView_Previews: PreviewProvider { - static var previews: some View { - SidebarContainerView() - .environmentObject(SceneModel()) - } -} diff --git a/Multiplatform/Shared/Sidebar/SidebarContextMenu.swift b/Multiplatform/Shared/Sidebar/SidebarContextMenu.swift deleted file mode 100644 index 21971884a..000000000 --- a/Multiplatform/Shared/Sidebar/SidebarContextMenu.swift +++ /dev/null @@ -1,184 +0,0 @@ -// -// SidebarContextMenu.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/17/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import RSCore -import Account - -struct SidebarContextMenu: View { - - @Environment(\.undoManager) var undoManager - @Environment(\.openURL) var openURL - @EnvironmentObject private var sidebarModel: SidebarModel - @Binding var showInspector: Bool - var sidebarItem: SidebarItem - - - var body: some View { - // MARK: Account Context Menu - if sidebarItem.representedType == .account { - Button { - showInspector = true - } label: { - Text("Get Info") - #if os(iOS) - AppAssets.getInfoImage - #endif - } - Button { - sidebarModel.markAllAsReadInAccount.send(sidebarItem.represented as! Account) - } label: { - Text("Mark All As Read") - #if os(iOS) - AppAssets.markAllAsReadImage - #endif - } - } - - // MARK: Pseudofeed Context Menu - if sidebarItem.representedType == .pseudoFeed { - Button { - guard let feed = sidebarItem.feed else { - return - } - sidebarModel.markAllAsReadInFeed.send(feed) - } label: { - Text("Mark All As Read") - #if os(iOS) - AppAssets.markAllAsReadImage - #endif - } - } - - // MARK: Webfeed Context Menu - if sidebarItem.representedType == .webFeed { - Button { - showInspector = true - } label: { - Text("Get Info") - #if os(iOS) - AppAssets.getInfoImage - #endif - } - Button { - guard let feed = sidebarItem.feed else { - return - } - sidebarModel.markAllAsReadInFeed.send(feed) - } label: { - Text("Mark All As Read") - #if os(iOS) - AppAssets.markAllAsReadImage - #endif - } - Divider() - Button { - guard let homepage = (sidebarItem.feed as? WebFeed)?.homePageURL, - let url = URL(string: homepage) else { - return - } - openURL(url) - } label: { - Text("Open Home Page") - #if os(iOS) - AppAssets.openInBrowserImage - #endif - } - Divider() - Button { - guard let feedUrl = (sidebarItem.feed as? WebFeed)?.url else { - return - } - #if os(macOS) - URLPasteboardWriter.write(urlString: feedUrl, to: NSPasteboard.general) - #else - UIPasteboard.general.string = feedUrl - #endif - - } label: { - Text("Copy Feed URL") - #if os(iOS) - AppAssets.copyImage - #endif - } - Button { - guard let homepage = (sidebarItem.feed as? WebFeed)?.homePageURL else { - return - } - #if os(macOS) - URLPasteboardWriter.write(urlString: homepage, to: NSPasteboard.general) - #else - UIPasteboard.general.string = homepage - #endif - } label: { - Text("Copy Home Page URL") - #if os(iOS) - AppAssets.copyImage - #endif - } - Divider() - Button { - if AppDefaults.shared.sidebarConfirmDelete == false { - sidebarModel.deleteFromAccount.send(sidebarItem.feed!) - } else { - sidebarModel.sidebarItemToDelete = sidebarItem.feed! - sidebarModel.showDeleteConfirmation = true - } - } label: { - Text("Delete") - #if os(iOS) - AppAssets.deleteImage - #endif - } - } - - // MARK: Folder Context Menu - if sidebarItem.representedType == .folder { - Button { - showInspector = true - } label: { - Text("Get Info") - #if os(iOS) - AppAssets.getInfoImage - #endif - } - Button { - guard let feed = sidebarItem.feed else { - return - } - sidebarModel.markAllAsReadInFeed.send(feed) - } label: { - Text("Mark All As Read") - #if os(iOS) - AppAssets.markAllAsReadImage - #endif - } - - /* - You cannot select folder level items in b4. Delete is disabled for the time being. - */ - /* - Divider() - Button { - if AppDefaults.shared.sidebarConfirmDelete == false { - sidebarModel.deleteFromAccount.send(sidebarItem.feed!) - } else { - sidebarModel.sidebarContextMenuItem = sidebarItem.feed - sidebarModel.showDeleteConfirmation = true - } - } label: { - Text("Delete") - #if os(iOS) - AppAssets.deleteImage - #endif - } - */ - } - - } -} diff --git a/Multiplatform/Shared/Sidebar/SidebarExpandedContainers.swift b/Multiplatform/Shared/Sidebar/SidebarExpandedContainers.swift deleted file mode 100644 index 878be0408..000000000 --- a/Multiplatform/Shared/Sidebar/SidebarExpandedContainers.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// SidebarExpandedContainers.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/30/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Combine -import Account - -struct SidebarExpandedContainers { - - var expandedTable = [ContainerIdentifier: Bool]() - - var data: Data { - get { - let encoder = PropertyListEncoder() - encoder.outputFormat = .binary - return (try? encoder.encode(expandedTable)) ?? Data() - } - set { - let decoder = PropertyListDecoder() - expandedTable = (try? decoder.decode([ContainerIdentifier: Bool].self, from: newValue)) ?? [ContainerIdentifier: Bool]() - } - } - - func contains(_ containerID: ContainerIdentifier) -> Bool { - return expandedTable.keys.contains(containerID) - } - - subscript(_ containerID: ContainerIdentifier) -> Bool { - get { - if let result = expandedTable[containerID] { - return result - } - switch containerID { - case .smartFeedController, .account: - return true - default: - return false - } - } - set(newValue) { - expandedTable[containerID] = newValue - } - } - -} diff --git a/Multiplatform/Shared/Sidebar/SidebarItem.swift b/Multiplatform/Shared/Sidebar/SidebarItem.swift deleted file mode 100644 index b4cda8772..000000000 --- a/Multiplatform/Shared/Sidebar/SidebarItem.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// SidebarItem.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/29/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import RSCore -import Account - -public enum SidebarItemIdentifier: Hashable, Equatable { - case smartFeedController - case account(String) - case feed(FeedIdentifier) -} - -public enum RepresentedType { - case smartFeedController, webFeed, folder, pseudoFeed, account, unknown -} - -struct SidebarItem: Identifiable { - - var id: SidebarItemIdentifier - var represented: Any - var children: [SidebarItem] = [SidebarItem]() - - var unreadCount: Int - var nameForDisplay: String - - var feed: Feed? { - represented as? Feed - } - - var containerID: ContainerIdentifier? { - return (represented as? ContainerIdentifiable)?.containerID - } - - var representedType: RepresentedType { - switch type(of: represented) { - case is SmartFeedsController.Type: - return .smartFeedController - case is SmartFeed.Type: - return .pseudoFeed - case is UnreadFeed.Type: - return .pseudoFeed - case is WebFeed.Type: - return .webFeed - case is Folder.Type: - return .folder - case is Account.Type: - return .account - default: - return .unknown - } - } - - init(_ smartFeedsController: SmartFeedsController) { - self.id = .smartFeedController - self.represented = smartFeedsController - self.unreadCount = 0 - self.nameForDisplay = smartFeedsController.nameForDisplay - } - - init(_ account: Account) { - self.id = .account(account.accountID) - self.represented = account - self.unreadCount = account.unreadCount - self.nameForDisplay = account.nameForDisplay - } - - init(_ feed: Feed, unreadCount: Int) { - self.id = .feed(feed.feedID!) - self.represented = feed - self.unreadCount = unreadCount - self.nameForDisplay = feed.nameForDisplay - } - - /// Add a sidebar item to the child list - mutating func addChild(_ sidebarItem: SidebarItem) { - children.append(sidebarItem) - } - - /// Recursively visits each sidebar item. Return true when done visiting. - @discardableResult - func visit(_ block: (SidebarItem) -> Bool) -> Bool { - let stop = block(self) - if !stop { - for child in children { - if child.visit(block) { - break - } - } - } - return stop - } -} diff --git a/Multiplatform/Shared/Sidebar/SidebarItemView.swift b/Multiplatform/Shared/Sidebar/SidebarItemView.swift deleted file mode 100644 index b14e18a62..000000000 --- a/Multiplatform/Shared/Sidebar/SidebarItemView.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// SidebarItemView.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/29/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SidebarItemView: View { - - @StateObject var feedIconImageLoader = FeedIconImageLoader() - @EnvironmentObject private var sidebarModel: SidebarModel - @State private var showInspector: Bool = false - var sidebarItem: SidebarItem - - var body: some View { - HStack { - #if os(macOS) - HStack { - if let image = feedIconImageLoader.image { - IconImageView(iconImage: image) - .frame(width: 20, height: 20, alignment: .center) - } - Text(verbatim: sidebarItem.nameForDisplay) - Spacer() - if sidebarItem.unreadCount > 0 { - UnreadCountView(count: sidebarItem.unreadCount) - } - } - #else - HStack(alignment: .top) { - if let image = feedIconImageLoader.image { - IconImageView(iconImage: image) - .frame(width: 20, height: 20) - } - Text(verbatim: sidebarItem.nameForDisplay) - } - Spacer() - if sidebarItem.unreadCount > 0 { - UnreadCountView(count: sidebarItem.unreadCount) - } - if sidebarItem.representedType == .webFeed || sidebarItem.representedType == .pseudoFeed { - Spacer() - .frame(width: 16) - } - #endif - } - .onAppear { - if let feed = sidebarItem.feed { - feedIconImageLoader.loadImage(for: feed) - } - }.contextMenu { - SidebarContextMenu(showInspector: $showInspector, sidebarItem: sidebarItem) - .environmentObject(sidebarModel) - } - .sheet(isPresented: $showInspector, onDismiss: { showInspector = false}) { - InspectorView(sidebarItem: sidebarItem) - } - } - -} diff --git a/Multiplatform/Shared/Sidebar/SidebarListStyleModifier.swift b/Multiplatform/Shared/Sidebar/SidebarListStyleModifier.swift deleted file mode 100644 index d564457e3..000000000 --- a/Multiplatform/Shared/Sidebar/SidebarListStyleModifier.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// SidebarListStyleModifier.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct SidebarListStyleModifier: ViewModifier { - - #if os(iOS) - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - #endif - - @ViewBuilder func body(content: Content) -> some View { - #if os(macOS) - content - .listStyle(SidebarListStyle()) - #else - if horizontalSizeClass == .compact { - content - .listStyle(PlainListStyle()) - } else { - content - .listStyle(SidebarListStyle()) - } - #endif - - } - -} diff --git a/Multiplatform/Shared/Sidebar/SidebarModel.swift b/Multiplatform/Shared/Sidebar/SidebarModel.swift deleted file mode 100644 index da8e36de7..000000000 --- a/Multiplatform/Shared/Sidebar/SidebarModel.swift +++ /dev/null @@ -1,357 +0,0 @@ -// -// SidebarModel.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/28/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import Combine -import RSCore -import Account -import Articles - -protocol SidebarModelDelegate: AnyObject { - func unreadCount(for: Feed) -> Int -} - -class SidebarModel: ObservableObject, UndoableCommandRunner { - - @Published var selectedFeedIdentifiers = Set() - @Published var selectedFeedIdentifier: FeedIdentifier? = .none - @Published var isReadFiltered = false - @Published var expandedContainers = SidebarExpandedContainers() - @Published var showDeleteConfirmation: Bool = false - - weak var delegate: SidebarModelDelegate? - - var sidebarItemsPublisher: AnyPublisher<[SidebarItem], Never>? - var selectedFeedsPublisher: AnyPublisher<[Feed], Never>? - - var selectNextUnread = PassthroughSubject() - var markAllAsReadInFeed = PassthroughSubject() - var markAllAsReadInAccount = PassthroughSubject() - var deleteFromAccount = PassthroughSubject() - - var sidebarItemToDelete: Feed? - - private var cancellables = Set() - - var undoManager: UndoManager? - var undoableCommands = [UndoableCommand]() - - init(delegate: SidebarModelDelegate) { - self.delegate = delegate - subscribeToSelectedFeedChanges() - subscribeToRebuildSidebarItemsEvents() - subscribeToNextUnread() - subscribeToMarkAllAsReadInFeed() - subscribeToMarkAllAsReadInAccount() - subscribeToDeleteFromAccount() - } - -} - - -extension SidebarModel { - - func countOfFeedsToDelete() -> Int { - var selectedFeeds = selectedFeedIdentifiers - - if sidebarItemToDelete != nil { - selectedFeeds.insert(sidebarItemToDelete!.feedID!) - } - - return selectedFeeds.count - } - - - func namesOfFeedsToDelete() -> String { - var selectedFeeds = selectedFeedIdentifiers - - if sidebarItemToDelete != nil { - selectedFeeds.insert(sidebarItemToDelete!.feedID!) - } - - let feeds: [Feed] = selectedFeeds - .compactMap({ AccountManager.shared.existingFeed(with: $0) }) - - return feeds - .map({ $0.nameForDisplay }) - .joined(separator: ", ") - } - -} - -// MARK: Private - -private extension SidebarModel { - - // MARK: Subscriptions - - func subscribeToSelectedFeedChanges() { - - let selectedFeedIdentifersPublisher = $selectedFeedIdentifiers - .map { [weak self] feedIDs -> [Feed] in - return feedIDs.compactMap { self?.findFeed($0) } - } - - - let selectedFeedIdentiferPublisher = $selectedFeedIdentifier - .compactMap { [weak self] feedID -> [Feed]? in - if let feedID = feedID, let feed = self?.findFeed(feedID) { - return [feed] - } else { - return nil - } - } - - selectedFeedsPublisher = selectedFeedIdentifersPublisher - .merge(with: selectedFeedIdentiferPublisher) - .removeDuplicates(by: { previousFeeds, currentFeeds in - return previousFeeds.elementsEqual(currentFeeds, by: { $0.feedID == $1.feedID }) - }) - .share() - .eraseToAnyPublisher() - } - - func subscribeToRebuildSidebarItemsEvents() { - guard let selectedFeedsPublisher = selectedFeedsPublisher else { return } - - let chidrenDidChangePublisher = NotificationCenter.default.publisher(for: .ChildrenDidChange) - let batchUpdateDidPerformPublisher = NotificationCenter.default.publisher(for: .BatchUpdateDidPerform) - let displayNameDidChangePublisher = NotificationCenter.default.publisher(for: .DisplayNameDidChange) - let accountStateDidChangePublisher = NotificationCenter.default.publisher(for: .AccountStateDidChange) - let userDidAddAccountPublisher = NotificationCenter.default.publisher(for: .UserDidAddAccount) - let userDidDeleteAccountPublisher = NotificationCenter.default.publisher(for: .UserDidDeleteAccount) - let unreadCountDidInitializePublisher = NotificationCenter.default.publisher(for: .UnreadCountDidInitialize) - let unreadCountDidChangePublisher = NotificationCenter.default.publisher(for: .UnreadCountDidChange) - - let sidebarRebuildPublishers = chidrenDidChangePublisher.merge(with: batchUpdateDidPerformPublisher, - displayNameDidChangePublisher, - accountStateDidChangePublisher, - userDidAddAccountPublisher, - userDidDeleteAccountPublisher, - unreadCountDidInitializePublisher, - unreadCountDidChangePublisher) - - let kickStarter = Notification(name: Notification.Name(rawValue: "Kick Starter")) - - sidebarItemsPublisher = sidebarRebuildPublishers - .prepend(kickStarter) - .debounce(for: .milliseconds(500), scheduler: RunLoop.main) - .combineLatest($isReadFiltered, selectedFeedsPublisher) - .compactMap { [weak self] _, readFilter, selectedFeeds in - self?.rebuildSidebarItems(isReadFiltered: readFilter, selectedFeeds: selectedFeeds) - } - .share() - .eraseToAnyPublisher() - } - - func subscribeToNextUnread() { - guard let sidebarItemsPublisher = sidebarItemsPublisher, let selectedFeedsPublisher = selectedFeedsPublisher else { return } - - selectNextUnread - .withLatestFrom(sidebarItemsPublisher, selectedFeedsPublisher) - .compactMap { [weak self] (sidebarItems, selectedFeeds) in - return self?.nextUnread(sidebarItems: sidebarItems, selectedFeeds: selectedFeeds) - } - .sink { [weak self] nextFeedID in - self?.select(nextFeedID) - } - .store(in: &cancellables) - } - - func subscribeToMarkAllAsReadInFeed() { - guard let selectedFeedsPublisher = selectedFeedsPublisher else { return } - - markAllAsReadInFeed - .withLatestFrom(selectedFeedsPublisher, resultSelector: { givenFeed, selectedFeeds -> [Feed] in - if selectedFeeds.contains(where: { $0.feedID == givenFeed.feedID }) { - return selectedFeeds - } else { - return [givenFeed] - } - }) - .map { feeds in - var articles = [Article]() - for feed in feeds { - articles.append(contentsOf: (try? feed.fetchUnreadArticles()) ?? Set
()) - } - return articles - } - .sink { [weak self] allArticles in - self?.markAllAsRead(allArticles) - } - .store(in: &cancellables) - } - - func subscribeToMarkAllAsReadInAccount() { - markAllAsReadInAccount - .map { account in - var articles = [Article]() - for feed in account.flattenedWebFeeds() { - articles.append(contentsOf: (try? feed.fetchUnreadArticles()) ?? Set
()) - } - return articles - } - .sink { [weak self] articles in - self?.markAllAsRead(articles) - } - .store(in: &cancellables) - } - - func subscribeToDeleteFromAccount() { - guard let selectedFeedsPublisher = selectedFeedsPublisher else { return } - - deleteFromAccount - .withLatestFrom(selectedFeedsPublisher.prepend([Feed]()), resultSelector: { givenFeed, selectedFeeds -> [Feed] in - if selectedFeeds.contains(where: { $0.feedID == givenFeed.feedID }) { - return selectedFeeds - } else { - return [givenFeed] - } - }) - .sink { feeds in - for feed in feeds { - if let webFeed = feed as? WebFeed { - guard let account = webFeed.account, - let containerID = account.containerID, - let container = AccountManager.shared.existingContainer(with: containerID) else { - return - } - account.removeWebFeed(webFeed, from: container, completion: { result in - switch result { - case .success: - break - case .failure(let err): - print(err) - } - }) - } - if let folder = feed as? Folder { - folder.account?.removeFolder(folder) { _ in } - } - } - } - .store(in: &cancellables) - } - - /// Marks provided artices as read. - /// - Parameter articles: An array of `Article`s. - /// - Warning: An `UndoManager` is created here as the `Environment`'s undo manager appears to be `nil`. - func markAllAsRead(_ articles: [Article]) { - guard let undoManager = undoManager, - let markAsReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else { - return - } - runCommand(markAsReadCommand) - } - - // MARK: Sidebar Building - - func sort(_ folders: Set) -> [Folder] { - return folders.sorted(by: { $0.nameForDisplay.localizedStandardCompare($1.nameForDisplay) == .orderedAscending }) - } - - func sort(_ feeds: Set) -> [Feed] { - return feeds.sorted(by: { $0.nameForDisplay.localizedStandardCompare($1.nameForDisplay) == .orderedAscending }) - } - - func rebuildSidebarItems(isReadFiltered: Bool, selectedFeeds: [Feed]) -> [SidebarItem] { - var items = [SidebarItem]() - guard let delegate = delegate else { return items } - - var smartFeedControllerItem = SidebarItem(SmartFeedsController.shared) - for feed in SmartFeedsController.shared.smartFeeds { -// It looks like SwiftUI loses its mind when the last element in a section is removed. Don't filter -// the smartfeeds yet or we crash about everytime because Starred is almost always filtered -// if !isReadFiltered || feed.unreadCount > 0 { - smartFeedControllerItem.addChild(SidebarItem(feed, unreadCount: delegate.unreadCount(for: feed))) -// } - } - items.append(smartFeedControllerItem) - - let selectedFeedIDs = Set(selectedFeeds.map { $0.feedID }) - - for account in AccountManager.shared.sortedActiveAccounts { - var accountItem = SidebarItem(account) - - for webFeed in sort(account.topLevelWebFeeds) { - if !isReadFiltered || !(webFeed.unreadCount < 1 && !selectedFeedIDs.contains(webFeed.feedID)) { - accountItem.addChild(SidebarItem(webFeed, unreadCount: delegate.unreadCount(for: webFeed))) - } - } - - for folder in sort(account.folders ?? Set()) { - if !isReadFiltered || !(folder.unreadCount < 1 && !selectedFeedIDs.contains(folder.feedID)) { - var folderItem = SidebarItem(folder, unreadCount: delegate.unreadCount(for: folder)) - for webFeed in sort(folder.topLevelWebFeeds) { - if !isReadFiltered || !(webFeed.unreadCount < 1 && !selectedFeedIDs.contains(webFeed.feedID)) { - folderItem.addChild(SidebarItem(webFeed, unreadCount: delegate.unreadCount(for: webFeed))) - } - } - accountItem.addChild(folderItem) - } - } - - items.append(accountItem) - } - - return items - } - - // MARK: - - func findFeed(_ feedID: FeedIdentifier) -> Feed? { - switch feedID { - case .smartFeed: - return SmartFeedsController.shared.find(by: feedID) - default: - return AccountManager.shared.existingFeed(with: feedID) - } - } - - func nextUnread(sidebarItems: [SidebarItem], selectedFeeds: [Feed]) -> FeedIdentifier? { - guard let startFeed = selectedFeeds.first ?? sidebarItems.first?.children.first?.feed else { return nil } - - if let feedID = nextUnread(sidebarItems: sidebarItems, startingAt: startFeed) { - return feedID - } else { - return nextUnread(sidebarItems: sidebarItems, startingAt: nil) - } - } - - @discardableResult - func nextUnread(sidebarItems: [SidebarItem], startingAt: Feed?) -> FeedIdentifier? { - var foundStartFeed = startingAt == nil ? true : false - var nextSidebarItem: SidebarItem? = nil - - for section in sidebarItems { - if nextSidebarItem == nil { - section.visit { sidebarItem in - if !foundStartFeed && sidebarItem.feed?.feedID == startingAt?.feedID { - foundStartFeed = true - return false - } - if foundStartFeed && sidebarItem.unreadCount > 0 { - nextSidebarItem = sidebarItem - return true - } - return false - } - } - } - - return nextSidebarItem?.feed?.feedID - } - - func select(_ feedID: FeedIdentifier) { - selectedFeedIdentifiers = Set([feedID]) - selectedFeedIdentifier = feedID - } - - - -} diff --git a/Multiplatform/Shared/Sidebar/SidebarToolbarModel.swift b/Multiplatform/Shared/Sidebar/SidebarToolbarModel.swift deleted file mode 100644 index d5b3ad00d..000000000 --- a/Multiplatform/Shared/Sidebar/SidebarToolbarModel.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// SidebarToolbarModel.swift -// NetNewsWire -// -// Created by Stuart Breckenridge on 4/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation - -enum SidebarSheets { - case none, web, twitter, reddit, folder, settings, fixCredentials -} - -class SidebarToolbarModel: ObservableObject { - - @Published var showSheet: Bool = false - @Published var sheetToShow: SidebarSheets = .none { - didSet { - sheetToShow != .none ? (showSheet = true) : (showSheet = false) - } - } - @Published var showAddSheet: Bool = false - -} diff --git a/Multiplatform/Shared/Sidebar/SidebarToolbarModifier.swift b/Multiplatform/Shared/Sidebar/SidebarToolbarModifier.swift deleted file mode 100644 index b0096513e..000000000 --- a/Multiplatform/Shared/Sidebar/SidebarToolbarModifier.swift +++ /dev/null @@ -1,105 +0,0 @@ -// -// SidebarToolbarModifier.swift -// Multiplatform iOS -// -// Created by Stuart Breckenridge on 30/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct SidebarToolbarModifier: ViewModifier { - - @EnvironmentObject private var refreshProgress: RefreshProgressModel - @EnvironmentObject private var defaults: AppDefaults - @EnvironmentObject private var sidebarModel: SidebarModel - @StateObject private var viewModel = SidebarToolbarModel() - - @ViewBuilder func body(content: Content) -> some View { - #if os(iOS) - content - .toolbar { - - ToolbarItem(placement: .primaryAction) { - Button { - withAnimation { - sidebarModel.isReadFiltered.toggle() - } - } label: { - if sidebarModel.isReadFiltered { - AppAssets.filterActiveImage.font(.title3) - } else { - AppAssets.filterInactiveImage.font(.title3) - } - } - .help(sidebarModel.isReadFiltered ? "Show Read Feeds" : "Filter Read Feeds") - } - - ToolbarItem(placement: .bottomBar) { - Button { - viewModel.sheetToShow = .settings - } label: { - AppAssets.settingsImage.font(.title3) - } - .help("Settings") - } - - ToolbarItem(placement: .bottomBar) { - Spacer() - } - - ToolbarItem(placement: .bottomBar) { - switch refreshProgress.state { - case .refreshProgress(let progress): - ProgressView(value: progress) - .frame(width: 100) - case .lastRefreshDateText(let text): - Text(text) - .lineLimit(1) - .font(.caption) - .foregroundColor(.secondary) - case .none: - EmptyView() - } - } - - ToolbarItem(placement: .bottomBar) { - Spacer() - } - - ToolbarItem(placement: .bottomBar, content: { - Menu(content: { - Button { viewModel.sheetToShow = .web } label: { Text("Add Web Feed") } - Button { viewModel.sheetToShow = .twitter } label: { Text("Add Twitter Feed") } - Button { viewModel.sheetToShow = .reddit } label: { Text("Add Reddit Feed") } - Button { viewModel.sheetToShow = .folder } label: { Text("Add Folder") } - }, label: { - AppAssets.addMenuImage.font(.title3) - }) - }) - - } - .sheet(isPresented: $viewModel.showSheet, onDismiss: { viewModel.sheetToShow = .none }) { - if viewModel.sheetToShow == .web { - AddWebFeedView(isPresented: $viewModel.showSheet) - } - if viewModel.sheetToShow == .folder { - AddFolderView(isPresented: $viewModel.showSheet) - } - if viewModel.sheetToShow == .settings { - SettingsView() - .preferredColorScheme(AppDefaults.userInterfaceColorScheme) - } - } - #else - content - .toolbar { - ToolbarItem { - Spacer() - } - } - #endif - } -} - - diff --git a/Multiplatform/Shared/Sidebar/SidebarView.swift b/Multiplatform/Shared/Sidebar/SidebarView.swift deleted file mode 100644 index 97cfe801f..000000000 --- a/Multiplatform/Shared/Sidebar/SidebarView.swift +++ /dev/null @@ -1,237 +0,0 @@ -// -// SidebarView.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/29/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SidebarView: View { - - @Binding var sidebarItems: [SidebarItem] - - @EnvironmentObject private var refreshProgress: RefreshProgressModel - @EnvironmentObject private var sceneModel: SceneModel - @EnvironmentObject private var sidebarModel: SidebarModel - - // I had to comment out SceneStorage because it blows up if used on macOS - // @SceneStorage("expandedContainers") private var expandedContainerData = Data() - - private let threshold: CGFloat = 80 - @State private var previousScrollOffset: CGFloat = 0 - @State private var scrollOffset: CGFloat = 0 - @State var pulling: Bool = false - @State var refreshing: Bool = false - - var body: some View { - #if os(macOS) - VStack { - HStack { - Spacer() - Button (action: { - withAnimation { - sidebarModel.isReadFiltered.toggle() - } - }, label: { - if sidebarModel.isReadFiltered { - AppAssets.filterActiveImage - } else { - AppAssets.filterInactiveImage - } - }) - .padding(.top, 8).padding(.trailing) - .buttonStyle(PlainButtonStyle()) - .help(sidebarModel.isReadFiltered ? "Show Read Feeds" : "Filter Read Feeds") - } - List(selection: $sidebarModel.selectedFeedIdentifiers) { - rows - } - if case .refreshProgress(let percent) = refreshProgress.state { - HStack(alignment: .center) { - Spacer() - ProgressView(value: percent).frame(width: 100) - Spacer() - } - .padding(8) - .background(Color(NSColor.windowBackgroundColor)) - .frame(height: 30) - .animation(.easeInOut(duration: 0.5)) - .transition(.move(edge: .bottom)) - } - } - .alert(isPresented: $sidebarModel.showDeleteConfirmation, content: { - Alert(title: sidebarModel.countOfFeedsToDelete() > 1 ? - (Text("Delete multiple items?")) : - (Text("Delete \(sidebarModel.namesOfFeedsToDelete())?")), - message: Text("Are you sure you wish to delete \(sidebarModel.namesOfFeedsToDelete())?"), - primaryButton: .destructive(Text("Delete"), - action: { - sidebarModel.deleteFromAccount.send(sidebarModel.sidebarItemToDelete!) - sidebarModel.sidebarItemToDelete = nil - sidebarModel.selectedFeedIdentifiers.removeAll() - sidebarModel.showDeleteConfirmation = false - }), - secondaryButton: .cancel(Text("Cancel"), action: { - sidebarModel.sidebarItemToDelete = nil - sidebarModel.showDeleteConfirmation = false - })) - }) - #else - ZStack(alignment: .top) { - List { - rows - } - .background(RefreshFixedView()) - .navigationTitle(Text("Feeds")) - .onPreferenceChange(RefreshKeyTypes.PrefKey.self) { values in - refreshLogic(values: values) - } - if pulling { - ProgressView().offset(y: -40) - } - } - .alert(isPresented: $sidebarModel.showDeleteConfirmation, content: { - Alert(title: sidebarModel.countOfFeedsToDelete() > 1 ? - (Text("Delete multiple items?")) : - (Text("Delete \(sidebarModel.namesOfFeedsToDelete())?")), - message: Text("Are you sure you wish to delete \(sidebarModel.namesOfFeedsToDelete())?"), - primaryButton: .destructive(Text("Delete"), - action: { - sidebarModel.deleteFromAccount.send(sidebarModel.sidebarItemToDelete!) - sidebarModel.sidebarItemToDelete = nil - sidebarModel.selectedFeedIdentifiers.removeAll() - sidebarModel.showDeleteConfirmation = false - }), - secondaryButton: .cancel(Text("Cancel"), action: { - sidebarModel.sidebarItemToDelete = nil - sidebarModel.showDeleteConfirmation = false - })) - }) - #endif - -// .onAppear { -// expandedContainers.data = expandedContainerData -// } -// .onReceive(expandedContainers.objectDidChange) { -// expandedContainerData = expandedContainers.data -// } - } - - func refreshLogic(values: [RefreshKeyTypes.PrefData]) { - DispatchQueue.main.async { - let movingBounds = values.first { $0.vType == .movingView }?.bounds ?? .zero - let fixedBounds = values.first { $0.vType == .fixedView }?.bounds ?? .zero - scrollOffset = movingBounds.minY - fixedBounds.minY - - // Crossing the threshold on the way down, we start the refresh process - if !pulling && (scrollOffset > threshold && previousScrollOffset <= threshold) { - pulling = true - AccountManager.shared.refreshAll() - } - - // Crossing the threshold on the way UP, we end the refresh - if pulling && previousScrollOffset > threshold && scrollOffset <= threshold { - pulling = false - } - - // Update last scroll offset - self.previousScrollOffset = self.scrollOffset - } - } - - struct RefreshFixedView: View { - var body: some View { - GeometryReader { proxy in - Color.clear.preference(key: RefreshKeyTypes.PrefKey.self, value: [RefreshKeyTypes.PrefData(vType: .fixedView, bounds: proxy.frame(in: .global))]) - } - } - } - - struct RefreshKeyTypes { - enum ViewType: Int { - case movingView - case fixedView - } - - struct PrefData: Equatable { - let vType: ViewType - let bounds: CGRect - } - - struct PrefKey: PreferenceKey { - static var defaultValue: [PrefData] = [] - - static func reduce(value: inout [PrefData], nextValue: () -> [PrefData]) { - value.append(contentsOf: nextValue()) - } - - typealias Value = [PrefData] - } - } - - var rows: some View { - ForEach(sidebarItems) { sidebarItem in - if let containerID = sidebarItem.containerID { - DisclosureGroup(isExpanded: $sidebarModel.expandedContainers[containerID]) { - ForEach(sidebarItem.children) { sidebarItem in - if let containerID = sidebarItem.containerID { - DisclosureGroup(isExpanded: $sidebarModel.expandedContainers[containerID]) { - ForEach(sidebarItem.children) { sidebarItem in - SidebarItemNavigation(sidebarItem: sidebarItem) - } - } label: { - SidebarItemNavigation(sidebarItem: sidebarItem) - } - } else { - SidebarItemNavigation(sidebarItem: sidebarItem) - } - } - } label: { - #if os(macOS) - SidebarItemView(sidebarItem: sidebarItem) - .padding(.leading, 4) - .environmentObject(sidebarModel) - #else - if sidebarItem.representedType == .smartFeedController { - GeometryReader { proxy in - SidebarItemView(sidebarItem: sidebarItem) - .preference(key: RefreshKeyTypes.PrefKey.self, value: [RefreshKeyTypes.PrefData(vType: .movingView, bounds: proxy.frame(in: .global))]) - .environmentObject(sidebarModel) - } - } else { - SidebarItemView(sidebarItem: sidebarItem) - .environmentObject(sidebarModel) - } - #endif - } - } - } - } - - struct SidebarItemNavigation: View { - - @EnvironmentObject private var sidebarModel: SidebarModel - var sidebarItem: SidebarItem - - var body: some View { - #if os(macOS) - SidebarItemView(sidebarItem: sidebarItem) - .tag(sidebarItem.feed!.feedID!) - #else - ZStack { - SidebarItemView(sidebarItem: sidebarItem) - NavigationLink(destination: TimelineContainerView(), - tag: sidebarItem.feed!.feedID!, - selection: $sidebarModel.selectedFeedIdentifier) { - EmptyView() - }.buttonStyle(PlainButtonStyle()) - } - #endif - } - - } - -} diff --git a/Multiplatform/Shared/Sidebar/UnreadCountView.swift b/Multiplatform/Shared/Sidebar/UnreadCountView.swift deleted file mode 100644 index 72cf9d5ec..000000000 --- a/Multiplatform/Shared/Sidebar/UnreadCountView.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// UnreadCountView.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/29/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct UnreadCountView: View { - - var count: Int - - var body: some View { - Text(verbatim: String(count)) - .font(.caption) - .fontWeight(.bold) - .padding(.horizontal, 7) - .padding(.vertical, 1) - .background(AppAssets.sidebarUnreadCountBackground) - .foregroundColor(AppAssets.sidebarUnreadCountForeground) - .cornerRadius(8) - } -} - -struct UnreadCountView_Previews: PreviewProvider { - static var previews: some View { - UnreadCountView(count: 123) - } -} diff --git a/Multiplatform/Shared/SwiftUI Extensions/HiddenModifier.swift b/Multiplatform/Shared/SwiftUI Extensions/HiddenModifier.swift deleted file mode 100644 index c59d23f59..000000000 --- a/Multiplatform/Shared/SwiftUI Extensions/HiddenModifier.swift +++ /dev/null @@ -1,21 +0,0 @@ -// -// HiddenModifier.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/12/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -extension View { - func hidden(_ hide: Bool) -> some View { - Group { - if hide { - self.hidden() - } else { - self - } - } - } -} diff --git a/Multiplatform/Shared/SwiftUI Extensions/Image-Extensions.swift b/Multiplatform/Shared/SwiftUI Extensions/Image-Extensions.swift deleted file mode 100644 index 517991676..000000000 --- a/Multiplatform/Shared/SwiftUI Extensions/Image-Extensions.swift +++ /dev/null @@ -1,23 +0,0 @@ -// -// Image-Extensions.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/1/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import RSCore - -extension Image { - - init(rsImage: RSImage) { - #if os(macOS) - self = Image(nsImage: rsImage) - #endif - #if os(iOS) - self = Image(uiImage: rsImage) - #endif - } - -} diff --git a/Multiplatform/Shared/SwiftUI Extensions/PreferredColorSchemeModifier.swift b/Multiplatform/Shared/SwiftUI Extensions/PreferredColorSchemeModifier.swift deleted file mode 100644 index ba9a04d36..000000000 --- a/Multiplatform/Shared/SwiftUI Extensions/PreferredColorSchemeModifier.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// PreferredColorSchemeModifier.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/3/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct PreferredColorSchemeModifier: ViewModifier { - - var preferredColorScheme: UserInterfaceColorPalette - - @ViewBuilder - func body(content: Content) -> some View { - switch preferredColorScheme { - case .automatic: - content.preferredColorScheme(nil) - case .dark: - content.preferredColorScheme(.dark) - case .light: - content.preferredColorScheme(.light) - } - } - -} diff --git a/Multiplatform/Shared/Timeline/TimelineContainerView.swift b/Multiplatform/Shared/Timeline/TimelineContainerView.swift deleted file mode 100644 index 70f8de1b8..000000000 --- a/Multiplatform/Shared/Timeline/TimelineContainerView.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// TimelineContainerView.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/30/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct TimelineContainerView: View { - - @Environment(\.undoManager) var undoManager - @EnvironmentObject private var sceneModel: SceneModel - - @State private var timelineItems = TimelineItems() - @State private var isReadFiltered: Bool? = nil - - var body: some View { - TimelineView(timelineItems: $timelineItems, isReadFiltered: $isReadFiltered) - .modifier(TimelineToolbarModifier()) - .environmentObject(sceneModel.timelineModel) - .onAppear { - sceneModel.timelineModel.undoManager = undoManager - } - .onReceive(sceneModel.timelineModel.readFilterAndFeedsPublisher!) { (_, filtered) in - isReadFiltered = filtered - } - .onReceive(sceneModel.timelineModel.timelineItemsSelectPublisher!) { (items, selectTimelineItemID) in - timelineItems = items - if let selectID = selectTimelineItemID { - #if os(macOS) - sceneModel.timelineModel.selectedTimelineItemIDs = Set([selectID]) - #else - sceneModel.timelineModel.selectedTimelineItemID = selectID - #endif - } - } - .onReceive(sceneModel.timelineModel.articleStatusChangePublisher!) { articleIDs in - articleIDs.forEach { articleID in - if let position = timelineItems.index[articleID] { - timelineItems.items[position].updateStatus() - } - } - } - } - -} diff --git a/Multiplatform/Shared/Timeline/TimelineContextMenu.swift b/Multiplatform/Shared/Timeline/TimelineContextMenu.swift deleted file mode 100644 index 96e49c9e0..000000000 --- a/Multiplatform/Shared/Timeline/TimelineContextMenu.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// TimelineContextMenu.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/17/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct TimelineContextMenu: View { - - @EnvironmentObject private var timelineModel: TimelineModel - var timelineItem: TimelineItem - - var body: some View { - - if timelineModel.canMarkIndicatedArticlesAsRead(timelineItem) { - Button { - timelineModel.markIndicatedArticlesAsRead(timelineItem) - } label: { - Text("Mark as Read") - #if os(iOS) - AppAssets.readOpenImage - #endif - } - } - - if timelineModel.canMarkIndicatedArticlesAsUnread(timelineItem) { - Button { - timelineModel.markIndicatedArticlesAsUnread(timelineItem) - } label: { - Text("Mark as Unread") - #if os(iOS) - AppAssets.readClosedImage - #endif - } - } - - if timelineModel.canMarkIndicatedArticlesAsStarred(timelineItem) { - Button { - timelineModel.markIndicatedArticlesAsStarred(timelineItem) - } label: { - Text("Mark as Starred") - #if os(iOS) - AppAssets.starClosedImage - #endif - } - } - - if timelineModel.canMarkIndicatedArticlesAsUnstarred(timelineItem) { - Button { - timelineModel.markIndicatedArticlesAsUnstarred(timelineItem) - } label: { - Text("Mark as Unstarred") - #if os(iOS) - AppAssets.starOpenImage - #endif - } - } - - if timelineModel.canMarkAboveAsRead(timelineItem) { - Button { - timelineModel.markAboveAsRead(timelineItem) - } label: { - Text("Mark Above as Read") - #if os(iOS) - AppAssets.markAboveAsReadImage - #endif - } - } - - if timelineModel.canMarkBelowAsRead(timelineItem) { - Button { - timelineModel.markBelowAsRead(timelineItem) - } label: { - Text("Mark Below As Read") - #if os(iOS) - AppAssets.markBelowAsReadImage - #endif - } - } - - if timelineModel.canMarkAllAsReadInWebFeed(timelineItem) { - Divider() - Button { - timelineModel.markAllAsReadInWebFeed(timelineItem) - } label: { - Text("Mark All as Read in “\(timelineItem.article.webFeed?.nameForDisplay ?? "")”") - #if os(iOS) - AppAssets.markAllAsReadImage - #endif - } - } - - if timelineModel.canOpenIndicatedArticleInBrowser(timelineItem) { - Divider() - Button { - timelineModel.openIndicatedArticleInBrowser(timelineItem) - } label: { - Text("Open in Browser") - #if os(iOS) - AppAssets.openInBrowserImage - #endif - } - } - - } -} diff --git a/Multiplatform/Shared/Timeline/TimelineItem.swift b/Multiplatform/Shared/Timeline/TimelineItem.swift deleted file mode 100644 index 77f5eedf6..000000000 --- a/Multiplatform/Shared/Timeline/TimelineItem.swift +++ /dev/null @@ -1,98 +0,0 @@ -// -// TimelineItem.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/30/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Articles - -enum TimelineItemStatus { - case showStar - case showUnread - case showNone -} - -struct TimelineItem: Identifiable { - - var id: String - var position: Int - var article: Article - - var status: TimelineItemStatus = .showNone - var truncatedTitle: String - var truncatedSummary: String - var byline: String - var dateTimeString: String - - init(position: Int, article: Article) { - self.id = article.articleID - self.position = position - self.article = article - self.byline = article.webFeed?.nameForDisplay ?? "" - self.dateTimeString = ArticleStringFormatter.dateString(article.logicalDatePublished) - self.truncatedTitle = ArticleStringFormatter.truncatedTitle(article) - self.truncatedSummary = ArticleStringFormatter.truncatedSummary(article) - updateStatus() - } - - var isReadOnly: Bool { - return article.status.read == true && article.status.starred == false - } - - mutating func updateStatus() { - if article.status.starred == true { - status = .showStar - } else { - if article.status.read == false { - status = .showUnread - } else { - status = .showNone - } - } - } - - func numberOfTitleLines(width: CGFloat) -> Int { - guard !truncatedTitle.isEmpty else { return 0 } - - #if os(macOS) - let descriptor = NSFont.preferredFont(forTextStyle: .body).fontDescriptor.withSymbolicTraits(.bold) - guard let font = NSFont(descriptor: descriptor, size: 0) else { return 0 } - #else - guard let descriptor = UIFont.preferredFont(forTextStyle: .body).fontDescriptor.withSymbolicTraits(.traitBold) else { return 0 } - let font = UIFont(descriptor: descriptor, size: 0) - #endif - - let lines = Int(AppDefaults.shared.timelineNumberOfLines) - let sizeInfo = TimelineTextSizer.size(for: truncatedTitle, font: font, numberOfLines: lines, width: adjustedWidth(width)) - return sizeInfo.numberOfLinesUsed - } - - func numberOfSummaryLines(width: CGFloat, titleLines: Int) -> Int { - guard !truncatedSummary.isEmpty else { return 0 } - - let remainingLines = Int(AppDefaults.shared.timelineNumberOfLines) - titleLines - guard remainingLines > 0 else { return 0 } - - #if os(macOS) - let font = NSFont.preferredFont(forTextStyle: .body) - #else - let font = UIFont.preferredFont(forTextStyle: .body) - #endif - - let sizeInfo = TimelineTextSizer.size(for: truncatedSummary, font: font, numberOfLines: remainingLines, width: adjustedWidth(width)) - return sizeInfo.numberOfLinesUsed - } - -} - -private extension TimelineItem { - - // This clearly isn't correct yet, but it gets us close enough for now. -Maurice - func adjustedWidth(_ width: CGFloat) -> Int { - return Int(width - CGFloat(AppDefaults.shared.timelineIconDimensions + 64)) - } - -} diff --git a/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift b/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift deleted file mode 100644 index 540c35f04..000000000 --- a/Multiplatform/Shared/Timeline/TimelineItemStatusView.swift +++ /dev/null @@ -1,52 +0,0 @@ -// -// TimelineItemStatusView.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/1/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct TimelineItemStatusView: View { - - var selected: Bool - var status: TimelineItemStatus - - var statusView: some View { - ZStack { - Spacer().frame(width: 12) - switch status { - case .showUnread: - if selected { - AppAssets.timelineUnreadSelected - .resizable() - .frame(width: 8, height: 8, alignment: .center) - .padding(.all, 2) - } else { - AppAssets.timelineUnread - .resizable() - .frame(width: 8, height: 8, alignment: .center) - .padding(.all, 2) - } - case .showStar: - AppAssets.timelineStarred - .resizable() - .frame(width: 10, height: 10, alignment: .center) - case .showNone: - AppAssets.timelineUnread - .resizable() - .frame(width: 8, height: 8, alignment: .center) - .padding(.all, 2) - .opacity(0) - } - } - } - - var body: some View { - statusView - .padding(.top, 4) - .padding(.leading, 4) - } - -} diff --git a/Multiplatform/Shared/Timeline/TimelineItemView.swift b/Multiplatform/Shared/Timeline/TimelineItemView.swift deleted file mode 100644 index 955d6dd97..000000000 --- a/Multiplatform/Shared/Timeline/TimelineItemView.swift +++ /dev/null @@ -1,76 +0,0 @@ -// -// TimelineItemView.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/1/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct TimelineItemView: View { - - @EnvironmentObject var defaults: AppDefaults - @StateObject var articleIconImageLoader = ArticleIconImageLoader() - - var selected: Bool - var width: CGFloat - var timelineItem: TimelineItem - - #if os(macOS) - var verticalPadding: CGFloat = 10 - #endif - #if os(iOS) - var verticalPadding: CGFloat = 0 - #endif - - var body: some View { - HStack(alignment: .top) { - TimelineItemStatusView(selected: selected, status: timelineItem.status) - if let image = articleIconImageLoader.image { - IconImageView(iconImage: image) - .frame(width: CGFloat(defaults.timelineIconDimensions), height: CGFloat(defaults.timelineIconDimensions), alignment: /*@START_MENU_TOKEN@*/.center/*@END_MENU_TOKEN@*/) - } - VStack { - let titleLines = timelineItem.numberOfTitleLines(width: width) - if titleLines > 0 { - Text(verbatim: timelineItem.truncatedTitle) - .fontWeight(.semibold) - .lineLimit(titleLines) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.trailing, 4) - .fixedSize(horizontal: false, vertical: true) - } - let summaryLines = timelineItem.numberOfSummaryLines(width: width, titleLines: titleLines) - if summaryLines > 0 { - Text(verbatim: timelineItem.truncatedSummary) - .lineLimit(summaryLines) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.trailing, 4) - .fixedSize(horizontal: false, vertical: true) - } - Spacer(minLength: 0) - HStack { - Text(verbatim: timelineItem.byline) - .lineLimit(1) - .truncationMode(.tail) - .font(.footnote) - .foregroundColor(.secondary) - Spacer() - Text(verbatim: timelineItem.dateTimeString) - .lineLimit(1) - .font(.footnote) - .foregroundColor(.secondary) - .padding(.trailing, 4) - } - } - } - .padding(.vertical, verticalPadding) - .onAppear { - articleIconImageLoader.loadImage(for: timelineItem.article) - } - .contextMenu { - TimelineContextMenu(timelineItem: timelineItem) - } - } -} diff --git a/Multiplatform/Shared/Timeline/TimelineItems.swift b/Multiplatform/Shared/Timeline/TimelineItems.swift deleted file mode 100644 index 787323ff5..000000000 --- a/Multiplatform/Shared/Timeline/TimelineItems.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// TimelineItems.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/25/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation - -struct TimelineItems { - - var index = [String: Int]() - var items = [TimelineItem]() - - init() {} - - subscript(key: String) -> TimelineItem? { - get { - if let position = index[key] { - return items[position] - } - return nil - } - } - - mutating func append(_ item: TimelineItem) { - index[item.id] = item.position - items.append(item) - } - -} diff --git a/Multiplatform/Shared/Timeline/TimelineModel.swift b/Multiplatform/Shared/Timeline/TimelineModel.swift deleted file mode 100644 index 35b1ed020..000000000 --- a/Multiplatform/Shared/Timeline/TimelineModel.swift +++ /dev/null @@ -1,635 +0,0 @@ -// -// TimelineModel.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/30/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -#if os(macOS) -import AppKit -#else -import UIKit -#endif -import Combine -import RSCore -import Account -import Articles - -protocol TimelineModelDelegate: AnyObject { - var selectedFeedsPublisher: AnyPublisher<[Feed], Never>? { get } - func timelineRequestedWebFeedSelection(_: TimelineModel, webFeed: WebFeed) -} - -class TimelineModel: ObservableObject, UndoableCommandRunner { - - weak var delegate: TimelineModelDelegate? - - @Published var nameForDisplay = "" - @Published var selectedTimelineItemIDs = Set() // Don't use directly. Use selectedTimelineItemsPublisher - @Published var selectedTimelineItemID: String? = nil // Don't use directly. Use selectedTimelineItemsPublisher - @Published var listID = "" - - var selectedArticles: [Article] { - return selectedTimelineItems.map { $0.article } - } - - var timelineItemsPublisher: AnyPublisher? - var timelineItemsSelectPublisher: AnyPublisher<(TimelineItems, String?), Never>? - var selectedTimelineItemsPublisher: AnyPublisher<[TimelineItem], Never>? - var selectedArticlesPublisher: AnyPublisher<[Article], Never>? - var articleStatusChangePublisher: AnyPublisher, Never>? - var readFilterAndFeedsPublisher: AnyPublisher<([Feed], Bool?), Never>? - - var articlesSubject = ReplaySubject<[Article], Never>(bufferSize: 1) - - var changeReadFilterSubject = PassthroughSubject() - var selectNextUnreadSubject = PassthroughSubject() - - var readFilterEnabledTable = [FeedIdentifier: Bool]() - - var undoManager: UndoManager? - var undoableCommands = [UndoableCommand]() - - private var cancellables = Set() - - private var sortDirectionSubject = ReplaySubject(bufferSize: 1) - private var groupByFeedSubject = ReplaySubject(bufferSize: 1) - - private var selectedTimelineItems = [TimelineItem]() - private var timelineItems = TimelineItems() - private var articles = [Article]() - - init(delegate: TimelineModelDelegate) { - self.delegate = delegate - subscribeToUserDefaultsChanges() - subscribeToReadFilterAndFeedChanges() - subscribeToArticleFetchChanges() - subscribeToArticleSelectionChanges() - subscribeToArticleStatusChanges() - } - - // MARK: API - - @discardableResult - func goToNextUnread() -> Bool { - var startIndex: Int - if let index = selectedTimelineItems.sorted(by: { $0.position < $1.position }).first?.position { - startIndex = index + 1 - } else { - startIndex = 0 - } - - for i in startIndex.. Article? { - return timelineItems[articleID]?.article - } - - func findPrevArticle(_ article: Article) -> Article? { - guard let index = timelineItems.index[article.articleID], index > 0 else { - return nil - } - return timelineItems.items[index - 1].article - } - - func findNextArticle(_ article: Article) -> Article? { - guard let index = timelineItems.index[article.articleID], index + 1 != timelineItems.items.count else { - return nil - } - return timelineItems.items[index + 1].article - } - - func selectArticle(_ article: Article) { - // TODO: Implement me! - } - - func toggleReadStatusForSelectedArticles() { - guard !selectedArticles.isEmpty else { - return - } - if selectedArticles.anyArticleIsUnread() { - markSelectedArticlesAsRead() - } else { - markSelectedArticlesAsUnread() - } - } - - func canMarkIndicatedArticlesAsRead(_ timelineItem: TimelineItem) -> Bool { - let articles = indicatedTimelineItems(timelineItem).map { $0.article } - return articles.anyArticleIsUnread() - } - - func markIndicatedArticlesAsRead(_ timelineItem: TimelineItem) { - let articles = indicatedTimelineItems(timelineItem).map { $0.article } - markArticlesWithUndo(articles, statusKey: .read, flag: true) - } - - func markSelectedArticlesAsRead() { - markArticlesWithUndo(selectedArticles, statusKey: .read, flag: true) - } - - func canMarkIndicatedArticlesAsUnread(_ timelineItem: TimelineItem) -> Bool { - let articles = indicatedTimelineItems(timelineItem).map { $0.article } - return articles.anyArticleIsReadAndCanMarkUnread() - } - - func markIndicatedArticlesAsUnread(_ timelineItem: TimelineItem) { - let articles = indicatedTimelineItems(timelineItem).map { $0.article } - markArticlesWithUndo(articles, statusKey: .read, flag: false) - } - - func markSelectedArticlesAsUnread() { - markArticlesWithUndo(selectedArticles, statusKey: .read, flag: false) - } - - func canMarkAboveAsRead(_ timelineItem: TimelineItem) -> Bool { - let timelineItem = indicatedAboveTimelineItem(timelineItem) - return articles.articlesAbove(position: timelineItem.position).canMarkAllAsRead() - } - - func markAboveAsRead(_ timelineItem: TimelineItem) { - let timelineItem = indicatedAboveTimelineItem(timelineItem) - let articlesToMark = articles.articlesAbove(position: timelineItem.position) - guard !articlesToMark.isEmpty else { return } - markArticlesWithUndo(articlesToMark, statusKey: .read, flag: true) - } - - func canMarkBelowAsRead(_ timelineItem: TimelineItem) -> Bool { - let timelineItem = indicatedBelowTimelineItem(timelineItem) - return articles.articlesBelow(position: timelineItem.position).canMarkAllAsRead() - } - - func markBelowAsRead(_ timelineItem: TimelineItem) { - let timelineItem = indicatedBelowTimelineItem(timelineItem) - let articlesToMark = articles.articlesBelow(position: timelineItem.position) - guard !articlesToMark.isEmpty else { return } - markArticlesWithUndo(articlesToMark, statusKey: .read, flag: true) - } - - func canMarkAllAsReadInWebFeed(_ timelineItem: TimelineItem) -> Bool { - return timelineItem.article.webFeed?.unreadCount ?? 0 > 0 - } - - func markAllAsReadInWebFeed(_ timelineItem: TimelineItem) { - guard let articlesSet = try? timelineItem.article.webFeed?.fetchArticles() else { return } - let articlesToMark = Array(articlesSet) - markArticlesWithUndo(articlesToMark, statusKey: .read, flag: true) - } - - func canMarkAllAsRead() -> Bool { - return articles.canMarkAllAsRead() - } - - func markAllAsRead() { - markArticlesWithUndo(articles, statusKey: .read, flag: true) - } - - func toggleStarredStatusForSelectedArticles() { - guard !selectedArticles.isEmpty else { - return - } - if selectedArticles.anyArticleIsUnstarred() { - markSelectedArticlesAsStarred() - } else { - markSelectedArticlesAsUnstarred() - } - } - - func canMarkIndicatedArticlesAsStarred(_ timelineItem: TimelineItem) -> Bool { - let articles = indicatedTimelineItems(timelineItem).map { $0.article } - return articles.anyArticleIsUnstarred() - } - - func markIndicatedArticlesAsStarred(_ timelineItem: TimelineItem) { - let articles = indicatedTimelineItems(timelineItem).map { $0.article } - markArticlesWithUndo(articles, statusKey: .starred, flag: true) - } - - func markSelectedArticlesAsStarred() { - markArticlesWithUndo(selectedArticles, statusKey: .starred, flag: true) - } - - func canMarkIndicatedArticlesAsUnstarred(_ timelineItem: TimelineItem) -> Bool { - let articles = indicatedTimelineItems(timelineItem).map { $0.article } - return articles.anyArticleIsStarred() - } - - func markIndicatedArticlesAsUnstarred(_ timelineItem: TimelineItem) { - let articles = indicatedTimelineItems(timelineItem).map { $0.article } - markArticlesWithUndo(articles, statusKey: .starred, flag: false) - } - - func markSelectedArticlesAsUnstarred() { - markArticlesWithUndo(selectedArticles, statusKey: .starred, flag: false) - } - - func canOpenIndicatedArticleInBrowser(_ timelineItem: TimelineItem) -> Bool { - guard indicatedTimelineItems(timelineItem).count == 1 else { return false } - return timelineItem.article.preferredLink != nil - } - - func openSelectedArticleInBrowser() { - guard let article = selectedArticles.first else { return } - openIndicatedArticleInBrowser(article) - } - - func openIndicatedArticleInBrowser(_ timelineItem: TimelineItem) { - openIndicatedArticleInBrowser(timelineItem.article) - } - - func openIndicatedArticleInBrowser(_ article: Article) { - #if os(macOS) - guard let link = article.preferredLink else { return } - Browser.open(link, invertPreference: NSApp.currentEvent?.modifierFlags.contains(.shift) ?? false) - #else - guard let url = article.preferredURL else { return } - UIApplication.shared.open(url, options: [:]) - #endif - } -} - -// MARK: Private - -private extension TimelineModel { - - // MARK: Subscriptions - - func subscribeToArticleStatusChanges() { - articleStatusChangePublisher = NotificationCenter.default.publisher(for: .StatusesDidChange) - .compactMap { $0.userInfo?[Account.UserInfoKey.articleIDs] as? Set } - .eraseToAnyPublisher() - } - - func subscribeToUserDefaultsChanges() { - let kickStartNote = Notification(name: Notification.Name("Kick Start")) - NotificationCenter.default.publisher(for: UserDefaults.didChangeNotification) - .prepend(kickStartNote) - .sink { [weak self] _ in - self?.sortDirectionSubject.send(AppDefaults.shared.timelineSortDirection) - self?.groupByFeedSubject.send(AppDefaults.shared.timelineGroupByFeed) - }.store(in: &cancellables) - } - - func subscribeToReadFilterAndFeedChanges() { - guard let selectedFeedsPublisher = delegate?.selectedFeedsPublisher else { return } - - // Set the timeline name for display - selectedFeedsPublisher - .map { feeds -> String in - switch feeds.count { - case 0: - return "" - case 1: - return feeds.first!.nameForDisplay - default: - return NSLocalizedString("Multiple", comment: "Multiple") - } - } - .assign(to: &$nameForDisplay) - - selectedFeedsPublisher - .map { _ in - return UUID().uuidString - } - .assign(to: &$listID) - - // Clear the selected timeline items when the selected feed(s) change - selectedFeedsPublisher - .sink { [weak self] _ in - self?.selectedTimelineItemIDs = Set() - self?.selectedTimelineItemID = nil - } - .store(in: &cancellables) - - let toggledReadFilterPublisher = changeReadFilterSubject - .map { Optional($0) } - .withLatestFrom(selectedFeedsPublisher, resultSelector: { ($1, $0) }) - .share() - - toggledReadFilterPublisher - .sink { [weak self] (selectedFeeds, readFiltered) in - if let feedID = selectedFeeds.first?.feedID { - self?.readFilterEnabledTable[feedID] = readFiltered - } - } - .store(in: &cancellables) - - let feedsReadFilterPublisher = selectedFeedsPublisher - .map { [weak self] feeds -> ([Feed], Bool?) in - guard let self = self else { return (feeds, nil) } - - guard feeds.count == 1, let timelineFeed = feeds.first else { - return (feeds, nil) - } - - guard timelineFeed.defaultReadFilterType != .alwaysRead else { - return (feeds, nil) - } - - if let feedID = timelineFeed.feedID, let readFilterEnabled = self.readFilterEnabledTable[feedID] { - return (feeds, readFilterEnabled) - } else { - return (feeds, timelineFeed.defaultReadFilterType == .read) - } - } - - readFilterAndFeedsPublisher = toggledReadFilterPublisher - .merge(with: feedsReadFilterPublisher) - .share(replay: 1) - .eraseToAnyPublisher() - } - - func subscribeToArticleFetchChanges() { - guard let readFilterAndFeedsPublisher = readFilterAndFeedsPublisher else { return } - - let sortDirectionPublisher = sortDirectionSubject.removeDuplicates() - let groupByPublisher = groupByFeedSubject.removeDuplicates() - - // Download articles and transform them into timeline items - let inputTimelineItemsPublisher = readFilterAndFeedsPublisher - .flatMap { (feeds, readFilter) in - Self.fetchArticlesPublisher(feeds: feeds, isReadFiltered: readFilter) - } - .combineLatest(sortDirectionPublisher, groupByPublisher) - .compactMap { articles, sortDirection, groupBy -> TimelineItems in - let sortedArticles = Array(articles).sortedByDate(sortDirection ? .orderedDescending : .orderedAscending, groupByFeed: groupBy) - return Self.buildTimelineItems(articles: sortedArticles) - } - - guard let selectedFeedsPublisher = delegate?.selectedFeedsPublisher else { return } - - // Subscribe to any article downloads that may need to update the timeline - let accountDidDownloadPublisher = NotificationCenter.default.publisher(for: .AccountDidDownloadArticles) - .compactMap { $0.userInfo?[Account.UserInfoKey.webFeeds] as? Set } - .withLatestFrom(selectedFeedsPublisher, resultSelector: { ($0, $1) }) - .map { (noteFeeds, selectedFeeds) in - return Self.anyFeedIsPseudoFeed(selectedFeeds) || Self.anyFeedIntersection(selectedFeeds, webFeeds: noteFeeds) - } - .filter { $0 } - - // Download articles and merge them and then transform into timeline items - let downloadTimelineItemsPublisher = accountDidDownloadPublisher - .withLatestFrom(readFilterAndFeedsPublisher) - .flatMap { (feeds, readFilter) in - Self.fetchArticlesPublisher(feeds: feeds, isReadFiltered: readFilter) - } - .withLatestFrom(articlesSubject, sortDirectionPublisher, groupByPublisher, resultSelector: { (downloadArticles, latest) in - return (downloadArticles, latest.0, latest.1, latest.2) - }) - .map { (downloadArticles, currentArticles, sortDirection, groupBy) -> TimelineItems in - let downloadArticleIDs = downloadArticles.articleIDs() - var updatedArticles = downloadArticles - - for article in currentArticles { - if !downloadArticleIDs.contains(article.articleID) { - updatedArticles.insert(article) - } - } - - let sortedArticles = Array(updatedArticles).sortedByDate(sortDirection ? .orderedDescending : .orderedAscending, groupByFeed: groupBy) - return Self.buildTimelineItems(articles: sortedArticles) - } - - timelineItemsPublisher = inputTimelineItemsPublisher - .merge(with: downloadTimelineItemsPublisher) - .share() - .eraseToAnyPublisher() - - timelineItemsPublisher! - .sink { [weak self] timelineItems in - self?.timelineItems = timelineItems - self?.articles = timelineItems.items.map { $0.article } - } - .store(in: &cancellables) - - // Transform to articles for those that just need articles - timelineItemsPublisher! - .map { timelineItems in - timelineItems.items.map { $0.article } - } - .sink { [weak self] articles in - self?.articlesSubject.send(articles) - } - .store(in: &cancellables) - - // Automatically select the first unread if requested - timelineItemsSelectPublisher = timelineItemsPublisher! - .withLatestFrom(selectNextUnreadSubject.prepend(false), resultSelector: { ($0, $1) }) - .map { (timelineItems, selectNextUnread) -> (TimelineItems, String?) in - var selectTimelineItemID: String? = nil - if selectNextUnread { - selectTimelineItemID = timelineItems.items.first(where: { $0.article.status.read == false })?.id - } - return (timelineItems, selectTimelineItemID) - } - .share(replay: 1) - .eraseToAnyPublisher() - - timelineItemsSelectPublisher! - .sink { [weak self] _ in - self?.selectNextUnreadSubject.send(false) - } - .store(in: &cancellables) - } - - func subscribeToArticleSelectionChanges() { - guard let timelineItemsPublisher = timelineItemsPublisher else { return } - - let timelineSelectedIDsPublisher = $selectedTimelineItemIDs - .withLatestFrom(timelineItemsPublisher, resultSelector: { timelineItemIds, timelineItems -> [TimelineItem] in - return timelineItemIds.compactMap { timelineItems[$0] } - }) - - let timelineSelectedIDPublisher = $selectedTimelineItemID - .withLatestFrom(timelineItemsPublisher, resultSelector: { timelineItemId, timelineItems -> [TimelineItem] in - if let id = timelineItemId, let item = timelineItems[id] { - return [item] - } else { - return [TimelineItem]() - } - }) - - selectedTimelineItemsPublisher = timelineSelectedIDsPublisher - .merge(with: timelineSelectedIDPublisher) - .share(replay: 1) - .eraseToAnyPublisher() - - selectedArticlesPublisher = selectedTimelineItemsPublisher! - .map { timelineItems in timelineItems.map { $0.article } } - .share(replay: 1) - .eraseToAnyPublisher() - - selectedTimelineItemsPublisher! - .sink { [weak self] selectedTimelineItems in - self?.selectedTimelineItems = selectedTimelineItems - } - .store(in: &cancellables) - - // Automatically mark a selected record as read - selectedTimelineItemsPublisher! - .filter { $0.count == 1 } - .compactMap { $0.first?.article } - .filter { !$0.status.read } - .sink { markArticles(Set([$0]), statusKey: .read, flag: true) } - .store(in: &cancellables) - } - - // MARK: Article Fetching - - func fetchArticles(feeds: [Feed], isReadFiltered: Bool?) -> Set
{ - if feeds.isEmpty { - return Set
() - } - - var fetchedArticles = Set
() - for feed in feeds { - if isReadFiltered ?? true { - if let articles = try? feed.fetchUnreadArticles() { - fetchedArticles.formUnion(articles) - } - } else { - if let articles = try? feed.fetchArticles() { - fetchedArticles.formUnion(articles) - } - } - } - - return fetchedArticles - } - - static func fetchArticlesPublisher(feeds: [Feed], isReadFiltered: Bool?) -> Future, Never> { - return Future, Never> { promise in - - if feeds.isEmpty { - promise(.success(Set
())) - } - - #if os(macOS) - - var result = Set
() - - for feed in feeds { - if isReadFiltered ?? true { - if let articles = try? feed.fetchUnreadArticles() { - result.formUnion(articles) - } - } else { - if let articles = try? feed.fetchArticles() { - result.formUnion(articles) - } - } - } - - promise(.success(result)) - - #else - - let group = DispatchGroup() - var result = Set
() - - for feed in feeds { - if isReadFiltered ?? true { - group.enter() - feed.fetchUnreadArticlesAsync { articleSetResult in - let articles = (try? articleSetResult.get()) ?? Set
() - result.formUnion(articles) - group.leave() - } - } - else { - group.enter() - feed.fetchArticlesAsync { articleSetResult in - let articles = (try? articleSetResult.get()) ?? Set
() - result.formUnion(articles) - group.leave() - } - } - } - - group.notify(queue: DispatchQueue.main) { - promise(.success(result)) - } - - #endif - - } - - } - - static func buildTimelineItems(articles: [Article]) -> TimelineItems { - var items = TimelineItems() - for (position, article) in articles.enumerated() { - items.append(TimelineItem(position: position, article: article)) - } - return items - } - - static func anyFeedIsPseudoFeed(_ feeds: [Feed]) -> Bool { - return feeds.contains(where: { $0 is PseudoFeed}) - } - - static func anyFeedIntersection(_ feeds: [Feed], webFeeds: Set) -> Bool { - for feed in feeds { - if let selectedWebFeed = feed as? WebFeed { - for webFeed in webFeeds { - if selectedWebFeed.webFeedID == webFeed.webFeedID || selectedWebFeed.url == webFeed.url { - return true - } - } - } else if let folder = feed as? Folder { - for webFeed in webFeeds { - if folder.hasWebFeed(with: webFeed.webFeedID) || folder.hasWebFeed(withURL: webFeed.url) { - return true - } - } - } - } - return false - } - - // MARK: Aricle Marking - - func indicatedTimelineItems(_ timelineItem: TimelineItem) -> [TimelineItem] { - if selectedTimelineItems.contains(where: { $0.id == timelineItem.id }) { - return selectedTimelineItems - } else { - return [timelineItem] - } - } - - func indicatedAboveTimelineItem(_ timelineItem: TimelineItem) -> TimelineItem { - if selectedTimelineItems.contains(where: { $0.id == timelineItem.id }) { - return selectedTimelineItems.sorted(by: { $0.position < $1.position }).first! - } else { - return timelineItem - } - } - - func indicatedBelowTimelineItem(_ timelineItem: TimelineItem) -> TimelineItem { - if selectedTimelineItems.contains(where: { $0.id == timelineItem.id }) { - return selectedTimelineItems.sorted(by: { $0.position < $1.position }).last! - } else { - return timelineItem - } - } - - func markArticlesWithUndo(_ articles: [Article], statusKey: ArticleStatus.Key, flag: Bool) { - if let undoManager = undoManager, let markReadCommand = MarkStatusCommand(initialArticles: articles, statusKey: statusKey, flag: flag, undoManager: undoManager) { - runCommand(markReadCommand) - } else { - markArticles(Set(articles), statusKey: statusKey, flag: flag) - } - } - -} diff --git a/Multiplatform/Shared/Timeline/TimelineSortOrderView.swift b/Multiplatform/Shared/Timeline/TimelineSortOrderView.swift deleted file mode 100644 index eb5034da6..000000000 --- a/Multiplatform/Shared/Timeline/TimelineSortOrderView.swift +++ /dev/null @@ -1,64 +0,0 @@ -// -// TimelineSortOrderView.swift -// Multiplatform macOS -// -// Created by Maurice Parker on 7/12/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct TimelineSortOrderView: View { - - @EnvironmentObject var settings: AppDefaults - @State var selection: Int = 1 - - var body: some View { - Menu { - Button { - settings.timelineSortDirection = true - } label: { - HStack { - Text("Newest to Oldest") - if settings.timelineSortDirection { - Spacer() - AppAssets.checkmarkImage - } - } - } - Button { - settings.timelineSortDirection = false - } label: { - HStack { - Text("Oldest to Newest") - if !settings.timelineSortDirection { - Spacer() - AppAssets.checkmarkImage - } - } - } - Divider() - Button { - settings.timelineGroupByFeed.toggle() - } label: { - HStack { - Text("Group by Feed") - if settings.timelineGroupByFeed { - Spacer() - AppAssets.checkmarkImage - } - } - } - } label : { - if settings.timelineSortDirection { - Text("Sort Newest to Oldest") - } else { - Text("Sort Oldest to Newest") - } - } - .font(.subheadline) - .frame(width: 150) - .padding(.top, 8).padding(.leading) - .menuStyle(BorderlessButtonMenuStyle()) - } -} diff --git a/Multiplatform/Shared/Timeline/TimelineTextSizer.swift b/Multiplatform/Shared/Timeline/TimelineTextSizer.swift deleted file mode 100644 index 82cfcfbf8..000000000 --- a/Multiplatform/Shared/Timeline/TimelineTextSizer.swift +++ /dev/null @@ -1,197 +0,0 @@ -// -// MultilineUILabelSizer.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/16/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -#if os(macOS) -import AppKit -typealias RSFont = NSFont -#else -import UIKit -typealias RSFont = UIFont -#endif - -// Get the height of an NSTextField given a string, font, and width. -// Uses a cache. Avoids actually measuring text as much as possible. -// Main thread only. - -typealias WidthHeightCache = [Int: Int] // width: height - -private struct TextSizerSpecifier: Hashable { - let numberOfLines: Int - let font: RSFont -} - -struct TextSizeInfo { - - let size: CGSize // Integral size (ceiled) - let numberOfLinesUsed: Int // A two-line text field may only use one line, for instance. This would equal 1, then. -} - -final class TimelineTextSizer { - - private let numberOfLines: Int - private let font: RSFont - private let singleLineHeightEstimate: Int - private let doubleLineHeightEstimate: Int - private var cache = [String: WidthHeightCache]() // Each string has a cache. - private static var sizers = [TextSizerSpecifier: TimelineTextSizer]() - - private init(numberOfLines: Int, font: RSFont) { - - self.numberOfLines = numberOfLines - self.font = font - - self.singleLineHeightEstimate = TimelineTextSizer.calculateHeight("AqLjJ0/y", 200, font) - self.doubleLineHeightEstimate = TimelineTextSizer.calculateHeight("AqLjJ0/y\nAqLjJ0/y", 200, font) - - } - - static func size(for string: String, font: RSFont, numberOfLines: Int, width: Int) -> TextSizeInfo { - return sizer(numberOfLines: numberOfLines, font: font).sizeInfo(for: string, width: width) - } - - static func emptyCache() { - sizers = [TextSizerSpecifier: TimelineTextSizer]() - } - -} - -// MARK: - Private - -private extension TimelineTextSizer { - - static func sizer(numberOfLines: Int, font: RSFont) -> TimelineTextSizer { - - let specifier = TextSizerSpecifier(numberOfLines: numberOfLines, font: font) - if let cachedSizer = sizers[specifier] { - return cachedSizer - } - - let newSizer = TimelineTextSizer(numberOfLines: numberOfLines, font: font) - sizers[specifier] = newSizer - return newSizer - - } - - func sizeInfo(for string: String, width: Int) -> TextSizeInfo { - - let textFieldHeight = height(for: string, width: width) - let numberOfLinesUsed = numberOfLines(for: textFieldHeight) - - let size = CGSize(width: width, height: textFieldHeight) - let sizeInfo = TextSizeInfo(size: size, numberOfLinesUsed: numberOfLinesUsed) - return sizeInfo - - } - - func height(for string: String, width: Int) -> Int { - - if cache[string] == nil { - cache[string] = WidthHeightCache() - } - - if let height = cache[string]![width] { - return height - } - - if let height = heightConsideringNeighbors(cache[string]!, width) { - return height - } - - var height = TimelineTextSizer.calculateHeight(string, width, font) - - if numberOfLines != 0 { - let maxHeight = singleLineHeightEstimate * numberOfLines - if height > maxHeight { - height = maxHeight - } - } - - cache[string]![width] = height - - return height - } - - static func calculateHeight(_ string: String, _ width: Int, _ font: RSFont) -> Int { - let height = string.height(withConstrainedWidth: CGFloat(width), font: font) - return Int(ceil(height)) - } - - func numberOfLines(for height: Int) -> Int { - - // We’ll have to see if this really works reliably. - - let averageHeight = CGFloat(doubleLineHeightEstimate) / 2.0 - let lines = Int(round(CGFloat(height) / averageHeight)) - return lines - - } - - func heightIsProbablySingleLineHeight(_ height: Int) -> Bool { - return heightIsProbablyEqualToEstimate(height, singleLineHeightEstimate) - } - - func heightIsProbablyDoubleLineHeight(_ height: Int) -> Bool { - return heightIsProbablyEqualToEstimate(height, doubleLineHeightEstimate) - } - - func heightIsProbablyEqualToEstimate(_ height: Int, _ estimate: Int) -> Bool { - - let slop = 4 - let minimum = estimate - slop - let maximum = estimate + slop - return height >= minimum && height <= maximum - - } - - func heightConsideringNeighbors(_ heightCache: WidthHeightCache, _ width: Int) -> Int? { - - // Given width, if the height at width - something and width + something is equal, - // then that height must be correct for the given width. - // Also: - // If a narrower neighbor’s height is single line height, then this wider width must also be single-line height. - // If a wider neighbor’s height is double line height, and numberOfLines == 2, then this narrower width must able be double-line height. - - var smallNeighbor = (width: 0, height: 0) - var largeNeighbor = (width: 0, height: 0) - - for (oneWidth, oneHeight) in heightCache { - - if oneWidth < width && heightIsProbablySingleLineHeight(oneHeight) { - return oneHeight - } - if numberOfLines == 2 && oneWidth > width && heightIsProbablyDoubleLineHeight(oneHeight) { - return oneHeight - } - - if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) { - smallNeighbor = (oneWidth, oneHeight) - } - else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) { - largeNeighbor = (oneWidth, oneHeight) - } - - if smallNeighbor.width != 0 && smallNeighbor.height == largeNeighbor.height { - return smallNeighbor.height - } - } - - return nil - - } - -} - -extension String { - - func height(withConstrainedWidth width: CGFloat, font: RSFont) -> CGFloat { - let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude) - let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil) - return ceil(boundingBox.height) - } - -} diff --git a/Multiplatform/Shared/Timeline/TimelineToolbarModifier.swift b/Multiplatform/Shared/Timeline/TimelineToolbarModifier.swift deleted file mode 100644 index 59329427f..000000000 --- a/Multiplatform/Shared/Timeline/TimelineToolbarModifier.swift +++ /dev/null @@ -1,66 +0,0 @@ -// -// TimelineToolbarModifier.swift -// NetNewsWire -// -// Created by Maurice Parker on 7/5/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct TimelineToolbarModifier: ViewModifier { - - @EnvironmentObject private var sceneModel: SceneModel - @EnvironmentObject private var timelineModel: TimelineModel - @Environment(\.presentationMode) var presentationMode - #if os(iOS) - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - #endif - @State private var isReadFiltered: Bool? = nil - - func body(content: Content) -> some View { - content - .toolbar { - #if os(iOS) - ToolbarItem(placement: .primaryAction) { - Button { - if let filter = isReadFiltered { - timelineModel.changeReadFilterSubject.send(!filter) - } - } label: { - if isReadFiltered ?? false { - AppAssets.filterActiveImage.font(.title3) - } else { - AppAssets.filterInactiveImage.font(.title3) - } - } - .onReceive(timelineModel.readFilterAndFeedsPublisher!) { (_, filtered) in - isReadFiltered = filtered - } - .hidden(isReadFiltered == nil) - .help(isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles") - } - - ToolbarItem(placement: .bottomBar) { - Button { - sceneModel.markAllAsRead() - #if os(iOS) - if horizontalSizeClass == .compact { - presentationMode.wrappedValue.dismiss() - } - #endif - } label: { - AppAssets.markAllAsReadImage - } - .disabled(sceneModel.markAllAsReadButtonState == nil) - .help("Mark All As Read") - } - - ToolbarItem(placement: .bottomBar) { - Spacer() - } - #endif - } - } - -} diff --git a/Multiplatform/Shared/Timeline/TimelineView.swift b/Multiplatform/Shared/Timeline/TimelineView.swift deleted file mode 100644 index 34a0a2421..000000000 --- a/Multiplatform/Shared/Timeline/TimelineView.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// TimelineView.swift -// NetNewsWire -// -// Created by Maurice Parker on 6/30/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct TimelineView: View { - - @Binding var timelineItems: TimelineItems - @Binding var isReadFiltered: Bool? - - @EnvironmentObject private var timelineModel: TimelineModel - - @State private var timelineItemFrames = [String: CGRect]() - - var body: some View { - GeometryReader { geometryReaderProxy in - #if os(macOS) - VStack { - HStack { - TimelineSortOrderView() - Spacer() - Button (action: { - if let filtered = isReadFiltered { - timelineModel.changeReadFilterSubject.send(!filtered) - } - }, label: { - if isReadFiltered ?? false { - AppAssets.filterActiveImage - } else { - AppAssets.filterInactiveImage - } - }) - .hidden(isReadFiltered == nil) - .padding(.top, 8).padding(.trailing) - .buttonStyle(PlainButtonStyle()) - .help(isReadFiltered ?? false ? "Show Read Articles" : "Filter Read Articles") - } - ScrollViewReader { scrollViewProxy in - List(timelineItems.items, selection: $timelineModel.selectedTimelineItemIDs) { timelineItem in - let selected = timelineModel.selectedTimelineItemIDs.contains(timelineItem.article.articleID) - TimelineItemView(selected: selected, width: geometryReaderProxy.size.width, timelineItem: timelineItem) - .background(TimelineItemFramePreferenceView(timelineItem: timelineItem)) - } - .id(timelineModel.listID) - .onPreferenceChange(TimelineItemFramePreferenceKey.self) { preferences in - for pref in preferences { - timelineItemFrames[pref.articleID] = pref.frame - } - } - .onChange(of: timelineModel.selectedTimelineItemIDs) { selectedArticleIDs in - let proxyFrame = geometryReaderProxy.frame(in: .global) - for articleID in selectedArticleIDs { - if let itemFrame = timelineItemFrames[articleID] { - if itemFrame.minY < proxyFrame.minY + 3 || itemFrame.maxY > proxyFrame.maxY - 35 { - withAnimation { - scrollViewProxy.scrollTo(articleID, anchor: .center) - } - } - } - } - } - } - } - .navigationTitle(Text(verbatim: timelineModel.nameForDisplay)) - #else - ScrollViewReader { scrollViewProxy in - List(timelineItems.items) { timelineItem in - ZStack { - let selected = timelineModel.selectedTimelineItemID == timelineItem.article.articleID - TimelineItemView(selected: selected, width: geometryReaderProxy.size.width, timelineItem: timelineItem) - .background(TimelineItemFramePreferenceView(timelineItem: timelineItem)) - NavigationLink(destination: ArticleContainerView(), - tag: timelineItem.article.articleID, - selection: $timelineModel.selectedTimelineItemID) { - EmptyView() - }.buttonStyle(PlainButtonStyle()) - } - } - .id(timelineModel.listID) - .onPreferenceChange(TimelineItemFramePreferenceKey.self) { preferences in - for pref in preferences { - timelineItemFrames[pref.articleID] = pref.frame - } - } - .onChange(of: timelineModel.selectedTimelineItemID) { selectedArticleID in - let proxyFrame = geometryReaderProxy.frame(in: .global) - if let articleID = selectedArticleID, let itemFrame = timelineItemFrames[articleID] { - if itemFrame.minY < proxyFrame.minY + 3 || itemFrame.maxY > proxyFrame.maxY - 3 { - withAnimation { - scrollViewProxy.scrollTo(articleID, anchor: .center) - } - } - } - } - } - .navigationBarTitle(Text(verbatim: timelineModel.nameForDisplay), displayMode: .inline) - #endif - } - } - -} - -struct TimelineItemFramePreferenceKey: PreferenceKey { - typealias Value = [TimelineItemFramePreference] - - static var defaultValue: [TimelineItemFramePreference] = [] - - static func reduce(value: inout [TimelineItemFramePreference], nextValue: () -> [TimelineItemFramePreference]) { - value.append(contentsOf: nextValue()) - } -} - -struct TimelineItemFramePreference: Equatable { - let articleID: String - let frame: CGRect -} - -struct TimelineItemFramePreferenceView: View { - let timelineItem: TimelineItem - - var body: some View { - GeometryReader { proxy in - Rectangle() - .fill(Color.clear) - .preference(key: TimelineItemFramePreferenceKey.self, - value: [TimelineItemFramePreference(articleID: timelineItem.article.articleID, frame: proxy.frame(in: .global))]) - } - } -} diff --git a/Multiplatform/iOS/AppDelegate.swift b/Multiplatform/iOS/AppDelegate.swift deleted file mode 100644 index 864043edf..000000000 --- a/Multiplatform/iOS/AppDelegate.swift +++ /dev/null @@ -1,412 +0,0 @@ -// -// AppDelegate.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 6/28/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit -import RSCore -import RSWeb -import Account -import BackgroundTasks -import os.log -import Secrets - -var appDelegate: AppDelegate! - -class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider { - - private var bgTaskDispatchQueue = DispatchQueue.init(label: "BGTaskScheduler") - - private var waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid - private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid - - var syncTimer: ArticleStatusSyncTimer? - - var shuttingDown = false { - didSet { - if shuttingDown { - syncTimer?.shuttingDown = shuttingDown - syncTimer?.invalidate() - } - } - } - - var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") - - var userNotificationManager: UserNotificationManager! - var faviconDownloader: FaviconDownloader! - var imageDownloader: ImageDownloader! - var authorAvatarDownloader: AuthorAvatarDownloader! - var webFeedIconDownloader: WebFeedIconDownloader! - // TODO: Add Extension back in -// var extensionContainersFile: ExtensionContainersFile! -// var extensionFeedAddRequestFile: ExtensionFeedAddRequestFile! - - var unreadCount = 0 { - didSet { - if unreadCount != oldValue { - postUnreadCountDidChangeNotification() - UIApplication.shared.applicationIconBadgeNumber = unreadCount - } - } - } - - var isSyncArticleStatusRunning = false - var isWaitingForSyncTasks = false - - override init() { - super.init() - appDelegate = self - - SecretsManager.provider = Secrets() - let documentAccountURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - let documentAccountsFolder = documentAccountURL.appendingPathComponent("Accounts").absoluteString - let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7))) - AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath) - FeedProviderManager.shared.delegate = ExtensionPointManager.shared - - NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil) - } - - func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - AppDefaults.registerDefaults() - - let isFirstRun = AppDefaults.shared.isFirstRun() - if isFirstRun { - os_log("Is first run.", log: log, type: .info) - } - - if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() { - let localAccount = AccountManager.shared.defaultAccount - DefaultFeedsImporter.importDefaultFeeds(account: localAccount) - } - - registerBackgroundTasks() - CacheCleaner.purgeIfNecessary() - initializeDownloaders() - initializeHomeScreenQuickActions() - - DispatchQueue.main.async { - self.unreadCount = AccountManager.shared.unreadCount - } - - UNUserNotificationCenter.current().getNotificationSettings { (settings) in - if settings.authorizationStatus == .authorized { - DispatchQueue.main.async { - UIApplication.shared.registerForRemoteNotifications() - } - } - } - - UNUserNotificationCenter.current().delegate = self - userNotificationManager = UserNotificationManager() - - NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange), name: UserDefaults.didChangeNotification, object: nil) - - -// extensionContainersFile = ExtensionContainersFile() -// extensionFeedAddRequestFile = ExtensionFeedAddRequestFile() - - syncTimer = ArticleStatusSyncTimer() - - #if DEBUG - syncTimer!.update() - #endif - - return true - - } - - func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { - DispatchQueue.main.async { - self.resumeDatabaseProcessingIfNecessary() - AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) { - self.suspendApplication() - completionHandler(.newData) - } - } - } - - func applicationWillTerminate(_ application: UIApplication) { - shuttingDown = true - } - - // MARK: Notifications - - @objc func unreadCountDidChange(_ note: Notification) { - if note.object is AccountManager { - unreadCount = AccountManager.shared.unreadCount - } - } - - @objc func accountRefreshDidFinish(_ note: Notification) { - AppDefaults.shared.lastRefresh = Date() - } - - // MARK: - API - - func resumeDatabaseProcessingIfNecessary() { - if AccountManager.shared.isSuspended { - AccountManager.shared.resumeAll() - os_log("Application processing resumed.", log: self.log, type: .info) - } - } - - func prepareAccountsForBackground() { -// extensionFeedAddRequestFile.suspend() - syncTimer?.invalidate() - scheduleBackgroundFeedRefresh() - syncArticleStatus() - waitForSyncTasksToFinish() - } - - func prepareAccountsForForeground() { -// extensionFeedAddRequestFile.resume() - syncTimer?.update() - - if let lastRefresh = AppDefaults.shared.lastRefresh { - if Date() > lastRefresh.addingTimeInterval(15 * 60) { - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) - } else { - AccountManager.shared.syncArticleStatusAll() - } - } else { - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) - } - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler([.banner, .badge, .sound]) - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { - defer { completionHandler() } - - // TODO: Add back in User Notification handling -// if let sceneDelegate = response.targetScene?.delegate as? SceneDelegate { -// sceneDelegate.handle(response) -// } - - } - -} - -// MARK: App Initialization - -private extension AppDelegate { - - private func initializeDownloaders() { - let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! - let faviconsFolderURL = tempDir.appendingPathComponent("Favicons") - let imagesFolderURL = tempDir.appendingPathComponent("Images") - - try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil) - let faviconsFolder = faviconsFolderURL.absoluteString - let faviconsFolderPath = faviconsFolder.suffix(from: faviconsFolder.index(faviconsFolder.startIndex, offsetBy: 7)) - faviconDownloader = FaviconDownloader(folder: String(faviconsFolderPath)) - - let imagesFolder = imagesFolderURL.absoluteString - let imagesFolderPath = imagesFolder.suffix(from: imagesFolder.index(imagesFolder.startIndex, offsetBy: 7)) - try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil) - imageDownloader = ImageDownloader(folder: String(imagesFolderPath)) - - authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader) - - let tempFolder = tempDir.absoluteString - let tempFolderPath = tempFolder.suffix(from: tempFolder.index(tempFolder.startIndex, offsetBy: 7)) - webFeedIconDownloader = WebFeedIconDownloader(imageDownloader: imageDownloader, folder: String(tempFolderPath)) - } - - private func initializeHomeScreenQuickActions() { - let unreadTitle = NSLocalizedString("First Unread", comment: "First Unread") - let unreadIcon = UIApplicationShortcutIcon(systemImageName: "chevron.down.circle") - let unreadItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.FirstUnread", localizedTitle: unreadTitle, localizedSubtitle: nil, icon: unreadIcon, userInfo: nil) - - let searchTitle = NSLocalizedString("Search", comment: "Search") - let searchIcon = UIApplicationShortcutIcon(systemImageName: "magnifyingglass") - let searchItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.ShowSearch", localizedTitle: searchTitle, localizedSubtitle: nil, icon: searchIcon, userInfo: nil) - - let addTitle = NSLocalizedString("Add Feed", comment: "Add Feed") - let addIcon = UIApplicationShortcutIcon(systemImageName: "plus") - let addItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.ShowAdd", localizedTitle: addTitle, localizedSubtitle: nil, icon: addIcon, userInfo: nil) - - UIApplication.shared.shortcutItems = [addItem, searchItem, unreadItem] - } - -} - -// MARK: Go To Background - -private extension AppDelegate { - - func waitForSyncTasksToFinish() { - guard !isWaitingForSyncTasks && UIApplication.shared.applicationState == .background else { return } - - isWaitingForSyncTasks = true - - self.waitBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { [weak self] in - guard let self = self else { return } - self.completeProcessing(true) - os_log("Accounts wait for progress terminated for running too long.", log: self.log, type: .info) - } - - DispatchQueue.main.async { [weak self] in - self?.waitToComplete() { [weak self] suspend in - self?.completeProcessing(suspend) - } - } - } - - func waitToComplete(completion: @escaping (Bool) -> Void) { - guard UIApplication.shared.applicationState == .background else { - os_log("App came back to forground, no longer waiting.", log: self.log, type: .info) - completion(false) - return - } - - if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning { - os_log("Waiting for sync to finish...", log: self.log, type: .info) - DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in - self?.waitToComplete(completion: completion) - } - } else { - os_log("Refresh progress complete.", log: self.log, type: .info) - completion(true) - } - } - - func completeProcessing(_ suspend: Bool) { - if suspend { - suspendApplication() - } - UIApplication.shared.endBackgroundTask(self.waitBackgroundUpdateTask) - self.waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid - isWaitingForSyncTasks = false - } - - func syncArticleStatus() { - guard !isSyncArticleStatusRunning else { return } - - isSyncArticleStatusRunning = true - - let completeProcessing = { [unowned self] in - self.isSyncArticleStatusRunning = false - UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask) - self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid - } - - self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { - completeProcessing() - os_log("Accounts sync processing terminated for running too long.", log: self.log, type: .info) - } - - DispatchQueue.main.async { - AccountManager.shared.syncArticleStatusAll() { - completeProcessing() - } - } - } - - func suspendApplication() { - guard UIApplication.shared.applicationState == .background else { return } - - AccountManager.shared.suspendNetworkAll() - AccountManager.shared.suspendDatabaseAll() - CoalescingQueue.standard.performCallsImmediately() - - os_log("Application processing suspended.", log: self.log, type: .info) - } - -} - -// MARK: Background Tasks - -private extension AppDelegate { - - /// Register all background tasks. - func registerBackgroundTasks() { - // Register background feed refresh. - BGTaskScheduler.shared.register(forTaskWithIdentifier: "com.ranchero.NetNewsWire.FeedRefresh", using: nil) { (task) in - self.performBackgroundFeedRefresh(with: task as! BGAppRefreshTask) - } - } - - /// Schedules a background app refresh based on `AppDefaults.refreshInterval`. - func scheduleBackgroundFeedRefresh() { - let request = BGAppRefreshTaskRequest(identifier: "com.ranchero.NetNewsWire.FeedRefresh") - request.earliestBeginDate = Date(timeIntervalSinceNow: 15 * 60) - - // We send this to a dedicated serial queue because as of 11/05/19 on iOS 13.2 the call to the - // task scheduler can hang indefinitely. - bgTaskDispatchQueue.async { - do { - try BGTaskScheduler.shared.submit(request) - } catch { - os_log(.error, log: self.log, "Could not schedule app refresh: %@", error.localizedDescription) - } - } - } - - /// Performs background feed refresh. - /// - Parameter task: `BGAppRefreshTask` - /// - Warning: As of Xcode 11 beta 2, when triggered from the debugger this doesn't work. - func performBackgroundFeedRefresh(with task: BGAppRefreshTask) { - - scheduleBackgroundFeedRefresh() // schedule next refresh - - os_log("Woken to perform account refresh.", log: self.log, type: .info) - - DispatchQueue.main.async { - if AccountManager.shared.isSuspended { - AccountManager.shared.resumeAll() - } - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) { [unowned self] in - if !AccountManager.shared.isSuspended { - self.suspendApplication() - os_log("Account refresh operation completed.", log: self.log, type: .info) - task.setTaskCompleted(success: true) - } - } - } - - // set expiration handler - task.expirationHandler = { [weak task] in - DispatchQueue.main.sync { - self.suspendApplication() - } - os_log("Accounts refresh processing terminated for running too long.", log: self.log, type: .info) - task?.setTaskCompleted(success: false) - } - } - -} - -private extension AppDelegate { - @objc func userDefaultsDidChange() { - updateUserInterfaceStyle() - } - - var window: UIWindow? { - guard let scene = UIApplication.shared.connectedScenes.first, - let windowSceneDelegate = scene.delegate as? UIWindowSceneDelegate, - let window = windowSceneDelegate.window else { - return nil - } - return window - } - - func updateUserInterfaceStyle() { -// switch AppDefaults.shared.userInterfaceColorPalette { -// case .automatic: -// window?.overrideUserInterfaceStyle = .unspecified -// case .light: -// window?.overrideUserInterfaceStyle = .light -// case .dark: -// window?.overrideUserInterfaceStyle = .dark -// } - } -} diff --git a/Multiplatform/iOS/Article/ActivityViewController.swift b/Multiplatform/iOS/Article/ActivityViewController.swift deleted file mode 100644 index 9f4497b1f..000000000 --- a/Multiplatform/iOS/Article/ActivityViewController.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// ArticleShareView.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/13/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit -import SwiftUI -import Articles - -extension UIActivityViewController { - convenience init(url: URL, title: String?, applicationActivities: [UIActivity]?) { - let itemSource = ArticleActivityItemSource(url: url, subject: title) - let titleSource = TitleActivityItemSource(title: title) - self.init(activityItems: [titleSource, itemSource], applicationActivities: applicationActivities) - } -} - -struct ActivityViewController: UIViewControllerRepresentable { - - var title: String? - var url: URL - - func makeUIViewController(context: Context) -> UIActivityViewController { - return UIActivityViewController(url: url, title: title, applicationActivities: [FindInArticleActivity(), OpenInSafariActivity()]) - } - - func updateUIViewController(_ controller: UIActivityViewController, context: Context) { - } - -} diff --git a/Multiplatform/iOS/Article/ArticleActivityItemSource.swift b/Multiplatform/iOS/Article/ArticleActivityItemSource.swift deleted file mode 100644 index f87258a9d..000000000 --- a/Multiplatform/iOS/Article/ArticleActivityItemSource.swift +++ /dev/null @@ -1,33 +0,0 @@ -// -// ArticleActivityItemSource.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/13/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit - -class ArticleActivityItemSource: NSObject, UIActivityItemSource { - - private let url: URL - private let subject: String? - - init(url: URL, subject: String?) { - self.url = url - self.subject = subject - } - - func activityViewControllerPlaceholderItem(_ : UIActivityViewController) -> Any { - return url - } - - func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { - return url - } - - func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String { - return subject ?? "" - } - -} diff --git a/Multiplatform/iOS/Article/ArticleView.swift b/Multiplatform/iOS/Article/ArticleView.swift deleted file mode 100644 index 4310e51c3..000000000 --- a/Multiplatform/iOS/Article/ArticleView.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// ArticleView.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Articles - -struct ArticleView: UIViewControllerRepresentable { - - @EnvironmentObject private var sceneModel: SceneModel - - func makeUIViewController(context: Context) -> ArticleViewController { - let controller = ArticleViewController() - controller.sceneModel = sceneModel - return controller - } - - func updateUIViewController(_ uiViewController: ArticleViewController, context: Context) { - - } - -} diff --git a/Multiplatform/iOS/Article/ArticleViewController.swift b/Multiplatform/iOS/Article/ArticleViewController.swift deleted file mode 100644 index 96581c83b..000000000 --- a/Multiplatform/iOS/Article/ArticleViewController.swift +++ /dev/null @@ -1,163 +0,0 @@ -// -// ArticleViewController.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit -import Combine -import WebKit -import Account -import Articles -import SafariServices - -class ArticleViewController: UIViewController { - - weak var sceneModel: SceneModel? - - private var pageViewController: UIPageViewController! - - private var currentWebViewController: WebViewController? { - return pageViewController?.viewControllers?.first as? WebViewController - } - - var articles: [Article]? { - didSet { - currentArticle = articles?.first - } - } - - var currentArticle: Article? { - didSet { - if let controller = currentWebViewController, controller.article != currentArticle { - controller.setArticle(currentArticle) - DispatchQueue.main.async { - // You have to set the view controller to clear out the UIPageViewController child controller cache. - // You also have to do it in an async call or you will get a strange assertion error. - self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil) - } - } - } - } - - private var cancellables = Set() - - override func viewDidLoad() { - super.viewDidLoad() - - pageViewController = UIPageViewController(transitionStyle: .scroll, navigationOrientation: .horizontal, options: [:]) - pageViewController.delegate = self - pageViewController.dataSource = self - - pageViewController.view.translatesAutoresizingMaskIntoConstraints = false - view.addSubview(pageViewController.view) - addChild(pageViewController!) - NSLayoutConstraint.activate([ - view.leadingAnchor.constraint(equalTo: pageViewController.view.leadingAnchor), - view.trailingAnchor.constraint(equalTo: pageViewController.view.trailingAnchor), - view.topAnchor.constraint(equalTo: pageViewController.view.topAnchor), - view.bottomAnchor.constraint(equalTo: pageViewController.view.bottomAnchor) - ]) - - sceneModel?.timelineModel.selectedArticlesPublisher?.sink { [weak self] articles in - self?.articles = articles - } - .store(in: &cancellables) - - let controller = createWebViewController(currentArticle, updateView: true) - self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil) - - } - - // MARK: API - - func focus() { - currentWebViewController?.focus() - } - - func canScrollDown() -> Bool { - return currentWebViewController?.canScrollDown() ?? false - } - - func scrollPageDown() { - currentWebViewController?.scrollPageDown() - } - - func stopArticleExtractorIfProcessing() { - currentWebViewController?.stopArticleExtractorIfProcessing() - } - -} - - -// MARK: WebViewControllerDelegate - -extension ArticleViewController: WebViewControllerDelegate { - - func webViewController(_ webViewController: WebViewController, articleExtractorButtonStateDidUpdate buttonState: ArticleExtractorButtonState) { - if webViewController === currentWebViewController { -// articleExtractorButton.buttonState = buttonState - } - } - -} - -// MARK: UIPageViewControllerDataSource - -extension ArticleViewController: UIPageViewControllerDataSource { - - func pageViewController(_ pageViewController: UIPageViewController, viewControllerBefore viewController: UIViewController) -> UIViewController? { - guard let webViewController = viewController as? WebViewController, - let currentArticle = webViewController.article, - let article = sceneModel?.findPrevArticle(currentArticle) else { - return nil - } - return createWebViewController(article) - } - - func pageViewController(_ pageViewController: UIPageViewController, viewControllerAfter viewController: UIViewController) -> UIViewController? { - guard let webViewController = viewController as? WebViewController, - let currentArticle = webViewController.article, - let article = sceneModel?.findNextArticle(currentArticle) else { - return nil - } - return createWebViewController(article) - } - -} - -// MARK: UIPageViewControllerDelegate - -extension ArticleViewController: UIPageViewControllerDelegate { - - func pageViewController(_ pageViewController: UIPageViewController, didFinishAnimating finished: Bool, previousViewControllers: [UIViewController], transitionCompleted completed: Bool) { - guard finished, completed else { return } -// guard let article = currentWebViewController?.article else { return } - -// articleExtractorButton.buttonState = currentWebViewController?.articleExtractorButtonState ?? .off - - previousViewControllers.compactMap({ $0 as? WebViewController }).forEach({ $0.stopWebViewActivity() }) - } - -} - -// MARK: Private - -private extension ArticleViewController { - - func createWebViewController(_ article: Article?, updateView: Bool = true) -> WebViewController { - let controller = WebViewController() - controller.sceneModel = sceneModel - controller.delegate = self - controller.setArticle(article, updateView: updateView) - return controller - } - -} - -public extension Notification.Name { - static let FindInArticle = Notification.Name("FindInArticle") - static let EndFindInArticle = Notification.Name("EndFindInArticle") -} diff --git a/Multiplatform/iOS/Article/FindInArticleActivity.swift b/Multiplatform/iOS/Article/FindInArticleActivity.swift deleted file mode 100644 index 334a142fb..000000000 --- a/Multiplatform/iOS/Article/FindInArticleActivity.swift +++ /dev/null @@ -1,40 +0,0 @@ -// -// FindInArticleActivity.swift -// NetNewsWire-iOS -// -// Created by Brian Sanders on 5/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit - -class FindInArticleActivity: UIActivity { - override var activityTitle: String? { - NSLocalizedString("Find in Article", comment: "Find in Article") - } - - override var activityType: UIActivity.ActivityType? { - UIActivity.ActivityType(rawValue: "com.ranchero.NetNewsWire.find") - } - - override var activityImage: UIImage? { - UIImage(systemName: "magnifyingglass", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)) - } - - override class var activityCategory: UIActivity.Category { - .action - } - - override func canPerform(withActivityItems activityItems: [Any]) -> Bool { - true - } - - override func prepare(withActivityItems activityItems: [Any]) { - - } - - override func perform() { - NotificationCenter.default.post(Notification(name: .FindInArticle)) - activityDidFinish(true) - } -} diff --git a/Multiplatform/iOS/Article/IconView.swift b/Multiplatform/iOS/Article/IconView.swift deleted file mode 100644 index dc44dc0d6..000000000 --- a/Multiplatform/iOS/Article/IconView.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// IconView.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit - -final class IconView: UIView { - - var iconImage: IconImage? = nil { - didSet { - if iconImage !== oldValue { - imageView.image = iconImage?.image - - if self.traitCollection.userInterfaceStyle == .dark { - if self.iconImage?.isDark ?? false { - self.isDiscernable = false - self.setNeedsLayout() - } else { - self.isDiscernable = true - self.setNeedsLayout() - } - } else { - if self.iconImage?.isBright ?? false { - self.isDiscernable = false - self.setNeedsLayout() - } else { - self.isDiscernable = true - self.setNeedsLayout() - } - } - self.setNeedsLayout() - } - } - } - - private var isDiscernable = true - - private let imageView: UIImageView = { - let imageView = UIImageView(image: AppAssets.faviconTemplateImage) - imageView.contentMode = .scaleAspectFit - imageView.clipsToBounds = true - imageView.layer.cornerRadius = 4.0 - return imageView - }() - - private var isVerticalBackgroundExposed: Bool { - return imageView.frame.size.height < bounds.size.height - } - - private var isSymbolImage: Bool { - return iconImage?.isSymbol ?? false - } - - private var isBackgroundSuppressed: Bool { - return iconImage?.isBackgroundSupressed ?? false - } - - override init(frame: CGRect) { - super.init(frame: frame) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - convenience init() { - self.init(frame: .zero) - } - - override func didMoveToSuperview() { - setNeedsLayout() - } - - override func layoutSubviews() { - imageView.setFrameIfNotEqual(rectForImageView()) - if (iconImage != nil && isVerticalBackgroundExposed) || !isDiscernable { - backgroundColor = AppAssets.uiIconBackgroundColor - } else { - backgroundColor = nil - } - } - -} - -private extension IconView { - - func commonInit() { - layer.cornerRadius = 4 - clipsToBounds = true - addSubview(imageView) - } - - func rectForImageView() -> CGRect { - guard let image = iconImage?.image else { - return CGRect.zero - } - - let imageSize = image.size - let viewSize = bounds.size - if imageSize.height == imageSize.width { - if imageSize.height >= viewSize.height { - return CGRect(x: 0.0, y: 0.0, width: viewSize.width, height: viewSize.height) - } - let offset = floor((viewSize.height - imageSize.height) / 2.0) - return CGRect(x: offset, y: offset, width: imageSize.width, height: imageSize.height) - } - else if imageSize.height > imageSize.width { - let factor = viewSize.height / imageSize.height - let width = imageSize.width * factor - let originX = floor((viewSize.width - width) / 2.0) - return CGRect(x: originX, y: 0.0, width: width, height: viewSize.height) - } - - // Wider than tall: imageSize.width > imageSize.height - let factor = viewSize.width / imageSize.width - let height = imageSize.height * factor - let originY = floor((viewSize.height - height) / 2.0) - return CGRect(x: 0.0, y: originY, width: viewSize.width, height: height) - } - -} diff --git a/Multiplatform/iOS/Article/ImageScrollView.swift b/Multiplatform/iOS/Article/ImageScrollView.swift deleted file mode 100644 index 31fe3289c..000000000 --- a/Multiplatform/iOS/Article/ImageScrollView.swift +++ /dev/null @@ -1,361 +0,0 @@ -// -// ImageScrollView.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit - -@objc public protocol ImageScrollViewDelegate: UIScrollViewDelegate { - func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView) - func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView) -} - -open class ImageScrollView: UIScrollView { - - @objc public enum ScaleMode: Int { - case aspectFill - case aspectFit - case widthFill - case heightFill - } - - @objc public enum Offset: Int { - case begining - case center - } - - static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2 - - @objc open var imageContentMode: ScaleMode = .widthFill - @objc open var initialOffset: Offset = .begining - - @objc public private(set) var zoomView: UIImageView? = nil - - @objc open weak var imageScrollViewDelegate: ImageScrollViewDelegate? - - var imageSize: CGSize = CGSize.zero - private var pointToCenterAfterResize: CGPoint = CGPoint.zero - private var scaleToRestoreAfterResize: CGFloat = 1.0 - var maxScaleFromMinScale: CGFloat = 3.0 - - var zoomedFrame: CGRect { - return zoomView?.frame ?? CGRect.zero - } - - override open var frame: CGRect { - willSet { - if frame.equalTo(newValue) == false && newValue.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false { - prepareToResize() - } - } - - didSet { - if frame.equalTo(oldValue) == false && frame.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false { - recoverFromResizing() - } - } - } - - override public init(frame: CGRect) { - super.init(frame: frame) - - initialize() - } - - required public init?(coder aDecoder: NSCoder) { - super.init(coder: aDecoder) - - initialize() - } - - private func initialize() { - showsVerticalScrollIndicator = false - showsHorizontalScrollIndicator = false - bouncesZoom = true - decelerationRate = UIScrollView.DecelerationRate.fast - delegate = self - } - - @objc public func adjustFrameToCenter() { - - guard let unwrappedZoomView = zoomView else { - return - } - - var frameToCenter = unwrappedZoomView.frame - - // center horizontally - if frameToCenter.size.width < bounds.width { - frameToCenter.origin.x = (bounds.width - frameToCenter.size.width) / 2 - } else { - frameToCenter.origin.x = 0 - } - - // center vertically - if frameToCenter.size.height < bounds.height { - frameToCenter.origin.y = (bounds.height - frameToCenter.size.height) / 2 - } else { - frameToCenter.origin.y = 0 - } - - unwrappedZoomView.frame = frameToCenter - } - - private func prepareToResize() { - let boundsCenter = CGPoint(x: bounds.midX, y: bounds.midY) - pointToCenterAfterResize = convert(boundsCenter, to: zoomView) - - scaleToRestoreAfterResize = zoomScale - - // If we're at the minimum zoom scale, preserve that by returning 0, which will be converted to the minimum - // allowable scale when the scale is restored. - if scaleToRestoreAfterResize <= minimumZoomScale + CGFloat(Float.ulpOfOne) { - scaleToRestoreAfterResize = 0 - } - } - - private func recoverFromResizing() { - setMaxMinZoomScalesForCurrentBounds() - - // restore zoom scale, first making sure it is within the allowable range. - let maxZoomScale = max(minimumZoomScale, scaleToRestoreAfterResize) - zoomScale = min(maximumZoomScale, maxZoomScale) - - // restore center point, first making sure it is within the allowable range. - - // convert our desired center point back to our own coordinate space - let boundsCenter = convert(pointToCenterAfterResize, to: zoomView) - - // calculate the content offset that would yield that center point - var offset = CGPoint(x: boundsCenter.x - bounds.size.width/2.0, y: boundsCenter.y - bounds.size.height/2.0) - - // restore offset, adjusted to be within the allowable range - let maxOffset = maximumContentOffset() - let minOffset = minimumContentOffset() - - var realMaxOffset = min(maxOffset.x, offset.x) - offset.x = max(minOffset.x, realMaxOffset) - - realMaxOffset = min(maxOffset.y, offset.y) - offset.y = max(minOffset.y, realMaxOffset) - - contentOffset = offset - } - - private func maximumContentOffset() -> CGPoint { - return CGPoint(x: contentSize.width - bounds.width,y:contentSize.height - bounds.height) - } - - private func minimumContentOffset() -> CGPoint { - return CGPoint.zero - } - - // MARK: - Set up - - open func setup() { - var topSupperView = superview - - while topSupperView?.superview != nil { - topSupperView = topSupperView?.superview - } - - // Make sure views have already layout with precise frame - topSupperView?.layoutIfNeeded() - } - - // MARK: - Display image - - @objc open func display(image: UIImage) { - - if let zoomView = zoomView { - zoomView.removeFromSuperview() - } - - zoomView = UIImageView(image: image) - zoomView!.isUserInteractionEnabled = true - addSubview(zoomView!) - - let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doubleTapGestureRecognizer(_:))) - tapGesture.numberOfTapsRequired = 2 - zoomView!.addGestureRecognizer(tapGesture) - - let downSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeUpGestureRecognizer(_:))) - downSwipeGesture.direction = .down - zoomView!.addGestureRecognizer(downSwipeGesture) - - let upSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeDownGestureRecognizer(_:))) - upSwipeGesture.direction = .up - zoomView!.addGestureRecognizer(upSwipeGesture) - - configureImageForSize(image.size) - adjustFrameToCenter() - } - - private func configureImageForSize(_ size: CGSize) { - imageSize = size - contentSize = imageSize - setMaxMinZoomScalesForCurrentBounds() - zoomScale = minimumZoomScale - - switch initialOffset { - case .begining: - contentOffset = CGPoint.zero - case .center: - let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2 - let yOffset = contentSize.height < bounds.height ? 0 : (contentSize.height - bounds.height)/2 - - switch imageContentMode { - case .aspectFit: - contentOffset = CGPoint.zero - case .aspectFill: - contentOffset = CGPoint(x: xOffset, y: yOffset) - case .heightFill: - contentOffset = CGPoint(x: xOffset, y: 0) - case .widthFill: - contentOffset = CGPoint(x: 0, y: yOffset) - } - } - } - - private func setMaxMinZoomScalesForCurrentBounds() { - // calculate min/max zoomscale - let xScale = bounds.width / imageSize.width // the scale needed to perfectly fit the image width-wise - let yScale = bounds.height / imageSize.height // the scale needed to perfectly fit the image height-wise - - var minScale: CGFloat = 1 - - switch imageContentMode { - case .aspectFill: - minScale = max(xScale, yScale) - case .aspectFit: - minScale = min(xScale, yScale) - case .widthFill: - minScale = xScale - case .heightFill: - minScale = yScale - } - - - let maxScale = maxScaleFromMinScale*minScale - - // don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.) - if minScale > maxScale { - minScale = maxScale - } - - maximumZoomScale = maxScale - minimumZoomScale = minScale // * 0.999 // the multiply factor to prevent user cannot scroll page while they use this control in UIPageViewController - } - - // MARK: - Gesture - - @objc func doubleTapGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { - // zoom out if it bigger than middle scale point. Else, zoom in - if zoomScale >= maximumZoomScale / 2.0 { - setZoomScale(minimumZoomScale, animated: true) - } else { - let center = gestureRecognizer.location(in: gestureRecognizer.view) - let zoomRect = zoomRectForScale(ImageScrollView.kZoomInFactorFromMinWhenDoubleTap * minimumZoomScale, center: center) - zoom(to: zoomRect, animated: true) - } - } - - @objc func swipeUpGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { - if gestureRecognizer.state == .ended { - imageScrollViewDelegate?.imageScrollViewDidGestureSwipeUp(imageScrollView: self) - } - } - - @objc func swipeDownGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { - if gestureRecognizer.state == .ended { - imageScrollViewDelegate?.imageScrollViewDidGestureSwipeDown(imageScrollView: self) - } - } - - private func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect { - var zoomRect = CGRect.zero - - // the zoom rect is in the content view's coordinates. - // at a zoom scale of 1.0, it would be the size of the imageScrollView's bounds. - // as the zoom scale decreases, so more content is visible, the size of the rect grows. - zoomRect.size.height = frame.size.height / scale - zoomRect.size.width = frame.size.width / scale - - // choose an origin so as to get the right center. - zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0) - zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0) - - return zoomRect - } - - open func refresh() { - if let image = zoomView?.image { - display(image: image) - } - } - - open func resize() { - self.configureImageForSize(self.imageSize) - } -} - -extension ImageScrollView: UIScrollViewDelegate { - - public func scrollViewDidScroll(_ scrollView: UIScrollView) { - imageScrollViewDelegate?.scrollViewDidScroll?(scrollView) - } - - public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { - imageScrollViewDelegate?.scrollViewWillBeginDragging?(scrollView) - } - - public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer) { - imageScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset) - } - - public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { - imageScrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate) - } - - public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { - imageScrollViewDelegate?.scrollViewWillBeginDecelerating?(scrollView) - } - - public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { - imageScrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView) - } - - public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { - imageScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView) - } - - public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { - imageScrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view) - } - - public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { - imageScrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale) - } - - public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { - return false - } - - @available(iOS 11.0, *) - public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) { - imageScrollViewDelegate?.scrollViewDidChangeAdjustedContentInset?(scrollView) - } - - public func viewForZooming(in scrollView: UIScrollView) -> UIView? { - return zoomView - } - - public func scrollViewDidZoom(_ scrollView: UIScrollView) { - adjustFrameToCenter() - imageScrollViewDelegate?.scrollViewDidZoom?(scrollView) - } - -} diff --git a/Multiplatform/iOS/Article/ImageTransition.swift b/Multiplatform/iOS/Article/ImageTransition.swift deleted file mode 100644 index 01f460348..000000000 --- a/Multiplatform/iOS/Article/ImageTransition.swift +++ /dev/null @@ -1,110 +0,0 @@ -// -// ImageTransition.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit - -class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning { - - private weak var webViewController: WebViewController? - private let duration = 0.4 - var presenting = true - var originFrame: CGRect! - var maskFrame: CGRect! - var originImage: UIImage! - - init(controller: WebViewController) { - self.webViewController = controller - } - - func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { - return duration - } - - func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { - if presenting { - animateTransitionPresenting(using: transitionContext) - } else { - animateTransitionReturning(using: transitionContext) - } - } - - private func animateTransitionPresenting(using transitionContext: UIViewControllerContextTransitioning) { - - let imageView = UIImageView(image: originImage) - imageView.frame = originFrame - - let fromView = transitionContext.view(forKey: .from)! - fromView.removeFromSuperview() - - transitionContext.containerView.backgroundColor = .systemBackground - transitionContext.containerView.addSubview(imageView) - - webViewController?.hideClickedImage() - - UIView.animate( - withDuration: duration, - delay:0.0, - usingSpringWithDamping: 0.8, - initialSpringVelocity: 0.2, - animations: { - let imageController = transitionContext.viewController(forKey: .to) as! ImageViewController - imageView.frame = imageController.zoomedFrame - }, completion: { _ in - imageView.removeFromSuperview() - let toView = transitionContext.view(forKey: .to)! - transitionContext.containerView.addSubview(toView) - transitionContext.completeTransition(true) - }) - } - - private func animateTransitionReturning(using transitionContext: UIViewControllerContextTransitioning) { - let imageController = transitionContext.viewController(forKey: .from) as! ImageViewController - let imageView = UIImageView(image: originImage) - imageView.frame = imageController.zoomedFrame - - let fromView = transitionContext.view(forKey: .from)! - let windowFrame = fromView.window!.frame - fromView.removeFromSuperview() - - let toView = transitionContext.view(forKey: .to)! - transitionContext.containerView.addSubview(toView) - - let maskingView = UIView() - - let fullMaskFrame = CGRect(x: windowFrame.minX, y: maskFrame.minY, width: windowFrame.width, height: maskFrame.height) - let path = UIBezierPath(rect: fullMaskFrame) - let maskLayer = CAShapeLayer() - maskLayer.path = path.cgPath - maskingView.layer.mask = maskLayer - - maskingView.addSubview(imageView) - transitionContext.containerView.addSubview(maskingView) - - UIView.animate( - withDuration: duration, - delay:0.0, - usingSpringWithDamping: 0.8, - initialSpringVelocity: 0.2, - animations: { - imageView.frame = self.originFrame - }, completion: { _ in - if let controller = self.webViewController { - controller.showClickedImage() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - imageView.removeFromSuperview() - transitionContext.completeTransition(true) - } - } - } else { - imageView.removeFromSuperview() - transitionContext.completeTransition(true) - } - }) - } - -} diff --git a/Multiplatform/iOS/Article/ImageViewController.swift b/Multiplatform/iOS/Article/ImageViewController.swift deleted file mode 100644 index 4e9ae9466..000000000 --- a/Multiplatform/iOS/Article/ImageViewController.swift +++ /dev/null @@ -1,92 +0,0 @@ -// -// ImageViewController.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit - -class ImageViewController: UIViewController { - - @IBOutlet weak var closeButton: UIButton! - @IBOutlet weak var shareButton: UIButton! - @IBOutlet weak var imageScrollView: ImageScrollView! - @IBOutlet weak var titleLabel: UILabel! - @IBOutlet weak var titleBackground: UIVisualEffectView! - @IBOutlet weak var titleLeading: NSLayoutConstraint! - @IBOutlet weak var titleTrailing: NSLayoutConstraint! - - var image: UIImage! - var imageTitle: String? - var zoomedFrame: CGRect { - return imageScrollView.zoomedFrame - } - - override func viewDidLoad() { - super.viewDidLoad() - - closeButton.imageView?.contentMode = .scaleAspectFit - closeButton.accessibilityLabel = NSLocalizedString("Close", comment: "Close") - shareButton.accessibilityLabel = NSLocalizedString("Share", comment: "Share") - - imageScrollView.setup() - imageScrollView.imageScrollViewDelegate = self - imageScrollView.imageContentMode = .aspectFit - imageScrollView.initialOffset = .center - imageScrollView.display(image: image) - - titleLabel.text = imageTitle ?? "" - layoutTitleLabel() - - guard imageTitle != "" else { - titleBackground.removeFromSuperview() - return - } - titleBackground.layer.cornerRadius = 6 - } - - override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { - super.viewWillTransition(to: size, with: coordinator) - coordinator.animate(alongsideTransition: { [weak self] context in - self?.imageScrollView.resize() - }) - } - - @IBAction func share(_ sender: Any) { - guard let image = image else { return } - let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: nil) - activityViewController.popoverPresentationController?.sourceView = shareButton - activityViewController.popoverPresentationController?.sourceRect = shareButton.bounds - present(activityViewController, animated: true) - } - - @IBAction func done(_ sender: Any) { - dismiss(animated: true) - } - - private func layoutTitleLabel(){ - let width = view.frame.width - let multiplier = UIDevice.current.userInterfaceIdiom == .pad ? CGFloat(0.1) : CGFloat(0.04) - titleLeading.constant += width * multiplier - titleTrailing.constant -= width * multiplier - titleLabel.layoutIfNeeded() - } -} - -// MARK: ImageScrollViewDelegate - -extension ImageViewController: ImageScrollViewDelegate { - - func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView) { - dismiss(animated: true) - } - - func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView) { - dismiss(animated: true) - } - - -} - diff --git a/Multiplatform/iOS/Article/OpenInSafariActivity.swift b/Multiplatform/iOS/Article/OpenInSafariActivity.swift deleted file mode 100644 index 5f76c768d..000000000 --- a/Multiplatform/iOS/Article/OpenInSafariActivity.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// OpenInSafariActivity.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/13/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit - -class OpenInSafariActivity: UIActivity { - - private var activityItems: [Any]? - - override var activityTitle: String? { - return NSLocalizedString("Open in Safari", comment: "Open in Safari") - } - - override var activityImage: UIImage? { - return UIImage(systemName: "safari", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)) - } - - override var activityType: UIActivity.ActivityType? { - return UIActivity.ActivityType(rawValue: "com.rancharo.NetNewsWire-Evergreen.safari") - } - - override class var activityCategory: UIActivity.Category { - return .action - } - - override func canPerform(withActivityItems activityItems: [Any]) -> Bool { - return true - } - - override func prepare(withActivityItems activityItems: [Any]) { - self.activityItems = activityItems - } - - override func perform() { - guard let url = activityItems?.first(where: { $0 is URL }) as? URL else { - activityDidFinish(false) - return - } - - UIApplication.shared.open(url, options: [:], completionHandler: nil) - activityDidFinish(true) - } - -} diff --git a/Multiplatform/iOS/Article/TitleActivityItemSource.swift b/Multiplatform/iOS/Article/TitleActivityItemSource.swift deleted file mode 100644 index 6eaf1f520..000000000 --- a/Multiplatform/iOS/Article/TitleActivityItemSource.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// TitleActivityItemSource.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/13/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit - -class TitleActivityItemSource: NSObject, UIActivityItemSource { - - private let title: String? - - init(title: String?) { - self.title = title - } - - func activityViewControllerPlaceholderItem(_ activityViewController: UIActivityViewController) -> Any { - return title as Any - } - - func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { - guard let activityType = activityType, - let title = title else { - return NSNull() - } - - switch activityType.rawValue { - case "com.omnigroup.OmniFocus3.iOS.QuickEntry", - "com.culturedcode.ThingsiPhone.ShareExtension", - "com.tapbots.Tweetbot4.shareextension", - "com.tapbots.Tweetbot6.shareextension", - "com.buffer.buffer.Buffer": - return title - default: - return NSNull() - } - } - -} diff --git a/Multiplatform/iOS/Article/WebViewController.swift b/Multiplatform/iOS/Article/WebViewController.swift deleted file mode 100644 index 5e5364d54..000000000 --- a/Multiplatform/iOS/Article/WebViewController.swift +++ /dev/null @@ -1,765 +0,0 @@ -// -// WebViewController.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit -import WebKit -import RSCore -import Account -import Articles -import SafariServices -import MessageUI - -protocol WebViewControllerDelegate: AnyObject { - func webViewController(_: WebViewController, articleExtractorButtonStateDidUpdate: ArticleExtractorButtonState) -} - -class WebViewController: UIViewController { - - private struct MessageName { - static let imageWasClicked = "imageWasClicked" - static let imageWasShown = "imageWasShown" - static let showFeedInspector = "showFeedInspector" - } - - private var topShowBarsView: UIView! - private var bottomShowBarsView: UIView! - private var topShowBarsViewConstraint: NSLayoutConstraint! - private var bottomShowBarsViewConstraint: NSLayoutConstraint! - - private var webView: PreloadedWebView? { - guard view.subviews.count > 0 else { return nil } - return view.subviews[0] as? PreloadedWebView - } - -// private lazy var contextMenuInteraction = UIContextMenuInteraction(delegate: self) - private var isFullScreenAvailable: Bool { - return AppDefaults.shared.articleFullscreenAvailable && traitCollection.userInterfaceIdiom == .phone // && coordinator.isRootSplitCollapsed - } - private lazy var transition = ImageTransition(controller: self) - private var clickedImageCompletion: (() -> Void)? - - private var articleExtractor: ArticleExtractor? = nil - var extractedArticle: ExtractedArticle? { - didSet { - windowScrollY = 0 - } - } - var isShowingExtractedArticle = false - - var articleExtractorButtonState: ArticleExtractorButtonState = .off { - didSet { - delegate?.webViewController(self, articleExtractorButtonStateDidUpdate: articleExtractorButtonState) - } - } - - var sceneModel: SceneModel? - weak var delegate: WebViewControllerDelegate? - - private(set) var article: Article? - - let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 0.3) - var windowScrollY = 0 - - override func viewDidLoad() { - super.viewDidLoad() - - NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) - - // Configure the tap zones -// configureTopShowBarsView() -// configureBottomShowBarsView() - - loadWebView() - - } - - // MARK: Notifications - - @objc func webFeedIconDidBecomeAvailable(_ note: Notification) { - reloadArticleImage() - } - - @objc func avatarDidBecomeAvailable(_ note: Notification) { - reloadArticleImage() - } - - @objc func faviconDidBecomeAvailable(_ note: Notification) { - reloadArticleImage() - } - - // MARK: Actions - -// @objc func showBars(_ sender: Any) { -// showBars() -// } - - // MARK: API - - func setArticle(_ article: Article?, updateView: Bool = true) { - stopArticleExtractor() - - if article != self.article { - self.article = article - if updateView { - if article?.webFeed?.isArticleExtractorAlwaysOn ?? false { - startArticleExtractor() - } - windowScrollY = 0 - loadWebView() - } - } - - } - - func focus() { - webView?.becomeFirstResponder() - } - - func canScrollDown() -> Bool { - guard let webView = webView else { return false } - return webView.scrollView.contentOffset.y < finalScrollPosition() - } - - func scrollPageDown() { - guard let webView = webView else { return } - - let overlap = 2 * UIFont.systemFont(ofSize: UIFont.systemFontSize).lineHeight * UIScreen.main.scale - let scrollToY: CGFloat = { - let fullScroll = webView.scrollView.contentOffset.y + webView.scrollView.layoutMarginsGuide.layoutFrame.height - overlap - let final = finalScrollPosition() - return fullScroll < final ? fullScroll : final - }() - - let convertedPoint = self.view.convert(CGPoint(x: 0, y: 0), to: webView.scrollView) - let scrollToPoint = CGPoint(x: convertedPoint.x, y: scrollToY) - webView.scrollView.setContentOffset(scrollToPoint, animated: true) - } - - func hideClickedImage() { - webView?.evaluateJavaScript("hideClickedImage();") - } - - func showClickedImage(completion: @escaping () -> Void) { - clickedImageCompletion = completion - webView?.evaluateJavaScript("showClickedImage();") - } - - func fullReload() { - loadWebView(replaceExistingWebView: true) - } - -// func showBars() { -// AppDefaults.shared.articleFullscreenEnabled = false -// coordinator.showStatusBar() -// topShowBarsViewConstraint?.constant = 0 -// bottomShowBarsViewConstraint?.constant = 0 -// navigationController?.setNavigationBarHidden(false, animated: true) -// navigationController?.setToolbarHidden(false, animated: true) -// configureContextMenuInteraction() -// } -// -// func hideBars() { -// if isFullScreenAvailable { -// AppDefaults.shared.articleFullscreenEnabled = true -// coordinator.hideStatusBar() -// topShowBarsViewConstraint?.constant = -44.0 -// bottomShowBarsViewConstraint?.constant = 44.0 -// navigationController?.setNavigationBarHidden(true, animated: true) -// navigationController?.setToolbarHidden(true, animated: true) -// configureContextMenuInteraction() -// } -// } - - func toggleArticleExtractor() { - - guard let article = article else { - return - } - - guard articleExtractor?.state != .processing else { - stopArticleExtractor() - loadWebView() - return - } - - guard !isShowingExtractedArticle else { - isShowingExtractedArticle = false - loadWebView() - articleExtractorButtonState = .off - return - } - - if let articleExtractor = articleExtractor { - if article.preferredLink == articleExtractor.articleLink { - isShowingExtractedArticle = true - loadWebView() - articleExtractorButtonState = .on - } - } else { - startArticleExtractor() - } - - } - - func stopArticleExtractorIfProcessing() { - if articleExtractor?.state == .processing { - stopArticleExtractor() - } - } - - func stopWebViewActivity() { - if let webView = webView { - stopMediaPlayback(webView) - cancelImageLoad(webView) - } - } - -} - -// MARK: ArticleExtractorDelegate - -extension WebViewController: ArticleExtractorDelegate { - - func articleExtractionDidFail(with: Error) { - stopArticleExtractor() - articleExtractorButtonState = .error - loadWebView() - } - - func articleExtractionDidComplete(extractedArticle: ExtractedArticle) { - if articleExtractor?.state != .cancelled { - self.extractedArticle = extractedArticle - isShowingExtractedArticle = true - loadWebView() - articleExtractorButtonState = .on - } - } - -} - -// MARK: UIContextMenuInteractionDelegate - -//extension WebViewController: UIContextMenuInteractionDelegate { -// func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { -// -// return UIContextMenuConfiguration(identifier: nil, previewProvider: contextMenuPreviewProvider) { [weak self] suggestedActions in -// guard let self = self else { return nil } -// var actions = [UIAction]() -// -// if let action = self.prevArticleAction() { -// actions.append(action) -// } -// if let action = self.nextArticleAction() { -// actions.append(action) -// } -// if let action = self.toggleReadAction() { -// actions.append(action) -// } -// actions.append(self.toggleStarredAction()) -// if let action = self.nextUnreadArticleAction() { -// actions.append(action) -// } -// actions.append(self.toggleArticleExtractorAction()) -// actions.append(self.shareAction()) -// -// return UIMenu(title: "", children: actions) -// } -// } -// -// func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { -// coordinator.showBrowserForCurrentArticle() -// } -// -//} - -// MARK: WKNavigationDelegate - -extension WebViewController: WKNavigationDelegate { - - func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { - for (index, view) in view.subviews.enumerated() { - if index != 0, let oldWebView = view as? PreloadedWebView { - oldWebView.removeFromSuperview() - } - } - } - - func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - - if navigationAction.navigationType == .linkActivated { - guard let url = navigationAction.request.url else { - decisionHandler(.allow) - return - } - - let components = URLComponents(url: url, resolvingAgainstBaseURL: false) - if components?.scheme == "http" || components?.scheme == "https" { - decisionHandler(.cancel) - - // If the resource cannot be opened with an installed app, present the web view. - UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { didOpen in - assert(Thread.isMainThread) - guard didOpen == false else { - return - } - let vc = SFSafariViewController(url: url) - self.present(vc, animated: true) - } - } else if components?.scheme == "mailto" { - decisionHandler(.cancel) - - guard let emailAddress = url.percentEncodedEmailAddress else { - return - } - - if UIApplication.shared.canOpenURL(emailAddress) { - UIApplication.shared.open(emailAddress, options: [.universalLinksOnly : false], completionHandler: nil) - } else { - let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"), message: NSLocalizedString("This device cannot send emails.", comment: "This device cannot send emails."), preferredStyle: .alert) - alert.addAction(.init(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil)) - self.present(alert, animated: true, completion: nil) - } - } else if components?.scheme == "tel" { - decisionHandler(.cancel) - - if UIApplication.shared.canOpenURL(url) { - UIApplication.shared.open(url, options: [.universalLinksOnly : false], completionHandler: nil) - } - - } else { - decisionHandler(.allow) - } - } else { - decisionHandler(.allow) - } - } - - func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { - fullReload() - } - -} - -// MARK: WKUIDelegate - -extension WebViewController: WKUIDelegate { - func webView(_ webView: WKWebView, contextMenuForElement elementInfo: WKContextMenuElementInfo, willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) { - // We need to have at least an unimplemented WKUIDelegate assigned to the WKWebView. This makes the - // link preview launch Safari when the link preview is tapped. In theory, you shoud be able to get - // the link from the elementInfo above and transition to SFSafariViewController instead of launching - // Safari. As the time of this writing, the link in elementInfo is always nil. ¯\_(ツ)_/¯ - } -} - -// MARK: WKScriptMessageHandler - -extension WebViewController: WKScriptMessageHandler { - - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - switch message.name { - case MessageName.imageWasShown: - clickedImageCompletion?() - case MessageName.imageWasClicked: - imageWasClicked(body: message.body as? String) - case MessageName.showFeedInspector: - return -// if let webFeed = article?.webFeed { -// coordinator.showFeedInspector(for: webFeed) -// } - default: - return - } - } - -} - -// MARK: UIViewControllerTransitioningDelegate - -extension WebViewController: UIViewControllerTransitioningDelegate { - - func animationController(forPresented presented: UIViewController, presenting: UIViewController, source: UIViewController) -> UIViewControllerAnimatedTransitioning? { - transition.presenting = true - return transition - } - - func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { - transition.presenting = false - return transition - } -} - -// MARK: - -extension WebViewController: UIScrollViewDelegate { - - func scrollViewDidScroll(_ scrollView: UIScrollView) { - scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) - } - - @objc func scrollPositionDidChange() { - webView?.evaluateJavaScript("window.scrollY") { (scrollY, error) in - guard error == nil else { return } - let javascriptScrollY = scrollY as? Int ?? 0 - // I don't know why this value gets returned sometimes, but it is in error - guard javascriptScrollY != 33554432 else { return } - self.windowScrollY = javascriptScrollY - } - } - -} - -// MARK: JSON - -private struct ImageClickMessage: Codable { - let x: Float - let y: Float - let width: Float - let height: Float - let imageTitle: String? - let imageURL: String -} - -// MARK: Private - -private extension WebViewController { - - func loadWebView(replaceExistingWebView: Bool = false) { - guard isViewLoaded else { return } - - if !replaceExistingWebView, let webView = webView { - self.renderPage(webView) - return - } - - sceneModel?.webViewProvider?.dequeueWebView() { webView in - - webView.ready { - - // Add the webview - webView.translatesAutoresizingMaskIntoConstraints = false - self.view.insertSubview(webView, at: 0) - NSLayoutConstraint.activate([ - self.view.leadingAnchor.constraint(equalTo: webView.leadingAnchor), - self.view.trailingAnchor.constraint(equalTo: webView.trailingAnchor), - self.view.topAnchor.constraint(equalTo: webView.topAnchor), - self.view.bottomAnchor.constraint(equalTo: webView.bottomAnchor) - ]) - - // UISplitViewController reports the wrong size to WKWebView which can cause horizontal - // rubberbanding on the iPad. This interferes with our UIPageViewController preventing - // us from easily swiping between WKWebViews. This hack fixes that. - webView.scrollView.contentInset = UIEdgeInsets(top: 0, left: -1, bottom: 0, right: 0) - - webView.scrollView.setZoomScale(1.0, animated: false) - - self.view.setNeedsLayout() - self.view.layoutIfNeeded() - - // Configure the webview - webView.navigationDelegate = self - webView.uiDelegate = self - webView.scrollView.delegate = self - // self.configureContextMenuInteraction() - - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked) - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown) - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.showFeedInspector) - - self.renderPage(webView) - - } - - } - - } - - func renderPage(_ webView: PreloadedWebView?) { - guard let webView = webView else { return } - - let style = ArticleStylesManager.shared.currentStyle - let rendering: ArticleRenderer.Rendering - - if let articleExtractor = articleExtractor, articleExtractor.state == .processing { - rendering = ArticleRenderer.loadingHTML(style: style) - } else if let articleExtractor = articleExtractor, articleExtractor.state == .failedToParse, let article = article { - rendering = ArticleRenderer.articleHTML(article: article, style: style) - } else if let article = article, let extractedArticle = extractedArticle { - if isShowingExtractedArticle { - rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style) - } else { - rendering = ArticleRenderer.articleHTML(article: article, style: style) - } - } else if let article = article { - rendering = ArticleRenderer.articleHTML(article: article, style: style) - } else { - rendering = ArticleRenderer.noSelectionHTML(style: style) - } - - let substitutions = [ - "title": rendering.title, - "baseURL": rendering.baseURL, - "style": rendering.style, - "body": rendering.html, - "windowScrollY": String(windowScrollY) - ] - - let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions) - webView.loadHTMLString(html, baseURL: ArticleRenderer.page.baseURL) - - } - - func finalScrollPosition() -> CGFloat { - guard let webView = webView else { return 0 } - return webView.scrollView.contentSize.height - webView.scrollView.bounds.height + webView.scrollView.safeAreaInsets.bottom - } - - func startArticleExtractor() { - if let link = article?.preferredLink, let extractor = ArticleExtractor(link) { - extractor.delegate = self - extractor.process() - articleExtractor = extractor - articleExtractorButtonState = .animated - } - } - - func stopArticleExtractor() { - articleExtractor?.cancel() - articleExtractor = nil - isShowingExtractedArticle = false - articleExtractorButtonState = .off - } - - func reloadArticleImage() { - guard let article = article else { return } - - var components = URLComponents() - components.scheme = ArticleRenderer.imageIconScheme - components.path = article.articleID - - if let imageSrc = components.string { - webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")") - } - } - - func imageWasClicked(body: String?) { - guard let webView = webView, - let body = body, - let data = body.data(using: .utf8), - let clickMessage = try? JSONDecoder().decode(ImageClickMessage.self, from: data), - let range = clickMessage.imageURL.range(of: ";base64,") - else { return } - - let base64Image = String(clickMessage.imageURL.suffix(from: range.upperBound)) - if let imageData = Data(base64Encoded: base64Image), let image = UIImage(data: imageData) { - - let y = CGFloat(clickMessage.y) + webView.safeAreaInsets.top - let rect = CGRect(x: CGFloat(clickMessage.x), y: y, width: CGFloat(clickMessage.width), height: CGFloat(clickMessage.height)) - transition.originFrame = webView.convert(rect, to: nil) - - if navigationController?.navigationBar.isHidden ?? false { - transition.maskFrame = webView.convert(webView.frame, to: nil) - } else { - transition.maskFrame = webView.convert(webView.safeAreaLayoutGuide.layoutFrame, to: nil) - } - - transition.originImage = image - -// coordinator.showFullScreenImage(image: image, imageTitle: clickMessage.imageTitle, transitioningDelegate: self) - } - } - - func stopMediaPlayback(_ webView: WKWebView) { - webView.evaluateJavaScript("stopMediaPlayback();") - } - - func cancelImageLoad(_ webView: WKWebView) { - webView.evaluateJavaScript("cancelImageLoad();") - } - -// func configureTopShowBarsView() { -// topShowBarsView = UIView() -// topShowBarsView.backgroundColor = .clear -// topShowBarsView.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(topShowBarsView) -// -// if AppDefaults.shared.articleFullscreenEnabled { -// topShowBarsViewConstraint = view.topAnchor.constraint(equalTo: topShowBarsView.bottomAnchor, constant: -44.0) -// } else { -// topShowBarsViewConstraint = view.topAnchor.constraint(equalTo: topShowBarsView.bottomAnchor, constant: 0.0) -// } -// -// NSLayoutConstraint.activate([ -// topShowBarsViewConstraint, -// view.leadingAnchor.constraint(equalTo: topShowBarsView.leadingAnchor), -// view.trailingAnchor.constraint(equalTo: topShowBarsView.trailingAnchor), -// topShowBarsView.heightAnchor.constraint(equalToConstant: 44.0) -// ]) -// topShowBarsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBars(_:)))) -// } -// -// func configureBottomShowBarsView() { -// bottomShowBarsView = UIView() -// topShowBarsView.backgroundColor = .clear -// bottomShowBarsView.translatesAutoresizingMaskIntoConstraints = false -// view.addSubview(bottomShowBarsView) -// if AppDefaults.shared.articleFullscreenEnabled { -// bottomShowBarsViewConstraint = view.bottomAnchor.constraint(equalTo: bottomShowBarsView.topAnchor, constant: 44.0) -// } else { -// bottomShowBarsViewConstraint = view.bottomAnchor.constraint(equalTo: bottomShowBarsView.topAnchor, constant: 0.0) -// } -// NSLayoutConstraint.activate([ -// bottomShowBarsViewConstraint, -// view.leadingAnchor.constraint(equalTo: bottomShowBarsView.leadingAnchor), -// view.trailingAnchor.constraint(equalTo: bottomShowBarsView.trailingAnchor), -// bottomShowBarsView.heightAnchor.constraint(equalToConstant: 44.0) -// ]) -// bottomShowBarsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBars(_:)))) -// } - -// func configureContextMenuInteraction() { -// if isFullScreenAvailable { -// if navigationController?.isNavigationBarHidden ?? false { -// webView?.addInteraction(contextMenuInteraction) -// } else { -// webView?.removeInteraction(contextMenuInteraction) -// } -// } -// } -// -// func contextMenuPreviewProvider() -> UIViewController { -// let previewProvider = UIStoryboard.main.instantiateController(ofType: ContextMenuPreviewViewController.self) -// previewProvider.article = article -// return previewProvider -// } -// -// func prevArticleAction() -> UIAction? { -// guard coordinator.isPrevArticleAvailable else { return nil } -// let title = NSLocalizedString("Previous Article", comment: "Previous Article") -// return UIAction(title: title, image: AppAssets.prevArticleImage) { [weak self] action in -// self?.coordinator.selectPrevArticle() -// } -// } -// -// func nextArticleAction() -> UIAction? { -// guard coordinator.isNextArticleAvailable else { return nil } -// let title = NSLocalizedString("Next Article", comment: "Next Article") -// return UIAction(title: title, image: AppAssets.nextArticleImage) { [weak self] action in -// self?.coordinator.selectNextArticle() -// } -// } -// -// func toggleReadAction() -> UIAction? { -// guard let article = article, !article.status.read || article.isAvailableToMarkUnread else { return nil } -// -// let title = article.status.read ? NSLocalizedString("Mark as Unread", comment: "Mark as Unread") : NSLocalizedString("Mark as Read", comment: "Mark as Read") -// let readImage = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage -// return UIAction(title: title, image: readImage) { [weak self] action in -// self?.coordinator.toggleReadForCurrentArticle() -// } -// } -// -// func toggleStarredAction() -> UIAction { -// let starred = article?.status.starred ?? false -// let title = starred ? NSLocalizedString("Mark as Unstarred", comment: "Mark as Unstarred") : NSLocalizedString("Mark as Starred", comment: "Mark as Starred") -// let starredImage = starred ? AppAssets.starOpenImage : AppAssets.starClosedImage -// return UIAction(title: title, image: starredImage) { [weak self] action in -// self?.coordinator.toggleStarredForCurrentArticle() -// } -// } -// -// func nextUnreadArticleAction() -> UIAction? { -// guard coordinator.isAnyUnreadAvailable else { return nil } -// let title = NSLocalizedString("Next Unread Article", comment: "Next Unread Article") -// return UIAction(title: title, image: AppAssets.nextUnreadArticleImage) { [weak self] action in -// self?.coordinator.selectNextUnread() -// } -// } -// -// func toggleArticleExtractorAction() -> UIAction { -// let extracted = articleExtractorButtonState == .on -// let title = extracted ? NSLocalizedString("Show Feed Article", comment: "Show Feed Article") : NSLocalizedString("Show Reader View", comment: "Show Reader View") -// let extractorImage = extracted ? AppAssets.articleExtractorOffSF : AppAssets.articleExtractorOnSF -// return UIAction(title: title, image: extractorImage) { [weak self] action in -// self?.toggleArticleExtractor() -// } -// } -// -// func shareAction() -> UIAction { -// let title = NSLocalizedString("Share", comment: "Share") -// return UIAction(title: title, image: AppAssets.shareImage) { [weak self] action in -// self?.showActivityDialog() -// } -// } - -} - -// MARK: Find in Article - -private struct FindInArticleOptions: Codable { - var text: String - var caseSensitive = false - var regex = false -} - -internal struct FindInArticleState: Codable { - struct WebViewClientRect: Codable { - let x: Double - let y: Double - let width: Double - let height: Double - } - - struct FindInArticleResult: Codable { - let rects: [WebViewClientRect] - let bounds: WebViewClientRect - let index: UInt - let matchGroups: [String] - } - - let index: UInt? - let results: [FindInArticleResult] - let count: UInt -} - -extension WebViewController { - - func searchText(_ searchText: String, completionHandler: @escaping (FindInArticleState) -> Void) { - guard let json = try? JSONEncoder().encode(FindInArticleOptions(text: searchText)) else { - return - } - let encoded = json.base64EncodedString() - - webView?.evaluateJavaScript("updateFind(\"\(encoded)\")") { - (result, error) in - guard error == nil, - let b64 = result as? String, - let rawData = Data(base64Encoded: b64), - let findState = try? JSONDecoder().decode(FindInArticleState.self, from: rawData) else { - return - } - - completionHandler(findState) - } - } - - func endSearch() { - webView?.evaluateJavaScript("endFind()") - } - - func selectNextSearchResult() { - webView?.evaluateJavaScript("selectNextResult()") - } - - func selectPreviousSearchResult() { - webView?.evaluateJavaScript("selectPreviousResult()") - } - -} - diff --git a/Multiplatform/iOS/AttributedStringView.swift b/Multiplatform/iOS/AttributedStringView.swift deleted file mode 100644 index 9aa730fb6..000000000 --- a/Multiplatform/iOS/AttributedStringView.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// AttributedStringView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 9/16/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct AttributedStringView: UIViewRepresentable { - - let string: NSAttributedString - let preferredMaxLayoutWidth: CGFloat - - func makeUIView(context: Context) -> HackedTextView { - return HackedTextView() - } - - func updateUIView(_ view: HackedTextView, context: Context) { - view.attributedText = string - - view.preferredMaxLayoutWidth = preferredMaxLayoutWidth - view.isScrollEnabled = false - view.textContainer.lineBreakMode = .byWordWrapping - - view.isUserInteractionEnabled = true - view.adjustsFontForContentSizeCategory = true - view.font = .preferredFont(forTextStyle: .body) - view.textColor = UIColor.label - view.tintColor = AppAssets.accentColor - view.backgroundColor = UIColor.secondarySystemGroupedBackground - - view.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - view.setContentCompressionResistancePriority(.required, for: .vertical) - } - -} - -class HackedTextView: UITextView { - var preferredMaxLayoutWidth = CGFloat.zero - override var intrinsicContentSize: CGSize { - return sizeThatFits(CGSize(width: preferredMaxLayoutWidth, height: .infinity)) - } -} diff --git a/Multiplatform/iOS/Info.plist b/Multiplatform/iOS/Info.plist deleted file mode 100644 index 5be8136b3..000000000 --- a/Multiplatform/iOS/Info.plist +++ /dev/null @@ -1,89 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - LSRequiresIPhoneOS - - UIApplicationSceneManifest - - UIApplicationSupportsMultipleScenes - - - UIApplicationSupportsIndirectInputEvents - - UILaunchScreen - - UIRequiredDeviceCapabilities - - armv7 - - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - BGTaskSchedulerPermittedIdentifiers - - com.ranchero.NetNewsWire.FeedRefresh - - NSPhotoLibraryAddUsageDescription - Grant permission to save images from the article. - NSUserActivityTypes - - AddWebFeedIntent - NextUnread - ReadArticle - Restoration - SelectFeed - - UIBackgroundModes - - fetch - processing - remote-notification - - UserAgent - NetNewsWire (RSS Reader; https://netnewswire.com/) - OrganizationIdentifier - $(ORGANIZATION_IDENTIFIER) - DeveloperEntitlements - $(DEVELOPER_ENTITLEMENTS) - AppGroup - group.$(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS - AppIdentifierPrefix - $(AppIdentifierPrefix) - LSApplicationQueriesSchemes - - mailto - - - diff --git a/Multiplatform/iOS/SafariView.swift b/Multiplatform/iOS/SafariView.swift deleted file mode 100644 index 62f1bc36c..000000000 --- a/Multiplatform/iOS/SafariView.swift +++ /dev/null @@ -1,63 +0,0 @@ -// -// SafariView.swift -// Multiplatform iOS -// -// Created by Stuart Breckenridge on 30/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import SafariServices - - -private final class Safari: UIViewControllerRepresentable { - - var urlToLoad: URL - - init(url: URL) { - self.urlToLoad = url - } - - func makeUIViewController(context: Context) -> SFSafariViewController { - let viewController = SFSafariViewController(url: urlToLoad) - viewController.delegate = context.coordinator - return viewController - } - - func updateUIViewController(_ uiViewController: SFSafariViewController, context: Context) { - - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, SFSafariViewControllerDelegate { - var parent: Safari - - init(_ parent: Safari) { - self.parent = parent - } - - func safariViewControllerDidFinish(_ controller: SFSafariViewController) { - - } - - } - -} - -struct SafariView: View { - - var url: URL - - var body: some View { - Safari(url: url) - } -} - -struct SafariView_Previews: PreviewProvider { - static var previews: some View { - SafariView(url: URL(string: "https://netnewswire.com/")!) - } -} diff --git a/Multiplatform/iOS/Settings/About/About.rtf b/Multiplatform/iOS/Settings/About/About.rtf deleted file mode 100644 index b8aa6f4e5..000000000 --- a/Multiplatform/iOS/Settings/About/About.rtf +++ /dev/null @@ -1,12 +0,0 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2513 -\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande-Bold;} -{\colortbl;\red255\green255\blue255;\red0\green0\blue0;\red10\green96\blue255;} -{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;\cssrgb\c0\c47843\c100000\cname systemBlueColor;} -\margl1440\margr1440\vieww8340\viewh9300\viewkind0 -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\li363\fi-364\pardirnatural\partightenfactor0 - -\f0\b\fs28 \cf2 By Brent Simmons and the Ranchero Software team -\fs22 \ -\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\pardirnatural\partightenfactor0 -{\field{\*\fldinst{HYPERLINK "https://ranchero.com/netnewswire/"}}{\fldrslt -\fs28 \cf3 netnewswire.com}}} \ No newline at end of file diff --git a/Multiplatform/iOS/Settings/About/Credits.rtf b/Multiplatform/iOS/Settings/About/Credits.rtf deleted file mode 100644 index a4c868701..000000000 --- a/Multiplatform/iOS/Settings/About/Credits.rtf +++ /dev/null @@ -1,20 +0,0 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2511 -\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;} -{\colortbl;\red255\green255\blue255;\red0\green0\blue0;} -{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;} -\margl1440\margr1440\vieww14220\viewh13280\viewkind0 -\deftab720 -\pard\pardeftab720\li360\fi-360\sa60\partightenfactor0 - -\f0\fs22 \cf2 iOS app design: {\field{\*\fldinst{HYPERLINK "https://inessential.com/"}}{\fldrslt Brent Simmons}} and {\field{\*\fldinst{HYPERLINK "https://github.com/vincode-io"}}{\fldrslt Maurice Parker}}\ -Lead iOS developer: {\field{\*\fldinst{HYPERLINK "https://github.com/vincode-io"}}{\fldrslt Maurice Parker}}\ -App icon: {\field{\*\fldinst{HYPERLINK "https://twitter.com/BradEllis"}}{\fldrslt Brad Ellis}}\ -\pard\pardeftab720\li366\fi-367\sa60\partightenfactor0 -\cf2 Feedly syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/kielgillard"}}{\fldrslt Kiel Gillard}}\ -Under-the-hood magic and CSS stylin\'92s: {\field{\*\fldinst{HYPERLINK "https://github.com/wevah"}}{\fldrslt Nate Weaver}}\ -\pard\pardeftab720\li362\fi-363\sa60\partightenfactor0 -\cf2 Newsfoot (JS footnote displayer): {\field{\*\fldinst{HYPERLINK "https://github.com/brehaut/"}}{\fldrslt Andrew Brehaut}}\ -\pard\pardeftab720\li355\fi-356\sa60\partightenfactor0 -\cf2 Help book: {\field{\*\fldinst{HYPERLINK "https://nostodnayr.net/"}}{\fldrslt Ryan Dotson}}\ -\pard\pardeftab720\li358\fi-359\sa60\partightenfactor0 -\cf2 And more {\field{\*\fldinst{HYPERLINK "https://github.com/brentsimmons/NetNewsWire/graphs/contributors"}}{\fldrslt contributors}}!} \ No newline at end of file diff --git a/Multiplatform/iOS/Settings/About/Dedication.rtf b/Multiplatform/iOS/Settings/About/Dedication.rtf deleted file mode 100644 index 974f1b818..000000000 --- a/Multiplatform/iOS/Settings/About/Dedication.rtf +++ /dev/null @@ -1,9 +0,0 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2513 -\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;} -{\colortbl;\red255\green255\blue255;\red0\green0\blue0;} -{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;} -\margl1440\margr1440\vieww9000\viewh8400\viewkind0 -\deftab720 -\pard\pardeftab720\sa60\partightenfactor0 - -\f0\fs22 \cf2 NetNewsWire 6 is dedicated to everyone working to save democracy around the world.} \ No newline at end of file diff --git a/Multiplatform/iOS/Settings/About/SettingsAboutModel.swift b/Multiplatform/iOS/Settings/About/SettingsAboutModel.swift deleted file mode 100644 index 4ef2565f0..000000000 --- a/Multiplatform/iOS/Settings/About/SettingsAboutModel.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// SettingsAboutModel.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -class SettingsAboutModel: ObservableObject { - - var about: NSAttributedString - var credits: NSAttributedString - var thanks: NSAttributedString - var dedication: NSAttributedString - - init() { - about = SettingsAboutModel.loadResource("About") - credits = SettingsAboutModel.loadResource("Credits") - thanks = SettingsAboutModel.loadResource("Thanks") - dedication = SettingsAboutModel.loadResource("Dedication") - } - - private static func loadResource(_ resource: String) -> NSAttributedString { - let url = Bundle.main.url(forResource: resource, withExtension: "rtf")! - return try! NSAttributedString(url: url, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil) - - } - -} diff --git a/Multiplatform/iOS/Settings/About/SettingsAboutView.swift b/Multiplatform/iOS/Settings/About/SettingsAboutView.swift deleted file mode 100644 index e4a02adb4..000000000 --- a/Multiplatform/iOS/Settings/About/SettingsAboutView.swift +++ /dev/null @@ -1,41 +0,0 @@ -// -// SettingsAboutView.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 9/16/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Combine - -struct SettingsAboutView: View { - - @StateObject var viewModel = SettingsAboutModel() - - var body: some View { - GeometryReader { geometry in - List { - Text("NetNewsWire").font(.largeTitle) - AttributedStringView(string: self.viewModel.about, preferredMaxLayoutWidth: geometry.size.width - 20) - Section(header: Text("CREDITS")) { - AttributedStringView(string: self.viewModel.credits, preferredMaxLayoutWidth: geometry.size.width - 20) - } - Section(header: Text("THANKS")) { - AttributedStringView(string: self.viewModel.thanks, preferredMaxLayoutWidth: geometry.size.width - 20) - } - Section(header: Text("DEDICATION"), footer: Text("Copyright © 2002-2021 Brent Simmons").font(.footnote)) { - AttributedStringView(string: self.viewModel.dedication, preferredMaxLayoutWidth: geometry.size.width - 20) - } - }.listStyle(InsetGroupedListStyle()) - } - .navigationTitle(Text("About")) - } - -} - -struct SettingsAboutView_Previews: PreviewProvider { - static var previews: some View { - SettingsAboutView() - } -} diff --git a/Multiplatform/iOS/Settings/About/Thanks.rtf b/Multiplatform/iOS/Settings/About/Thanks.rtf deleted file mode 100644 index c3cde3b4c..000000000 --- a/Multiplatform/iOS/Settings/About/Thanks.rtf +++ /dev/null @@ -1,11 +0,0 @@ -{\rtf1\ansi\ansicpg1252\cocoartf2511 -\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\fnil\fcharset0 LucidaGrande;} -{\colortbl;\red255\green255\blue255;\red0\green0\blue0;} -{\*\expandedcolortbl;;\cssrgb\c0\c0\c0\cname textColor;} -\margl1440\margr1440\vieww11780\viewh11640\viewkind0 -\deftab720 -\pard\pardeftab720\li365\fi-366\sa60\partightenfactor0 - -\f0\fs22 \cf2 Thanks to Sheila and my family; thanks to my friends in Seattle and around the globe; thanks to the ever-patient and ever-awesome NetNewsWire beta testers. \ -\pard\tx0\pardeftab720\li360\fi-361\sa60\partightenfactor0 -\cf2 Thanks to {\field{\*\fldinst{HYPERLINK "https://shapeof.com/"}}{\fldrslt Gus Mueller}} for {\field{\*\fldinst{HYPERLINK "https://github.com/ccgus/fmdb"}}{\fldrslt FMDB}} by {\field{\*\fldinst{HYPERLINK "http://flyingmeat.com/"}}{\fldrslt Flying Meat Software}}. Thanks to {\field{\*\fldinst{HYPERLINK "https://github.com"}}{\fldrslt GitHub}} and {\field{\*\fldinst{HYPERLINK "https://slack.com"}}{\fldrslt Slack}} for making open source collaboration easy and fun. Thanks to {\field{\*\fldinst{HYPERLINK "https://benubois.com/"}}{\fldrslt Ben Ubois}} at {\field{\*\fldinst{HYPERLINK "https://feedbin.com/"}}{\fldrslt Feedbin}} for all the extra help with syncing and article rendering \'97\'a0and for {\field{\*\fldinst{HYPERLINK "https://feedbin.com/blog/2019/03/11/the-future-of-full-content/"}}{\fldrslt hosting the server for the Reader view}}.} \ No newline at end of file diff --git a/Multiplatform/iOS/Settings/Accounts/AccountCredentialsError.swift b/Multiplatform/iOS/Settings/Accounts/AccountCredentialsError.swift deleted file mode 100644 index 87638aa23..000000000 --- a/Multiplatform/iOS/Settings/Accounts/AccountCredentialsError.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// AccountCredentialsError.swift -// Multiplatform iOS -// -// Created by Rizwan on 21/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation - -enum AccountCredentialsError: CustomStringConvertible, Equatable { - case none, keyChain, invalidCredentials, noNetwork, other(error: Error) - - var description: String { - switch self { - case .keyChain: - return NSLocalizedString("Keychain error while storing credentials.", comment: "") - case .invalidCredentials: - return NSLocalizedString("Invalid email/password combination.", comment: "") - case .noNetwork: - return NSLocalizedString("Network error. Try again later.", comment: "") - case .other(let error): - return NSLocalizedString(error.localizedDescription, comment: "Other add account error") - default: - return "" - } - } - - static func ==(lhs: AccountCredentialsError, rhs: AccountCredentialsError) -> Bool { - switch (lhs, rhs) { - case (.other(let lhsError), .other(let rhsError)): - return lhsError.localizedDescription == rhsError.localizedDescription - default: - return false - } - } -} diff --git a/Multiplatform/iOS/Settings/Accounts/AccountHeaderImageView.swift b/Multiplatform/iOS/Settings/Accounts/AccountHeaderImageView.swift deleted file mode 100644 index 028df300f..000000000 --- a/Multiplatform/iOS/Settings/Accounts/AccountHeaderImageView.swift +++ /dev/null @@ -1,37 +0,0 @@ -// -// AccountHeaderImageView.swift -// Multiplatform iOS -// -// Created by Rizwan on 08/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import RSCore - -struct AccountHeaderImageView: View { - var image: RSImage - - var body: some View { - HStack(alignment: .center) { - Spacer() - Image(rsImage: image) - .resizable() - .scaledToFit() - .frame(height: 48, alignment: .center) - .foregroundColor(Color.primary) - Spacer() - } - .padding(16) - } -} - -struct AccountHeaderImageView_Previews: PreviewProvider { - static var previews: some View { - Group { - AccountHeaderImageView(image: AppAssets.image(for: .onMyMac)!) - AccountHeaderImageView(image: AppAssets.image(for: .feedbin)!) - AccountHeaderImageView(image: AppAssets.accountLocalPadImage) - } - } -} diff --git a/Multiplatform/iOS/Settings/Accounts/SettingsAccountLabelView.swift b/Multiplatform/iOS/Settings/Accounts/SettingsAccountLabelView.swift deleted file mode 100644 index 29e3faa16..000000000 --- a/Multiplatform/iOS/Settings/Accounts/SettingsAccountLabelView.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// SettingsAccountLabelView.swift -// Multiplatform iOS -// -// Created by Rizwan on 07/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import RSCore - -struct SettingsAccountLabelView: View { - let accountImage: RSImage? - let accountLabel: String - - var body: some View { - HStack { - Image(rsImage: accountImage!) - .resizable() - .scaledToFit() - .frame(width: 32, height: 32) - Text(verbatim: accountLabel).font(.title) - } - .foregroundColor(.primary).padding(4.0) - } -} - -struct SettingsAccountLabelView_Previews: PreviewProvider { - static var previews: some View { - List { - SettingsAccountLabelView( - accountImage: AppAssets.image(for: .onMyMac), - accountLabel: "On My Device" - ) - SettingsAccountLabelView( - accountImage: AppAssets.image(for: .feedbin), - accountLabel: "Feedbin" - ) - SettingsAccountLabelView( - accountImage: AppAssets.accountLocalPadImage, - accountLabel: "On My iPad" - ) - SettingsAccountLabelView( - accountImage: AppAssets.accountLocalPhoneImage, - accountLabel: "On My iPhone" - ) - } - } -} diff --git a/Multiplatform/iOS/Settings/Accounts/SettingsAddAccountModel.swift b/Multiplatform/iOS/Settings/Accounts/SettingsAddAccountModel.swift deleted file mode 100644 index 5977bd775..000000000 --- a/Multiplatform/iOS/Settings/Accounts/SettingsAddAccountModel.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// SettingsAddAccountModel.swift -// Multiplatform iOS -// -// Created by Rizwan on 09/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore - -class SettingsAddAccountModel: ObservableObject { - - struct SettingsAddAccount: Identifiable { - var id: Int { accountType.rawValue } - - let name: String - let accountType: AccountType - - var image: RSImage { - AppAssets.image(for: accountType)! - } - } - - @Published var accounts: [SettingsAddAccount] = [] - @Published var isAddPresented = false - @Published var selectedAccountType: AccountType? = nil { - didSet { - selectedAccountType != nil ? (isAddPresented = true) : (isAddPresented = false) - } - } - - init() { - self.accounts = [ - SettingsAddAccount(name: Account.defaultLocalAccountName, accountType: .onMyMac), - SettingsAddAccount(name: "Feedbin", accountType: .feedbin), - SettingsAddAccount(name: "Feedly", accountType: .feedly), - SettingsAddAccount(name: "Feed Wrangler", accountType: .feedWrangler), - SettingsAddAccount(name: "iCloud", accountType: .cloudKit), - SettingsAddAccount(name: "NewsBlur", accountType: .newsBlur), - SettingsAddAccount(name: "Fresh RSS", accountType: .freshRSS) - ] - } - -} diff --git a/Multiplatform/iOS/Settings/Accounts/SettingsAddAccountView.swift b/Multiplatform/iOS/Settings/Accounts/SettingsAddAccountView.swift deleted file mode 100644 index c8855409e..000000000 --- a/Multiplatform/iOS/Settings/Accounts/SettingsAddAccountView.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// SettingsAddAccountView.swift -// Multiplatform iOS -// -// Created by Rizwan on 07/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SettingsAddAccountView: View { - @StateObject private var model = SettingsAddAccountModel() - - var body: some View { - List { - ForEach(model.accounts) { account in - Button(action: { - model.selectedAccountType = account.accountType - }) { - SettingsAccountLabelView( - accountImage: account.image, - accountLabel: account.name - ) - } - } - } - .listStyle(InsetGroupedListStyle()) - .sheet(isPresented: $model.isAddPresented) { - switch model.selectedAccountType! { - case .onMyMac: - AddLocalAccountView() - case .feedbin: - AddFeedbinAccountView() - case .cloudKit: - AddCloudKitAccountView() - case .feedWrangler: - AddFeedWranglerAccountView() - case .newsBlur: - AddNewsBlurAccountView() - case .feedly: - AddFeedlyAccountView() - default: - AddReaderAPIAccountView(accountType: model.selectedAccountType!) - } - } - .navigationBarTitle(Text("Add Account"), displayMode: .inline) - } -} - -struct SettingsAddAccountView_Previews: PreviewProvider { - static var previews: some View { - SettingsAddAccountView() - } -} diff --git a/Multiplatform/iOS/Settings/Accounts/SettingsCloudKitAccountView.swift b/Multiplatform/iOS/Settings/Accounts/SettingsCloudKitAccountView.swift deleted file mode 100644 index 814a9e0c7..000000000 --- a/Multiplatform/iOS/Settings/Accounts/SettingsCloudKitAccountView.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// SettingsCloudKitAccountView.swift -// Multiplatform iOS -// -// Created by Rizwan on 13/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SettingsCloudKitAccountView: View { - @Environment(\.presentationMode) var presentationMode - - var body: some View { - NavigationView { - List { - Section(header: AccountHeaderImageView(image: AppAssets.image(for: .cloudKit)!)) { } - Section { - HStack { - Spacer() - Button(action: { self.addAccount() }) { - Text("Add Account") - } - Spacer() - } - } - } - .listStyle(InsetGroupedListStyle()) - .navigationBarTitle(Text(verbatim: "iCloud"), displayMode: .inline) - .navigationBarItems(leading: Button(action: { self.dismiss() }) { Text("Cancel") } ) - } - } - - private func addAccount() { - _ = AccountManager.shared.createAccount(type: .cloudKit) - dismiss() - } - - private func dismiss() { - presentationMode.wrappedValue.dismiss() - } -} - -struct SettingsCloudKitAccountView_Previews: PreviewProvider { - static var previews: some View { - SettingsCloudKitAccountView() - } -} diff --git a/Multiplatform/iOS/Settings/Accounts/SettingsCredentialsAccountModel.swift b/Multiplatform/iOS/Settings/Accounts/SettingsCredentialsAccountModel.swift deleted file mode 100644 index d3bb0671a..000000000 --- a/Multiplatform/iOS/Settings/Accounts/SettingsCredentialsAccountModel.swift +++ /dev/null @@ -1,281 +0,0 @@ -// -// SettingsCredentialsAccountModel.swift -// Multiplatform iOS -// -// Created by Rizwan on 21/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import Account -import Secrets - -class SettingsCredentialsAccountModel: ObservableObject { - var account: Account? = nil - var accountType: AccountType - @Published var shouldDismiss: Bool = false - @Published var email: String = "" - @Published var password: String = "" - @Published var apiUrl: String = "" - @Published var busy: Bool = false - @Published var accountCredentialsError: AccountCredentialsError? { - didSet { - accountCredentialsError != AccountCredentialsError.none ? (showError = true) : (showError = false) - } - } - @Published var showError: Bool = false - @Published var showPassword: Bool = false - - init(account: Account) { - self.account = account - self.accountType = account.type - if let credentials = try? account.retrieveCredentials(type: .basic) { - self.email = credentials.username - self.password = credentials.secret - } - } - - init(accountType: AccountType) { - self.accountType = accountType - } - - var isUpdate: Bool { - return account != nil - } - - var isValid: Bool { - if apiUrlEnabled { - return !email.isEmpty && !password.isEmpty && !apiUrl.isEmpty - } - return !email.isEmpty && !password.isEmpty - } - - var accountName: String { - switch accountType { - case .onMyMac: - return Account.defaultLocalAccountName - case .cloudKit: - return "iCloud" - case .feedbin: - return "Feedbin" - case .feedly: - return "Feedly" - case .feedWrangler: - return "Feed Wrangler" - case .newsBlur: - return "NewsBlur" - default: - return "" - } - } - - var emailText: String { - return accountType == .newsBlur ? NSLocalizedString("Username or Email", comment: "") : NSLocalizedString("Email", comment: "") - } - - var apiUrlEnabled: Bool { - return accountType == .freshRSS - } - - func addAccount() { - switch accountType { - case .feedbin: - addFeedbinAccount() - case .feedWrangler: - addFeedWranglerAccount() - case .newsBlur: - addNewsBlurAccount() - case .freshRSS: - addFreshRSSAccount() - default: - return - } - } -} - -extension SettingsCredentialsAccountModel { - // MARK:- Feedbin - - func addFeedbinAccount() { - busy = true - accountCredentialsError = AccountCredentialsError.none - - let emailAddress = email.trimmingCharacters(in: .whitespaces) - let credentials = Credentials(type: .basic, username: emailAddress, secret: password) - - Account.validateCredentials(type: .feedbin, credentials: credentials) { (result) in - self.busy = false - - switch result { - case .success(let authenticated): - if (authenticated != nil) { - var newAccount = false - let workAccount: Account - if self.account == nil { - workAccount = AccountManager.shared.createAccount(type: .feedbin) - newAccount = true - } else { - workAccount = self.account! - } - - do { - do { - try workAccount.removeCredentials(type: .basic) - } catch {} - try workAccount.storeCredentials(credentials) - - if newAccount { - workAccount.refreshAll() { result in } - } - - self.shouldDismiss = true - } catch { - self.accountCredentialsError = AccountCredentialsError.keyChain - } - - } else { - self.accountCredentialsError = AccountCredentialsError.invalidCredentials - } - case .failure: - self.accountCredentialsError = AccountCredentialsError.noNetwork - } - } - } - - // MARK: FeedWrangler - - func addFeedWranglerAccount() { - busy = true - let credentials = Credentials(type: .feedWranglerBasic, username: email, secret: password) - - Account.validateCredentials(type: .feedWrangler, credentials: credentials) { [weak self] result in - guard let self = self else { return } - - self.busy = false - switch result { - case .success(let validatedCredentials): - guard let validatedCredentials = validatedCredentials else { - self.accountCredentialsError = .invalidCredentials - return - } - - let account = AccountManager.shared.createAccount(type: .feedWrangler) - do { - try account.removeCredentials(type: .feedWranglerBasic) - try account.removeCredentials(type: .feedWranglerToken) - try account.storeCredentials(credentials) - try account.storeCredentials(validatedCredentials) - self.shouldDismiss = true - account.refreshAll(completion: { result in - switch result { - case .success: - break - case .failure(let error): - self.accountCredentialsError = .other(error: error) - } - }) - - } catch { - self.accountCredentialsError = .keyChain - } - - case .failure: - self.accountCredentialsError = .noNetwork - } - } - } - - // MARK:- NewsBlur - - func addNewsBlurAccount() { - busy = true - let credentials = Credentials(type: .newsBlurBasic, username: email, secret: password) - - Account.validateCredentials(type: .newsBlur, credentials: credentials) { [weak self] result in - - guard let self = self else { return } - - self.busy = false - - switch result { - case .success(let validatedCredentials): - - guard let validatedCredentials = validatedCredentials else { - self.accountCredentialsError = .invalidCredentials - return - } - - let account = AccountManager.shared.createAccount(type: .newsBlur) - - do { - try account.removeCredentials(type: .newsBlurBasic) - try account.removeCredentials(type: .newsBlurSessionId) - try account.storeCredentials(credentials) - try account.storeCredentials(validatedCredentials) - self.shouldDismiss = true - account.refreshAll(completion: { result in - switch result { - case .success: - break - case .failure(let error): - self.accountCredentialsError = .other(error: error) - } - }) - - } catch { - self.accountCredentialsError = .keyChain - } - - case .failure: - self.accountCredentialsError = .noNetwork - } - } - } - - // MARK:- Fresh RSS - - func addFreshRSSAccount() { - busy = true - let credentials = Credentials(type: .readerBasic, username: email, secret: password) - - Account.validateCredentials(type: .freshRSS, credentials: credentials, endpoint: URL(string: apiUrl)!) { [weak self] result in - - guard let self = self else { return } - - self.busy = false - - switch result { - case .success(let validatedCredentials): - - guard let validatedCredentials = validatedCredentials else { - self.accountCredentialsError = .invalidCredentials - return - } - - let account = AccountManager.shared.createAccount(type: .freshRSS) - - do { - try account.removeCredentials(type: .readerBasic) - try account.removeCredentials(type: .readerAPIKey) - try account.storeCredentials(credentials) - try account.storeCredentials(validatedCredentials) - self.shouldDismiss = true - account.refreshAll(completion: { result in - switch result { - case .success: - break - case .failure(let error): - self.accountCredentialsError = .other(error: error) - } - }) - - } catch { - self.accountCredentialsError = .keyChain - } - - case .failure: - self.accountCredentialsError = .noNetwork - } - } - } -} diff --git a/Multiplatform/iOS/Settings/Accounts/SettingsCredentialsAccountView.swift b/Multiplatform/iOS/Settings/Accounts/SettingsCredentialsAccountView.swift deleted file mode 100644 index 25b475c22..000000000 --- a/Multiplatform/iOS/Settings/Accounts/SettingsCredentialsAccountView.swift +++ /dev/null @@ -1,97 +0,0 @@ -// -// SettingsCredentialsAccountView.swift -// Multiplatform iOS -// -// Created by Rizwan on 21/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SettingsCredentialsAccountView: View { - @Environment(\.presentationMode) var presentationMode - @ObservedObject var settingsModel: SettingsCredentialsAccountModel - - init(account: Account) { - self.settingsModel = SettingsCredentialsAccountModel(account: account) - } - - init(accountType: AccountType) { - self.settingsModel = SettingsCredentialsAccountModel(accountType: accountType) - } - - var body: some View { - NavigationView { - List { - Section(header: AccountHeaderImageView(image: AppAssets.image(for: settingsModel.accountType)!)) { - TextField(settingsModel.emailText, text: $settingsModel.email).textContentType(.emailAddress) - HStack { - if settingsModel.showPassword { - TextField("Password", text:$settingsModel.password) - } - else { - SecureField("Password", text: $settingsModel.password) - } - Button(action: { - settingsModel.showPassword.toggle() - }) { - Text(settingsModel.showPassword ? "Hide" : "Show") - } - } - if settingsModel.apiUrlEnabled { - TextField("API URL", text: $settingsModel.apiUrl) - } - } - Section(footer: errorFooter) { - HStack { - Spacer() - Button(action: { settingsModel.addAccount() }) { - if settingsModel.isUpdate { - Text("Update Account") - } else { - Text("Add Account") - } - } - .disabled(!settingsModel.isValid) - Spacer() - if settingsModel.busy { - ProgressView() - } - } - } - } - .listStyle(InsetGroupedListStyle()) - .disabled(settingsModel.busy) - .onReceive(settingsModel.$shouldDismiss, perform: { dismiss in - if dismiss == true { - presentationMode.wrappedValue.dismiss() - } - }) - .navigationBarTitle(Text(verbatim: settingsModel.accountName), displayMode: .inline) - .navigationBarItems(leading: - Button(action: { self.dismiss() }) { Text("Cancel") } - ) - } - } - - var errorFooter: some View { - HStack { - Spacer() - if settingsModel.showError { - Text(verbatim: settingsModel.accountCredentialsError!.description).foregroundColor(.red) - } - Spacer() - } - } - - private func dismiss() { - presentationMode.wrappedValue.dismiss() - } -} - -struct SettingsCredentialsAccountView_Previews: PreviewProvider { - static var previews: some View { - SettingsCredentialsAccountView(accountType: .feedbin) - } -} diff --git a/Multiplatform/iOS/Settings/Accounts/SettingsDetailAccountModel.swift b/Multiplatform/iOS/Settings/Accounts/SettingsDetailAccountModel.swift deleted file mode 100644 index de4965ec5..000000000 --- a/Multiplatform/iOS/Settings/Accounts/SettingsDetailAccountModel.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// SettingsDetailAccountModel.swift -// Multiplatform iOS -// -// Created by Rizwan on 08/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import RSCore - -class SettingsDetailAccountModel: ObservableObject { - let account: Account - @Published var name: String { - didSet { - account.name = name.isEmpty ? nil : name - } - } - @Published var isActive: Bool { - didSet { - account.isActive = isActive - } - } - - init(_ account: Account) { - self.account = account - self.name = account.name ?? "" - self.isActive = account.isActive - } - - var defaultName: String { - account.defaultName - } - - var nameForDisplay: String { - account.nameForDisplay - } - - var accountImage: RSImage { - AppAssets.image(for: account.type)! - } - - var isCredentialsAvailable: Bool { - return account.type != .onMyMac - } - - var isDeletable: Bool { - return AccountManager.shared.defaultAccount != account - } - - func delete() { - AccountManager.shared.deleteAccount(account) - } -} diff --git a/Multiplatform/iOS/Settings/Accounts/SettingsDetailAccountView.swift b/Multiplatform/iOS/Settings/Accounts/SettingsDetailAccountView.swift deleted file mode 100644 index 0e2850526..000000000 --- a/Multiplatform/iOS/Settings/Accounts/SettingsDetailAccountView.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// SettingsDetailAccountView.swift -// Multiplatform iOS -// -// Created by Rizwan on 08/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Combine -import Account -import RSWeb -import RSCore - -struct SettingsDetailAccountView: View { - @Environment(\.presentationMode) var presentationMode - @ObservedObject var settingsModel: SettingsDetailAccountModel - @State private var isFeedbinCredentialsPresented = false - @State private var isDeleteAlertPresented = false - - init(_ account: Account) { - settingsModel = SettingsDetailAccountModel.init(account) - } - - var body: some View { - List { - Section(header:AccountHeaderImageView(image: settingsModel.accountImage)) { - HStack { - TextField(settingsModel.defaultName, text: $settingsModel.name) - } - Toggle(isOn: $settingsModel.isActive) { - Text("Active") - } - } - if settingsModel.isCredentialsAvailable { - Section { - HStack { - Spacer() - Button(action: { - self.isFeedbinCredentialsPresented.toggle() - }) { - Text("Credentials") - } - Spacer() - } - } - .sheet(isPresented: $isFeedbinCredentialsPresented) { - self.settingsCredentialsAccountView - } - } - if settingsModel.isDeletable { - Section { - HStack { - Spacer() - Button(action: { - self.isDeleteAlertPresented.toggle() - }) { - Text("Delete Account").foregroundColor(.red) - } - Spacer() - } - .alert(isPresented: $isDeleteAlertPresented) { - Alert( - title: Text("Are you sure you want to delete \"\(settingsModel.nameForDisplay)\"?"), - primaryButton: Alert.Button.default( - Text("Delete"), - action: { - self.settingsModel.delete() - self.dismiss() - }), - secondaryButton: Alert.Button.cancel() - ) - } - } - } - } - .listStyle(InsetGroupedListStyle()) - .navigationBarTitle(Text(verbatim: settingsModel.nameForDisplay), displayMode: .inline) - } - - var settingsCredentialsAccountView: SettingsCredentialsAccountView { - return SettingsCredentialsAccountView(account: settingsModel.account) - } - - func dismiss() { - presentationMode.wrappedValue.dismiss() - } -} - -struct SettingsDetailAccountView_Previews: PreviewProvider { - static var previews: some View { - return SettingsDetailAccountView(AccountManager.shared.defaultAccount) - } -} diff --git a/Multiplatform/iOS/Settings/Accounts/SettingsLocalAccountView.swift b/Multiplatform/iOS/Settings/Accounts/SettingsLocalAccountView.swift deleted file mode 100644 index c7753d6ac..000000000 --- a/Multiplatform/iOS/Settings/Accounts/SettingsLocalAccountView.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// SettingsLocalAccountView.swift -// Multiplatform iOS -// -// Created by Rizwan on 07/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SettingsLocalAccountView: View { - @Environment(\.presentationMode) var presentation - @State var name: String = "" - - var body: some View { - NavigationView { - List { - Section(header: AccountHeaderImageView(image: AppAssets.image(for: .onMyMac)!)) { - HStack { - TextField("Name", text: $name) - } - } - Section { - HStack { - Spacer() - Button(action: { self.addAccount() }) { - Text("Add Account") - } - Spacer() - } - } - } - .listStyle(InsetGroupedListStyle()) - .navigationBarTitle(Text(verbatim: Account.defaultLocalAccountName), displayMode: .inline) - .navigationBarItems(leading: Button(action: { self.dismiss() }) { Text("Cancel") } ) - } - } - - private func addAccount() { - let account = AccountManager.shared.createAccount(type: .onMyMac) - account.name = name - dismiss() - } - - private func dismiss() { - presentation.wrappedValue.dismiss() - } -} - -struct SettingsLocalAccountView_Previews: PreviewProvider { - static var previews: some View { - SettingsLocalAccountView() - } -} diff --git a/Multiplatform/iOS/Settings/ColorPaletteContainerView.swift b/Multiplatform/iOS/Settings/ColorPaletteContainerView.swift deleted file mode 100644 index 3cf4ee25a..000000000 --- a/Multiplatform/iOS/Settings/ColorPaletteContainerView.swift +++ /dev/null @@ -1,60 +0,0 @@ -// -// ColorPaletteContainerView.swift -// Multiplatform iOS -// -// Created by Rizwan on 02/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct ColorPaletteContainerView: View { - private let colorPalettes = UserInterfaceColorPalette.allCases - @EnvironmentObject private var appSettings: AppDefaults - @Environment(\.presentationMode) var presentationMode - - var body: some View { - List { - ForEach.init(0 ..< colorPalettes.count) { index in - Button(action: { - onTapColorPalette(at:index) - }) { - ColorPaletteView(colorPalette: colorPalettes[index]) - } - } - } - .listStyle(InsetGroupedListStyle()) - .navigationBarTitle("Color Palette", displayMode: .inline) - } - - func onTapColorPalette(at index: Int) { - if let colorPalette = UserInterfaceColorPalette(rawValue: index) { - appSettings.userInterfaceColorPalette = colorPalette - } - self.presentationMode.wrappedValue.dismiss() - } -} - -struct ColorPaletteView: View { - var colorPalette: UserInterfaceColorPalette - @EnvironmentObject private var appSettings: AppDefaults - - var body: some View { - HStack { - Text(colorPalette.description).foregroundColor(.primary) - Spacer() - if colorPalette == appSettings.userInterfaceColorPalette { - Image(systemName: "checkmark") - .foregroundColor(.blue) - } - } - } -} - -struct ColorPaletteContainerView_Previews: PreviewProvider { - static var previews: some View { - NavigationView { - ColorPaletteContainerView() - } - } -} diff --git a/Multiplatform/iOS/Settings/FeedsSettingsModel.swift b/Multiplatform/iOS/Settings/FeedsSettingsModel.swift deleted file mode 100644 index 70da469d0..000000000 --- a/Multiplatform/iOS/Settings/FeedsSettingsModel.swift +++ /dev/null @@ -1,109 +0,0 @@ -// -// FeedsSettingsModel.swift -// Multiplatform iOS -// -// Created by Rizwan on 04/07/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import SwiftUI -import Account -import UniformTypeIdentifiers - -enum FeedsSettingsError: LocalizedError, Equatable { - case none, noActiveAccount, exportFailed(reason: String?), importFailed - - var errorDescription: String? { - switch self { - case .noActiveAccount: - return NSLocalizedString("You must have at least one active account.", comment: "Missing active account") - case .exportFailed(let reason): - return reason - case .importFailed: - return NSLocalizedString( - "We were unable to process the selected file. Please ensure that it is a properly formatted OPML file.", - comment: "Import Failed Message" - ) - default: - return nil - } - } - - var title: String? { - switch self { - case .noActiveAccount: - return NSLocalizedString("Error", comment: "Error Title") - case .exportFailed: - return NSLocalizedString("OPML Export Error", comment: "Export Failed") - case .importFailed: - return NSLocalizedString("Import Failed", comment: "Import Failed") - default: - return nil - } - } -} - -class FeedsSettingsModel: ObservableObject { - @Published var exportingFilePath = "" - @Published var feedsSettingsError: FeedsSettingsError? { - didSet { - feedsSettingsError != FeedsSettingsError.none ? (showError = true) : (showError = false) - } - } - @Published var showError: Bool = false - @Published var isImporting: Bool = false - @Published var isExporting: Bool = false - @Published var selectedAccount: Account? = nil - - let importingContentTypes: [UTType] = [UTType(filenameExtension: "opml"), UTType("public.xml")].compactMap { $0 } - - func checkForActiveAccount() -> Bool { - if AccountManager.shared.activeAccounts.count == 0 { - feedsSettingsError = .noActiveAccount - return false - } - return true - } - - func importOPML(account: Account?) { - selectedAccount = account - isImporting = true - } - - func exportOPML(account: Account?) { - selectedAccount = account - isExporting = true - } - - func generateExportURL() -> URL? { - guard let account = selectedAccount else { return nil } - let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces) - let filename = "Subscriptions-\(accountName).opml" - let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename) - let opmlString = OPMLExporter.OPMLString(with: account, title: filename) - do { - try opmlString.write(to: tempFile, atomically: true, encoding: String.Encoding.utf8) - } catch { - feedsSettingsError = .exportFailed(reason: error.localizedDescription) - return nil - } - - return tempFile - } - - func processImportedFiles(_ urls: [URL]) { - urls.forEach{ - selectedAccount?.importOPML($0, completion: { [weak self] result in - switch result { - case .success: - break - case .failure: - self?.feedsSettingsError = .importFailed - break - } - }) - } - } -} - diff --git a/Multiplatform/iOS/Settings/SettingsModel.swift b/Multiplatform/iOS/Settings/SettingsModel.swift deleted file mode 100644 index 3c8218ae1..000000000 --- a/Multiplatform/iOS/Settings/SettingsModel.swift +++ /dev/null @@ -1,95 +0,0 @@ -// -// SettingsModel.swift -// Multiplatform iOS -// -// Created by Maurice Parker on 7/4/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import Account - -class SettingsModel: ObservableObject { - - enum HelpSites { - case netNewsWireHelp, netNewsWire, supportNetNewsWire, github, bugTracker, technotes, netNewsWireSlack, releaseNotes, none - - var url: URL? { - switch self { - case .netNewsWireHelp: - return URL(string: "https://netnewswire.com/help/ios/5.0/en/")! - case .netNewsWire: - return URL(string: "https://netnewswire.com/")! - case .supportNetNewsWire: - return URL(string: "https://github.com/brentsimmons/NetNewsWire/blob/main/Technotes/HowToSupportNetNewsWire.markdown")! - case .github: - return URL(string: "https://github.com/brentsimmons/NetNewsWire")! - case .bugTracker: - return URL(string: "https://github.com/brentsimmons/NetNewsWire/issues")! - case .technotes: - return URL(string: "https://github.com/brentsimmons/NetNewsWire/tree/main/Technotes")! - case .netNewsWireSlack: - return URL(string: "https://netnewswire.com/slack")! - case .releaseNotes: - return URL.releaseNotes - case .none: - return nil - } - } - } - - @Published var presentSheet: Bool = false - var accounts: [Account] { - get { - AccountManager.shared.sortedAccounts - } - set { - - } - } - - var activeAccounts: [Account] { - get { - AccountManager.shared.sortedActiveAccounts - } - set { - - } - } - - // MARK: Init - - init() { - NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange), name: .DisplayNameDidChange, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(userDidAddAccount), name: .UserDidAddAccount, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(userDidDeleteAccount), name: .UserDidDeleteAccount, object: nil) - } - - var selectedWebsite: HelpSites = .none { - didSet { - if selectedWebsite == .none { - presentSheet = false - } else { - presentSheet = true - } - } - } - - func refreshAccounts() { - objectWillChange.self.send() - } - - // MARK:- Notifications - - @objc func displayNameDidChange() { - refreshAccounts() - } - - @objc func userDidAddAccount() { - refreshAccounts() - } - - @objc func userDidDeleteAccount() { - refreshAccounts() - } -} diff --git a/Multiplatform/iOS/Settings/SettingsView.swift b/Multiplatform/iOS/Settings/SettingsView.swift deleted file mode 100644 index 33c94ae25..000000000 --- a/Multiplatform/iOS/Settings/SettingsView.swift +++ /dev/null @@ -1,244 +0,0 @@ -// -// SettingsView.swift -// Multiplatform iOS -// -// Created by Stuart Breckenridge on 30/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct SettingsView: View { - - @Environment(\.presentationMode) var presentationMode - - @StateObject private var viewModel = SettingsModel() - @StateObject private var feedsSettingsModel = FeedsSettingsModel() - @StateObject private var settings = AppDefaults.shared - - var body: some View { - NavigationView { - List { - systemSettings - accounts - importExport - timeline - articles - appearance - help - } - .listStyle(InsetGroupedListStyle()) - .navigationBarTitle("Settings", displayMode: .inline) - .navigationBarItems(leading: - HStack { - Button("Done") { - presentationMode.wrappedValue.dismiss() - } - } - ) - } - .fileImporter( - isPresented: $feedsSettingsModel.isImporting, - allowedContentTypes: feedsSettingsModel.importingContentTypes, - allowsMultipleSelection: true, - onCompletion: { result in - if let urls = try? result.get() { - feedsSettingsModel.processImportedFiles(urls) - } - } - ) - .fileMover(isPresented: $feedsSettingsModel.isExporting, - file: feedsSettingsModel.generateExportURL()) { _ in } - .sheet(isPresented: $viewModel.presentSheet, content: { - SafariView(url: viewModel.selectedWebsite.url!) - }) - } - - var systemSettings: some View { - Section(header: Text("Notifications, Badge, Data, & More"), content: { - Button(action: { - UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!) - }, label: { - Text("Open System Settings").foregroundColor(.primary) - }) - }) - } - - var accounts: some View { - Section(header: Text("Accounts"), content: { - ForEach(0.. 1 { - NavigationLink("Import Subscriptions", destination: importOptions) - } - else { - Button(action:{ - if feedsSettingsModel.checkForActiveAccount() { - feedsSettingsModel.importOPML(account: viewModel.activeAccounts.first) - } - }) { - Text("Import Subscriptions") - .foregroundColor(.primary) - } - } - - if viewModel.accounts.count > 1 { - NavigationLink("Export Subscriptions", destination: exportOptions) - } - else { - Button(action:{ - feedsSettingsModel.exportOPML(account: viewModel.accounts.first) - }) { - Text("Export Subscriptions") - .foregroundColor(.primary) - } - } - Toggle("Confirm When Deleting", isOn: $settings.sidebarConfirmDelete) - .toggleStyle(SwitchToggleStyle(tint: .accentColor)) - }) - .alert(isPresented: $feedsSettingsModel.showError) { - Alert( - title: Text(feedsSettingsModel.feedsSettingsError!.title ?? "Oops"), - message: Text(feedsSettingsModel.feedsSettingsError!.localizedDescription), - dismissButton: Alert.Button.cancel({ - feedsSettingsModel.feedsSettingsError = FeedsSettingsError.none - })) - } - } - - var importOptions: some View { - List { - Section(header: Text("Choose an account to receive the imported feeds and folders"), content: { - ForEach(0.. String { - let version = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "" - let build = Bundle.main.object(forInfoDictionaryKey: "CFBundleVersion") as? String ?? "" - return "NetNewsWire \(version) (Build \(build))" - } - -} - -struct SettingsView_Previews: PreviewProvider { - static var previews: some View { - SettingsView() - } -} diff --git a/Multiplatform/iOS/Settings/TimelineLayoutView.swift b/Multiplatform/iOS/Settings/TimelineLayoutView.swift deleted file mode 100644 index c0c47ccdb..000000000 --- a/Multiplatform/iOS/Settings/TimelineLayoutView.swift +++ /dev/null @@ -1,84 +0,0 @@ -// -// TimelineLayoutView.swift -// Multiplatform iOS -// -// Created by Stuart Breckenridge on 1/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct TimelineLayoutView: View { - - @EnvironmentObject private var defaults: AppDefaults - - private let sampleTitle = "Lorem dolor sed viverra ipsum. Gravida rutrum quisque non tellus. Rutrum tellus pellentesque eu tincidunt tortor. Sed blandit libero volutpat sed cras ornare. Et netus et malesuada fames ac. Ultrices eros in cursus turpis massa tincidunt dui ut ornare. Lacus sed viverra tellus in. Sollicitudin ac orci phasellus egestas. Purus in mollis nunc sed. Sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque. Interdum consectetur libero id faucibus nisl tincidunt eget." - - var body: some View { - VStack(spacing: 0) { - List { - Section(header: Text("Icon Size"), content: { - iconSize - }) - Section(header: Text("Number of Lines"), content: { - numberOfLines - }) } - .listStyle(InsetGroupedListStyle()) - - Divider() - timelineRowPreview.padding() - Divider() - } - .navigationBarTitle("Timeline Layout") - } - - var iconSize: some View { - Slider(value: $defaults.timelineIconDimensions, in: 20...60, step: 10, minimumValueLabel: Text("Small"), maximumValueLabel: Text("Large"), label: { - Text(String(defaults.timelineIconDimensions)) - }) - } - - var numberOfLines: some View { - Slider(value: $defaults.timelineNumberOfLines, in: 1...5, step: 1, minimumValueLabel: Text("1"), maximumValueLabel: Text("5"), label: { - Text("Article Title") - }) - - - } - - var timelineRowPreview: some View { - - HStack(alignment: .top) { - Image(systemName: "circle.fill") - .resizable() - .frame(width: 10, height: 10, alignment: .top) - .foregroundColor(.accentColor) - - Image(systemName: "paperplane.circle") - .resizable() - .frame(width: CGFloat(defaults.timelineIconDimensions), height: CGFloat(defaults.timelineIconDimensions), alignment: .top) - .foregroundColor(.accentColor) - - VStack(alignment: .leading, spacing: 4) { - Text(sampleTitle) - .font(.headline) - .lineLimit(Int(defaults.timelineNumberOfLines)) - HStack { - Text("Feed Name") - .foregroundColor(.secondary) - .font(.footnote) - Spacer() - Text("10:31") - .font(.footnote) - .foregroundColor(.secondary) - } - } - } - } -} - -struct TimelineLayout_Previews: PreviewProvider { - static var previews: some View { - TimelineLayoutView() - } -} diff --git a/Multiplatform/iOS/iOS-dev.entitlements b/Multiplatform/iOS/iOS-dev.entitlements deleted file mode 100644 index 05d04e805..000000000 --- a/Multiplatform/iOS/iOS-dev.entitlements +++ /dev/null @@ -1,14 +0,0 @@ - - - - - com.apple.security.application-groups - - group.$(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS - - keychain-access-groups - - $(AppIdentifierPrefix)$(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS - - - diff --git a/Multiplatform/iOS/iOS.entitlements b/Multiplatform/iOS/iOS.entitlements deleted file mode 100644 index 028d33157..000000000 --- a/Multiplatform/iOS/iOS.entitlements +++ /dev/null @@ -1,24 +0,0 @@ - - - - - aps-environment - development - com.apple.developer.icloud-container-identifiers - - iCloud.$(ORGANIZATION_IDENTIFIER).NetNewsWire - - com.apple.developer.icloud-services - - CloudKit - - com.apple.security.application-groups - - group.$(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS - - keychain-access-groups - - $(AppIdentifierPrefix)$(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS - - - diff --git a/Multiplatform/macOS/AppDelegate.swift b/Multiplatform/macOS/AppDelegate.swift deleted file mode 100644 index 1174c3fdf..000000000 --- a/Multiplatform/macOS/AppDelegate.swift +++ /dev/null @@ -1,284 +0,0 @@ -// -// AppDelegate.swift -// Multiplatform macOS -// -// Created by Maurice Parker on 6/28/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import AppKit -import os.log -import UserNotifications -import Articles -import RSWeb -import Account -import RSCore -import Secrets - -// If we're not going to import Sparkle, provide dummy protocols to make it easy -// for AppDelegate to comply -#if MAC_APP_STORE || TEST -protocol SPUStandardUserDriverDelegate {} -protocol SPUUpdaterDelegate {} -#else -import Sparkle -#endif - -var appDelegate: AppDelegate! - -class AppDelegate: NSObject, NSApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider, SPUStandardUserDriverDelegate, SPUUpdaterDelegate -{ - - private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") - - var userNotificationManager: UserNotificationManager! - var faviconDownloader: FaviconDownloader! - var imageDownloader: ImageDownloader! - var authorAvatarDownloader: AuthorAvatarDownloader! - var webFeedIconDownloader: WebFeedIconDownloader! - - var refreshTimer: AccountRefreshTimer? - var syncTimer: ArticleStatusSyncTimer? - - var shuttingDown = false { - didSet { - if shuttingDown { - refreshTimer?.shuttingDown = shuttingDown - refreshTimer?.invalidate() - syncTimer?.shuttingDown = shuttingDown - syncTimer?.invalidate() - } - } - } - - var unreadCount = 0 { - didSet { - if unreadCount != oldValue { - CoalescingQueue.standard.add(self, #selector(updateDockBadge)) - postUnreadCountDidChangeNotification() - } - } - } - - var appName: String! - - private let appMovementMonitor = RSAppMovementMonitor() - #if !MAC_APP_STORE && !TEST - var softwareUpdater: SPUUpdater! - #endif - - override init() { - super.init() - - SecretsManager.provider = Secrets() - AccountManager.shared = AccountManager(accountsFolder: Platform.dataSubfolder(forApplication: nil, folderName: "Accounts")!) - FeedProviderManager.shared.delegate = ExtensionPointManager.shared - - NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) - NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil) - - appDelegate = self - } - - // MARK: - NSApplicationDelegate - - func applicationWillFinishLaunching(_ notification: Notification) { - // TODO: add Apple Events back in -// installAppleEventHandlers() - - CacheCleaner.purgeIfNecessary() - - // Try to establish a cache in the Caches folder, but if it fails for some reason fall back to a temporary dir - let cacheFolder: String - if let userCacheFolder = try? FileManager.default.url(for: .cachesDirectory, in: .userDomainMask, appropriateFor: nil, create: false).path { - cacheFolder = userCacheFolder - } - else { - let bundleIdentifier = (Bundle.main.infoDictionary!["CFBundleIdentifier"]! as! String) - cacheFolder = (NSTemporaryDirectory() as NSString).appendingPathComponent(bundleIdentifier) - } - - let faviconsFolder = (cacheFolder as NSString).appendingPathComponent("Favicons") - let faviconsFolderURL = URL(fileURLWithPath: faviconsFolder) - try! FileManager.default.createDirectory(at: faviconsFolderURL, withIntermediateDirectories: true, attributes: nil) - faviconDownloader = FaviconDownloader(folder: faviconsFolder) - - let imagesFolder = (cacheFolder as NSString).appendingPathComponent("Images") - let imagesFolderURL = URL(fileURLWithPath: imagesFolder) - try! FileManager.default.createDirectory(at: imagesFolderURL, withIntermediateDirectories: true, attributes: nil) - imageDownloader = ImageDownloader(folder: imagesFolder) - - authorAvatarDownloader = AuthorAvatarDownloader(imageDownloader: imageDownloader) - webFeedIconDownloader = WebFeedIconDownloader(imageDownloader: imageDownloader, folder: cacheFolder) - - appName = (Bundle.main.infoDictionary!["CFBundleExecutable"]! as! String) - } - - func applicationDidFinishLaunching(_ note: Notification) { - - #if MAC_APP_STORE || TEST - checkForUpdatesMenuItem.isHidden = true - #else - // Initialize Sparkle... - let hostBundle = Bundle.main - let updateDriver = SPUStandardUserDriver(hostBundle: hostBundle, delegate: self) - self.softwareUpdater = SPUUpdater(hostBundle: hostBundle, applicationBundle: hostBundle, userDriver: updateDriver, delegate: self) - - do { - try self.softwareUpdater.start() - } - catch { - NSLog("Failed to start software updater with error: \(error)") - } - #endif - - AppDefaults.registerDefaults() - let isFirstRun = AppDefaults.shared.isFirstRun() - if isFirstRun { - os_log(.debug, log: log, "Is first run.") - } - let localAccount = AccountManager.shared.defaultAccount - - if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() { - DefaultFeedsImporter.importDefaultFeeds(account: localAccount) - } - - - NotificationCenter.default.addObserver(self, selector: #selector(webFeedSettingDidChange(_:)), name: .WebFeedSettingDidChange, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) - - DispatchQueue.main.async { - self.unreadCount = AccountManager.shared.unreadCount - } - - refreshTimer = AccountRefreshTimer() - syncTimer = ArticleStatusSyncTimer() - - NSApplication.shared.registerForRemoteNotifications() - - UNUserNotificationCenter.current().delegate = self - userNotificationManager = UserNotificationManager() - - // TODO: Add a debug menu -// if AppDefaults.showDebugMenu { -// refreshTimer!.update() -// syncTimer!.update() -// -// // The Web Inspector uses SPI and can never appear in a MAC_APP_STORE build. -// #if MAC_APP_STORE -// let debugMenu = debugMenuItem.submenu! -// let toggleWebInspectorItemIndex = debugMenu.indexOfItem(withTarget: self, andAction: #selector(toggleWebInspectorEnabled(_:))) -// if toggleWebInspectorItemIndex != -1 { -// debugMenu.removeItem(at: toggleWebInspectorItemIndex) -// } -// #endif -// } else { -// debugMenuItem.menu?.removeItem(debugMenuItem) -// DispatchQueue.main.async { -// self.refreshTimer!.timedRefresh(nil) -// self.syncTimer!.timedRefresh(nil) -// } -// } - - // TODO: Add back in crash reporter -// #if !MAC_APP_STORE -// DispatchQueue.main.async { -// CrashReporter.check(appName: "NetNewsWire") -// } -// #endif - - } - - func applicationDidBecomeActive(_ notification: Notification) { - fireOldTimers() - } - - func applicationDidResignActive(_ notification: Notification) { - ArticleStringFormatter.emptyCaches() - } - - func application(_ application: NSApplication, didReceiveRemoteNotification userInfo: [String : Any]) { - AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) - } - - func applicationWillTerminate(_ notification: Notification) { - shuttingDown = true - } - - // MARK: Notifications - @objc func unreadCountDidChange(_ note: Notification) { - if note.object is AccountManager { - unreadCount = AccountManager.shared.unreadCount - } - } - - @objc func webFeedSettingDidChange(_ note: Notification) { - guard let feed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else { - return - } - if key == WebFeed.WebFeedSettingKey.homePageURL || key == WebFeed.WebFeedSettingKey.faviconURL { - let _ = faviconDownloader.favicon(for: feed) - } - } - - @objc func userDefaultsDidChange(_ note: Notification) { - refreshTimer?.update() - updateDockBadge() - } - - @objc func didWakeNotification(_ note: Notification) { - fireOldTimers() - } - - // MARK: UNUserNotificationCenterDelegate - - func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler([.banner, .badge, .sound]) - } - - func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { -// TODO: Add back in Notification handling -// mainWindowController?.handle(response) - completionHandler() - } - - // MARK: - Dock Badge - @objc func updateDockBadge() { - let label = unreadCount > 0 && !AppDefaults.shared.hideDockUnreadCount ? "\(unreadCount)" : "" - NSApplication.shared.dockTile.badgeLabel = label - } - -} - -private extension AppDelegate { - - func fireOldTimers() { - // It’s possible there’s a refresh timer set to go off in the past. - // In that case, refresh now and update the timer. - refreshTimer?.fireOldTimer() - syncTimer?.fireOldTimer() - } - -} - -/* - the ScriptingAppDelegate protocol exposes a narrow set of accessors with - internal visibility which are very similar to some private vars. - - These would be unnecessary if the similar accessors were marked internal rather than private, - but for now, we'll keep the stratification of visibility -*/ -//extension AppDelegate : ScriptingAppDelegate { -// -// internal var scriptingMainWindowController: ScriptingMainWindowController? { -// return mainWindowController -// } -// -// internal var scriptingCurrentArticle: Article? { -// return self.scriptingMainWindowController?.scriptingCurrentArticle -// } -// -// internal var scriptingSelectedArticles: [Article] { -// return self.scriptingMainWindowController?.scriptingSelectedArticles ?? [] -// } -//} diff --git a/Multiplatform/macOS/Article/ArticleView.swift b/Multiplatform/macOS/Article/ArticleView.swift deleted file mode 100644 index 71f0704aa..000000000 --- a/Multiplatform/macOS/Article/ArticleView.swift +++ /dev/null @@ -1,25 +0,0 @@ -// -// ArticleView.swift -// Multiplatform macOS -// -// Created by Maurice Parker on 7/8/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Articles - -struct ArticleView: NSViewControllerRepresentable { - - @EnvironmentObject private var sceneModel: SceneModel - - func makeNSViewController(context: Context) -> WebViewController { - let controller = WebViewController() - controller.sceneModel = sceneModel - return controller - } - - func updateNSViewController(_ controller: WebViewController, context: Context) { - } - -} diff --git a/Multiplatform/macOS/Article/IconView.swift b/Multiplatform/macOS/Article/IconView.swift deleted file mode 100644 index 9a72b5dd5..000000000 --- a/Multiplatform/macOS/Article/IconView.swift +++ /dev/null @@ -1,134 +0,0 @@ -// -// IconView.swift -// Multiplatform macOS -// -// Created by Maurice Parker on 7/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import AppKit - -final class IconView: NSView { - - var iconImage: IconImage? = nil { - didSet { - if iconImage !== oldValue { - imageView.image = iconImage?.image - - if NSApplication.shared.effectiveAppearance.isDarkMode { - if self.iconImage?.isDark ?? false { - self.isDiscernable = false - } else { - self.isDiscernable = true - } - } else { - if self.iconImage?.isBright ?? false { - self.isDiscernable = false - } else { - self.isDiscernable = true - } - } - - needsDisplay = true - needsLayout = true - } - } - } - - private var isDiscernable = true - - override var isFlipped: Bool { - return true - } - - private let imageView: NSImageView = { - let imageView = NSImageView(frame: NSRect.zero) - imageView.animates = false - imageView.imageAlignment = .alignCenter - imageView.imageScaling = .scaleProportionallyUpOrDown - return imageView - }() - - private var hasExposedVerticalBackground: Bool { - return imageView.frame.size.height < bounds.size.height - } - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - convenience init() { - self.init(frame: NSRect.zero) - } - - override func viewDidMoveToSuperview() { - needsLayout = true - needsDisplay = true - } - - override func layout() { - resizeSubviews(withOldSize: NSZeroSize) - } - - override func resizeSubviews(withOldSize oldSize: NSSize) { - imageView.setFrame(ifNotEqualTo: rectForImageView()) - } - - override func draw(_ dirtyRect: NSRect) { - guard hasExposedVerticalBackground || !isDiscernable else { - return - } - - let color = AppAssets.nsIconBackgroundColor - color.set() - dirtyRect.fill() - } -} - -private extension IconView { - - func commonInit() { - addSubview(imageView) - wantsLayer = true - layer?.cornerRadius = 4.0 - } - - func rectForImageView() -> NSRect { - guard !(iconImage?.isSymbol ?? false) else { - return NSMakeRect(0.0, 0.0, bounds.size.width, bounds.size.height) - } - - guard let image = iconImage?.image else { - return NSRect.zero - } - - let imageSize = image.size - let viewSize = bounds.size - if imageSize.height == imageSize.width { - if imageSize.height >= viewSize.height { - return NSMakeRect(0.0, 0.0, viewSize.width, viewSize.height) - } - let offset = floor((viewSize.height - imageSize.height) / 2.0) - return NSMakeRect(offset, offset, imageSize.width, imageSize.height) - } - else if imageSize.height > imageSize.width { - let factor = viewSize.height / imageSize.height - let width = imageSize.width * factor - let originX = floor((viewSize.width - width) / 2.0) - return NSMakeRect(originX, 0.0, width, viewSize.height) - } - - // Wider than tall: imageSize.width > imageSize.height - let factor = viewSize.width / imageSize.width - let height = imageSize.height * factor - let originY = floor((viewSize.height - height) / 2.0) - return NSMakeRect(0.0, originY, viewSize.width, height) - } -} - diff --git a/Multiplatform/macOS/Article/SharingServiceDelegate.swift b/Multiplatform/macOS/Article/SharingServiceDelegate.swift deleted file mode 100644 index 01e78fc3f..000000000 --- a/Multiplatform/macOS/Article/SharingServiceDelegate.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// SharingServiceDelegate.swift -// NetNewsWire -// -// Created by Maurice Parker on 9/7/18. -// Copyright © 2018 Ranchero Software. All rights reserved. -// - -import AppKit - -@objc final class SharingServiceDelegate: NSObject, NSSharingServiceDelegate { - - weak var window: NSWindow? - - init(_ window: NSWindow?) { - self.window = window - } - - func sharingService(_ sharingService: NSSharingService, willShareItems items: [Any]) { - sharingService.subject = items - .compactMap { item in - let writer = item as? ArticlePasteboardWriter - return writer?.article.title - } - .joined(separator: ", ") - } - - func sharingService(_ sharingService: NSSharingService, sourceWindowForShareItems items: [Any], sharingContentScope: UnsafeMutablePointer) -> NSWindow? { - return window - } - -} diff --git a/Multiplatform/macOS/Article/SharingServicePickerDelegate.swift b/Multiplatform/macOS/Article/SharingServicePickerDelegate.swift deleted file mode 100644 index bc6659530..000000000 --- a/Multiplatform/macOS/Article/SharingServicePickerDelegate.swift +++ /dev/null @@ -1,55 +0,0 @@ -// -// SharingServicePickerDelegate.swift -// NetNewsWire -// -// Created by Brent Simmons on 2/17/18. -// Copyright © 2018 Ranchero Software. All rights reserved. -// - -import AppKit -import RSCore - -@objc final class SharingServicePickerDelegate: NSObject, NSSharingServicePickerDelegate { - - private let sharingServiceDelegate: SharingServiceDelegate - private let completion: (() -> Void)? - - init(_ window: NSWindow?, completion: (() -> Void)?) { - self.sharingServiceDelegate = SharingServiceDelegate(window) - self.completion = completion - } - - func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] { - let filteredServices = proposedServices.filter { $0.menuItemTitle != "NetNewsWire" } - return filteredServices + SharingServicePickerDelegate.customSharingServices(for: items) - } - - func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, delegateFor sharingService: NSSharingService) -> NSSharingServiceDelegate? { - return sharingServiceDelegate - } - - func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, didChoose service: NSSharingService?) { - completion?() - } - - static func customSharingServices(for items: [Any]) -> [NSSharingService] { - let customServices = ExtensionPointManager.shared.activeSendToCommands.compactMap { (sendToCommand) -> NSSharingService? in - - guard let object = items.first else { - return nil - } - - guard sendToCommand.canSendObject(object, selectedText: nil) else { - return nil - } - - let image = sendToCommand.image ?? NSImage() - return NSSharingService(title: sendToCommand.title, image: image, alternateImage: nil) { - sendToCommand.sendObject(object, selectedText: nil) - } - } - return customServices - } -} - - diff --git a/Multiplatform/macOS/Article/SharingServiceView.swift b/Multiplatform/macOS/Article/SharingServiceView.swift deleted file mode 100644 index 71eb7e7a0..000000000 --- a/Multiplatform/macOS/Article/SharingServiceView.swift +++ /dev/null @@ -1,56 +0,0 @@ -// -// SharingServiceView.swift -// Multiplatform macOS -// -// Created by Maurice Parker on 7/14/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import AppKit -import Articles - -class SharingServiceController: NSViewController { - - var sharingServicePickerDelegate: SharingServicePickerDelegate? = nil - var articles = [Article]() - var completion: (() -> Void)? = nil - - override func loadView() { - view = NSView() - } - - override func viewDidAppear() { - guard let anchor = view.superview?.superview else { return } - - sharingServicePickerDelegate = SharingServicePickerDelegate(self.view.window, completion: completion) - - let sortedArticles = articles.sortedByDate(.orderedAscending) - let items = sortedArticles.map { ArticlePasteboardWriter(article: $0) } - - let sharingServicePicker = NSSharingServicePicker(items: items) - sharingServicePicker.delegate = sharingServicePickerDelegate - - sharingServicePicker.show(relativeTo: anchor.bounds, of: anchor, preferredEdge: .minY) - } - -} - -struct SharingServiceView: NSViewControllerRepresentable { - - var articles: [Article] - @Binding var showing: Bool - - func makeNSViewController(context: Context) -> SharingServiceController { - let controller = SharingServiceController() - controller.articles = articles - controller.completion = { - showing = false - } - return controller - } - - func updateNSViewController(_ nsViewController: SharingServiceController, context: Context) { - } - -} diff --git a/Multiplatform/macOS/Article/WebStatusBarView.swift b/Multiplatform/macOS/Article/WebStatusBarView.swift deleted file mode 100644 index 3b14f0484..000000000 --- a/Multiplatform/macOS/Article/WebStatusBarView.swift +++ /dev/null @@ -1,101 +0,0 @@ -// -// WebStatusBarView.swift -// Multiplatform macOS -// -// Created by Maurice Parker on 7/8/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import AppKit -import Articles - -final class WebStatusBarView: NSView { - - var urlLabel = NSTextField(labelWithString: "") - - var mouseoverLink: String? { - didSet { - updateLinkForDisplay() - } - } - - private var linkForDisplay: String? { - didSet { - needsLayout = true - if let link = linkForDisplay { - urlLabel.stringValue = link - self.isHidden = false - } - else { - urlLabel.stringValue = "" - self.isHidden = true - } - } - } - - private var didConfigureLayerRadius = false - - override var isOpaque: Bool { - return false - } - - override var isFlipped: Bool { - return true - } - - override var wantsUpdateLayer: Bool { - return true - } - - override init(frame frameRect: NSRect) { - super.init(frame: frameRect) - commonInit() - } - - required init?(coder: NSCoder) { - super.init(coder: coder) - commonInit() - } - - override func updateLayer() { - guard let layer = layer else { - return - } - if !didConfigureLayerRadius { - layer.cornerRadius = 4.0 - didConfigureLayerRadius = true - } - - layer.backgroundColor = AppAssets.webStatusBarBackground.cgColor - } -} - -// MARK: - Private - -private extension WebStatusBarView { - - func commonInit() { - self.isHidden = true - urlLabel.translatesAutoresizingMaskIntoConstraints = false - urlLabel.lineBreakMode = .byTruncatingMiddle - urlLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) - - addSubview(urlLabel) - NSLayoutConstraint.activate([ - leadingAnchor.constraint(equalTo: urlLabel.leadingAnchor, constant: -6), - trailingAnchor.constraint(equalTo: urlLabel.trailingAnchor, constant: 6), - centerYAnchor.constraint(equalTo: urlLabel.centerYAnchor) - ]) - } - - func updateLinkForDisplay() { - if let mouseoverLink = mouseoverLink, !mouseoverLink.isEmpty { - linkForDisplay = mouseoverLink.strippingHTTPOrHTTPSScheme - } - else { - linkForDisplay = nil - } - } -} - - diff --git a/Multiplatform/macOS/Article/WebViewController.swift b/Multiplatform/macOS/Article/WebViewController.swift deleted file mode 100644 index e71c7ef1d..000000000 --- a/Multiplatform/macOS/Article/WebViewController.swift +++ /dev/null @@ -1,373 +0,0 @@ -// -// WebViewController.swift -// Multiplatform macOS -// -// Created by Maurice Parker on 7/8/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import AppKit -import Combine -import RSCore -import Articles - -protocol WebViewControllerDelegate: AnyObject { - func webViewController(_: WebViewController, articleExtractorButtonStateDidUpdate: ArticleExtractorButtonState) -} - -class WebViewController: NSViewController { - - private struct MessageName { - static let imageWasClicked = "imageWasClicked" - static let imageWasShown = "imageWasShown" - static let mouseDidEnter = "mouseDidEnter" - static let mouseDidExit = "mouseDidExit" - static let showFeedInspector = "showFeedInspector" - } - - var statusBarView: WebStatusBarView! - - private var webView: PreloadedWebView? - - private var articleExtractor: ArticleExtractor? = nil - var extractedArticle: ExtractedArticle? - var isShowingExtractedArticle = false - - var articleExtractorButtonState: ArticleExtractorButtonState = .off { - didSet { - delegate?.webViewController(self, articleExtractorButtonStateDidUpdate: articleExtractorButtonState) - } - } - - var sceneModel: SceneModel? - weak var delegate: WebViewControllerDelegate? - - var articles: [Article]? { - didSet { - if oldValue != articles { - loadWebView() - } - } - } - - private var cancellables = Set() - - override func loadView() { - view = NSView() - } - - override func viewDidLoad() { - super.viewDidLoad() - - NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) - - statusBarView = WebStatusBarView() - statusBarView.translatesAutoresizingMaskIntoConstraints = false - self.view.addSubview(statusBarView) - NSLayoutConstraint.activate([ - self.view.leadingAnchor.constraint(equalTo: statusBarView.leadingAnchor, constant: -6), - self.view.trailingAnchor.constraint(greaterThanOrEqualTo: statusBarView.trailingAnchor, constant: 6), - self.view.bottomAnchor.constraint(equalTo: statusBarView.bottomAnchor, constant: 2), - statusBarView.heightAnchor.constraint(equalToConstant: 20) - ]) - - sceneModel?.timelineModel.selectedArticlesPublisher?.sink { [weak self] articles in - self?.articles = articles - } - .store(in: &cancellables) - } - - // MARK: Notifications - - @objc func webFeedIconDidBecomeAvailable(_ note: Notification) { - reloadArticleImage() - } - - @objc func avatarDidBecomeAvailable(_ note: Notification) { - reloadArticleImage() - } - - @objc func faviconDidBecomeAvailable(_ note: Notification) { - reloadArticleImage() - } - - // MARK: API - - func focus() { - webView?.becomeFirstResponder() - } - - func canScrollDown(_ completion: @escaping (Bool) -> Void) { - fetchScrollInfo { (scrollInfo) in - completion(scrollInfo?.canScrollDown ?? false) - } - } - - override func scrollPageDown(_ sender: Any?) { - webView?.scrollPageDown(sender) - } - - func toggleArticleExtractor() { - - guard let article = articles?.first else { - return - } - - guard articleExtractor?.state != .processing else { - stopArticleExtractor() - loadWebView() - return - } - - guard !isShowingExtractedArticle else { - isShowingExtractedArticle = false - loadWebView() - articleExtractorButtonState = .off - return - } - - if let articleExtractor = articleExtractor { - if article.preferredLink == articleExtractor.articleLink { - isShowingExtractedArticle = true - loadWebView() - articleExtractorButtonState = .on - } - } else { - startArticleExtractor() - } - - } - - func stopArticleExtractorIfProcessing() { - if articleExtractor?.state == .processing { - stopArticleExtractor() - } - } - - func stopWebViewActivity() { - if let webView = webView { - stopMediaPlayback(webView) - } - } - -} - -// MARK: ArticleExtractorDelegate - -extension WebViewController: ArticleExtractorDelegate { - - func articleExtractionDidFail(with: Error) { - stopArticleExtractor() - articleExtractorButtonState = .error - loadWebView() - } - - func articleExtractionDidComplete(extractedArticle: ExtractedArticle) { - if articleExtractor?.state != .cancelled { - self.extractedArticle = extractedArticle - isShowingExtractedArticle = true - loadWebView() - articleExtractorButtonState = .on - } - } - -} - - -// MARK: WKScriptMessageHandler - -extension WebViewController: WKScriptMessageHandler { - - func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { - switch message.name { - case MessageName.imageWasShown: - return - case MessageName.imageWasClicked: - return - case MessageName.mouseDidEnter: - if let link = message.body as? String { - statusBarView.mouseoverLink = link - } - case MessageName.mouseDidExit: - statusBarView.mouseoverLink = nil - case MessageName.showFeedInspector: - return - default: - return - } - } - -} - -extension WebViewController: WKNavigationDelegate { - - public func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { - if navigationAction.navigationType == .linkActivated { - if let url = navigationAction.request.url { - let flags = navigationAction.modifierFlags - let invert = flags.contains(.shift) || flags.contains(.command) - Browser.open(url.absoluteString, invertPreference: invert) - } - decisionHandler(.cancel) - return - } - - decisionHandler(.allow) - } - -} - -// MARK: Private - -private extension WebViewController { - - func loadWebView() { - if let webView = webView { - self.renderPage(webView) - return - } - - sceneModel?.webViewProvider?.dequeueWebView() { webView in - - webView.ready { - - // Add the webview - self.webView = webView - - webView.translatesAutoresizingMaskIntoConstraints = false - self.view.addSubview(webView, positioned: .below, relativeTo: self.statusBarView) - NSLayoutConstraint.activate([ - self.view.leadingAnchor.constraint(equalTo: webView.leadingAnchor), - self.view.trailingAnchor.constraint(equalTo: webView.trailingAnchor), - self.view.topAnchor.constraint(equalTo: webView.topAnchor), - self.view.bottomAnchor.constraint(equalTo: webView.bottomAnchor) - ]) - - webView.navigationDelegate = self - - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasClicked) - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.imageWasShown) - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.mouseDidEnter) - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.mouseDidExit) - webView.configuration.userContentController.add(WrapperScriptMessageHandler(self), name: MessageName.showFeedInspector) - - self.renderPage(webView) - - } - } - - } - - func renderPage(_ webView: PreloadedWebView) { - let style = ArticleStylesManager.shared.currentStyle - let rendering: ArticleRenderer.Rendering - - if articles?.count ?? 0 > 1 { - rendering = ArticleRenderer.multipleSelectionHTML(style: style) - } else if let articleExtractor = articleExtractor, articleExtractor.state == .processing { - rendering = ArticleRenderer.loadingHTML(style: style) - } else if let articleExtractor = articleExtractor, articleExtractor.state == .failedToParse, let article = articles?.first { - rendering = ArticleRenderer.articleHTML(article: article, style: style) - } else if let article = articles?.first, let extractedArticle = extractedArticle { - if isShowingExtractedArticle { - rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style) - } else { - rendering = ArticleRenderer.articleHTML(article: article, style: style) - } - } else if let article = articles?.first { - rendering = ArticleRenderer.articleHTML(article: article, style: style) - } else { - rendering = ArticleRenderer.noSelectionHTML(style: style) - } - - let substitutions = [ - "title": rendering.title, - "baseURL": rendering.baseURL, - "style": rendering.style, - "body": rendering.html - ] - - let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions) - webView.loadHTMLString(html, baseURL: ArticleRenderer.page.baseURL) - - } - - func fetchScrollInfo(_ completion: @escaping (ScrollInfo?) -> Void) { - guard let webView = webView else { - completion(nil) - return - } - - let javascriptString = "var x = {contentHeight: document.body.scrollHeight, offsetY: window.pageYOffset}; x" - - webView.evaluateJavaScript(javascriptString) { (info, error) in - guard let info = info as? [String: Any] else { - completion(nil) - return - } - guard let contentHeight = info["contentHeight"] as? CGFloat, let offsetY = info["offsetY"] as? CGFloat else { - completion(nil) - return - } - - let scrollInfo = ScrollInfo(contentHeight: contentHeight, viewHeight: webView.frame.height, offsetY: offsetY) - completion(scrollInfo) - } - } - - func startArticleExtractor() { - if let link = articles?.first?.preferredLink, let extractor = ArticleExtractor(link) { - extractor.delegate = self - extractor.process() - articleExtractor = extractor - articleExtractorButtonState = .animated - } - } - - func stopArticleExtractor() { - articleExtractor?.cancel() - articleExtractor = nil - isShowingExtractedArticle = false - articleExtractorButtonState = .off - } - - func reloadArticleImage() { - guard let article = articles?.first else { return } - - var components = URLComponents() - components.scheme = ArticleRenderer.imageIconScheme - components.path = article.articleID - - if let imageSrc = components.string { - webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")") - } - } - - func stopMediaPlayback(_ webView: WKWebView) { - webView.evaluateJavaScript("stopMediaPlayback();") - } - -} - -// MARK: - ScrollInfo - -private struct ScrollInfo { - - let contentHeight: CGFloat - let viewHeight: CGFloat - let offsetY: CGFloat - let canScrollDown: Bool - let canScrollUp: Bool - - init(contentHeight: CGFloat, viewHeight: CGFloat, offsetY: CGFloat) { - self.contentHeight = contentHeight - self.viewHeight = viewHeight - self.offsetY = offsetY - - self.canScrollDown = viewHeight + offsetY < contentHeight - self.canScrollUp = offsetY > 0.1 - } - -} diff --git a/Multiplatform/macOS/Browser.swift b/Multiplatform/macOS/Browser.swift deleted file mode 100644 index dfcbb1571..000000000 --- a/Multiplatform/macOS/Browser.swift +++ /dev/null @@ -1,65 +0,0 @@ -// -// Browser.swift -// Evergren -// -// Created by Brent Simmons on 2/23/16. -// Copyright © 2016 Ranchero Software, LLC. All rights reserved. -// - -import Foundation -import RSWeb - -struct Browser { - - /// The user-specified default browser for opening web pages. - /// - /// The user-assigned default browser, or `nil` if none was assigned - /// (i.e., the system default should be used). - static var defaultBrowser: MacWebBrowser? { - if let bundleID = AppDefaults.shared.defaultBrowserID, let browser = MacWebBrowser(bundleIdentifier: bundleID) { - return browser - } - - return nil - } - - - /// Opens a URL in the default browser. - /// - /// - Parameters: - /// - urlString: The URL to open. - /// - invert: Whether to invert the "open in background in browser" preference - static func open(_ urlString: String, invertPreference invert: Bool = false) { - // Opens according to prefs. - open(urlString, inBackground: invert ? !AppDefaults.shared.openInBrowserInBackground : AppDefaults.shared.openInBrowserInBackground) - } - - - /// Opens a URL in the default browser. - /// - /// - Parameters: - /// - urlString: The URL to open. - /// - inBackground: Whether to open the URL in the background or not. - /// - Note: Some browsers (specifically Chromium-derived ones) will ignore the request - /// to open in the background. - static func open(_ urlString: String, inBackground: Bool) { - if let url = URL(string: urlString) { - if let defaultBrowser = defaultBrowser { - defaultBrowser.openURL(url, inBackground: inBackground) - } else { - MacWebBrowser.openURL(url, inBackground: inBackground) - } - } - } -} - -extension Browser { - - static var titleForOpenInBrowserInverted: String { - let openInBackgroundPref = AppDefaults.shared.openInBrowserInBackground - - return openInBackgroundPref ? - NSLocalizedString("Open in Browser in Foreground", comment: "Open in Browser in Foreground menu item title") : - NSLocalizedString("Open in Browser in Background", comment: "Open in Browser in Background menu item title") - } -} diff --git a/Multiplatform/macOS/Info.plist b/Multiplatform/macOS/Info.plist deleted file mode 100644 index 58377c4e4..000000000 --- a/Multiplatform/macOS/Info.plist +++ /dev/null @@ -1,57 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - $(PRODUCT_BUNDLE_PACKAGE_TYPE) - CFBundleShortVersionString - $(MARKETING_VERSION) - CFBundleVersion - $(CURRENT_PROJECT_VERSION) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - Copyright © 2002-2021 Ranchero Software. All rights reserved. - NSAppTransportSecurity - - NSAllowsArbitraryLoads - - - UserAgent - NetNewsWire (RSS Reader; https://netnewswire.com/) - OrganizationIdentifier - $(ORGANIZATION_IDENTIFIER) - DeveloperEntitlements - $(DEVELOPER_ENTITLEMENTS) - CFBundleURLTypes - - - CFBundleTypeRole - Viewer - CFBundleURLName - RSS Feed - CFBundleURLSchemes - - feed - feeds - - - - SUFeedURL - https://ranchero.com/downloads/netnewswire-release.xml - FeedURLForTestBuilds - https://ranchero.com/downloads/netnewswire-beta.xml - - diff --git a/Multiplatform/macOS/MacSearchField.swift b/Multiplatform/macOS/MacSearchField.swift deleted file mode 100644 index 2ae870735..000000000 --- a/Multiplatform/macOS/MacSearchField.swift +++ /dev/null @@ -1,49 +0,0 @@ -// -// MacSearchField.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 29/6/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import AppKit -import SwiftUI - - -final class MacSearchField: NSViewRepresentable { - - typealias NSViewType = NSSearchField - - - func makeNSView(context: Context) -> NSSearchField { - let searchField = NSSearchField() - searchField.delegate = context.coordinator - return searchField - } - - func updateNSView(_ nsView: NSSearchField, context: Context) { - - } - - func makeCoordinator() -> Coordinator { - Coordinator(self) - } - - class Coordinator: NSObject, NSSearchFieldDelegate { - var parent: MacSearchField - - init(_ parent: MacSearchField) { - self.parent = parent - } - - func searchFieldDidStartSearching(_ sender: NSSearchField) { - // - } - - func searchFieldDidEndSearching(_ sender: NSSearchField) { - // - } - - } - -} diff --git a/Multiplatform/macOS/Preferences/MacPreferencePanes.swift b/Multiplatform/macOS/Preferences/MacPreferencePanes.swift deleted file mode 100644 index 6a5624365..000000000 --- a/Multiplatform/macOS/Preferences/MacPreferencePanes.swift +++ /dev/null @@ -1,28 +0,0 @@ -// -// MacPreferencesView.swift -// macOS -// -// Created by Stuart Breckenridge on 27/6/20. -// - -import SwiftUI - -enum MacPreferencePane: Int, CaseIterable { - case general = 1 - case accounts = 2 - case viewing = 3 - case advanced = 4 - - var description: String { - switch self { - case .general: - return "General" - case .accounts: - return "Accounts" - case .viewing: - return "Appearance" - case .advanced: - return "Advanced" - } - } -} diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/AccountsPreferencesModel.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/AccountsPreferencesModel.swift deleted file mode 100644 index b3853d473..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/AccountsPreferencesModel.swift +++ /dev/null @@ -1,103 +0,0 @@ -// -// AccountsPreferencesModel.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 13/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import Account -import Combine - -public enum AccountConfigurationSheets: Equatable { - case addAccountPicker, addSelectedAccount(AccountType), credentials, none - - public static func == (lhs: AccountConfigurationSheets, rhs: AccountConfigurationSheets) -> Bool { - switch (lhs, rhs) { - case (let .addSelectedAccount(lhsType), let .addSelectedAccount(rhsType)): - return lhsType == rhsType - default: - return false - } - } - -} - -public class AccountsPreferencesModel: ObservableObject { - - // Selected Account - public private(set) var account: Account? - - // All Accounts - @Published var sortedAccounts: [Account] = [] - @Published var selectedConfiguredAccountID: String? = AccountManager.shared.defaultAccount.accountID { - didSet { - if let accountID = selectedConfiguredAccountID { - account = sortedAccounts.first(where: { $0.accountID == accountID }) - accountIsActive = account?.isActive ?? false - accountName = account?.name ?? "" - } - } - } - @Published var showAddAccountView: Bool = false - var selectedAccountIsDefault: Bool { - guard let selected = selectedConfiguredAccountID else { - return true - } - if selected == AccountManager.shared.defaultAccount.accountID { - return true - } - return false - } - - // Edit Account - @Published var accountIsActive: Bool = false { - didSet { - account?.isActive = accountIsActive - } - } - @Published var accountName: String = "" { - didSet { - account?.name = accountName - } - } - - // Sheets - @Published var showSheet: Bool = false - @Published var sheetToShow: AccountConfigurationSheets = .none { - didSet { - if sheetToShow == .none { showSheet = false } else { showSheet = true } - } - } - @Published var showDeleteConfirmation: Bool = false - - // Subscriptions - var cancellables = Set() - - init() { - sortedAccounts = AccountManager.shared.sortedAccounts - - NotificationCenter.default.publisher(for: .UserDidAddAccount).sink { [weak self] _ in - self?.sortedAccounts = AccountManager.shared.sortedAccounts - }.store(in: &cancellables) - - NotificationCenter.default.publisher(for: .UserDidDeleteAccount).sink { [weak self] _ in - self?.selectedConfiguredAccountID = nil - self?.sortedAccounts = AccountManager.shared.sortedAccounts - self?.selectedConfiguredAccountID = AccountManager.shared.defaultAccount.accountID - }.store(in: &cancellables) - - NotificationCenter.default.publisher(for: .AccountStateDidChange).sink { [weak self] notification in - guard let account = notification.object as? Account else { - return - } - if account.accountID == self?.account?.accountID { - self?.account = account - self?.accountIsActive = account.isActive - self?.accountName = account.name ?? "" - } - }.store(in: &cancellables) - } - -} diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/AccountsPreferencesView.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/AccountsPreferencesView.swift deleted file mode 100644 index bd72a621c..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/AccountsPreferencesView.swift +++ /dev/null @@ -1,127 +0,0 @@ -// -// AccountsPreferencesView.swift -// macOS -// -// Created by Stuart Breckenridge on 27/6/20. -// - -import SwiftUI -import Account - - -struct AccountsPreferencesView: View { - - @StateObject var viewModel = AccountsPreferencesModel() - @State private var hoverOnAdd: Bool = false - @State private var hoverOnRemove: Bool = false - - var body: some View { - VStack { - HStack(alignment: .top, spacing: 10) { - listOfAccounts - - AccountDetailView(viewModel: viewModel) - .frame(height: 300, alignment: .leading) - } - Spacer() - } - .sheet(isPresented: $viewModel.showSheet, - onDismiss: { viewModel.sheetToShow = .none }, - content: { - switch viewModel.sheetToShow { - case .addAccountPicker: - AddAccountView(accountToAdd: $viewModel.sheetToShow) - case .credentials: - EditAccountCredentialsView(viewModel: viewModel) - case .none: - EmptyView() - case .addSelectedAccount(let type): - switch type { - case .onMyMac: - AddLocalAccountView() - case .feedbin: - AddFeedbinAccountView() - case .cloudKit: - AddCloudKitAccountView() - case .feedWrangler: - AddFeedWranglerAccountView() - case .newsBlur: - AddNewsBlurAccountView() - case .feedly: - AddFeedlyAccountView() - default: - AddReaderAPIAccountView(accountType: type) - } - } - }) - .alert(isPresented: $viewModel.showDeleteConfirmation, content: { - Alert(title: Text("Delete \(viewModel.account!.nameForDisplay)?"), - message: Text("Are you sure you want to delete the account \"\(viewModel.account!.nameForDisplay)\"? This can not be undone."), - primaryButton: .destructive(Text("Delete"), action: { - AccountManager.shared.deleteAccount(viewModel.account!) - viewModel.showDeleteConfirmation = false - }), - secondaryButton: .cancel({ - viewModel.showDeleteConfirmation = false - })) - }) - } - - var listOfAccounts: some View { - VStack(alignment: .leading) { - List(viewModel.sortedAccounts, id: \.accountID, selection: $viewModel.selectedConfiguredAccountID) { - ConfiguredAccountRow(account: $0) - .id($0.accountID) - }.overlay( - Group { - bottomButtonStack - }, alignment: .bottom) - } - .frame(width: 160, height: 300, alignment: .leading) - .border(Color.gray, width: 1) - } - - var bottomButtonStack: some View { - VStack(alignment: .leading, spacing: 0) { - Divider() - HStack(alignment: .center, spacing: 4) { - Button(action: { - viewModel.sheetToShow = .addAccountPicker - }, label: { - Image(systemName: "plus") - .font(.title) - .frame(width: 30, height: 30) - .overlay(RoundedRectangle(cornerRadius: 4, style: .continuous) - .foregroundColor(hoverOnAdd ? Color.gray.opacity(0.1) : Color.clear)) - .padding(4) - }) - .buttonStyle(BorderlessButtonStyle()) - .onHover { hovering in - hoverOnAdd = hovering - } - .help("Add Account") - - Button(action: { - viewModel.showDeleteConfirmation = true - }, label: { - Image(systemName: "minus") - .font(.title) - .frame(width: 30, height: 30) - .overlay(RoundedRectangle(cornerRadius: 4, style: .continuous) - .foregroundColor(hoverOnRemove ? Color.gray.opacity(0.1) : Color.clear)) - .padding(4) - }) - .buttonStyle(BorderlessButtonStyle()) - .onHover { hovering in - hoverOnRemove = hovering - } - .disabled(viewModel.selectedAccountIsDefault) - .help("Delete Account") - - Spacer() - } - .background(Color.init(.windowBackgroundColor)) - } - } - -} diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountView.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountView.swift deleted file mode 100644 index 4f1001451..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Add Account/AddAccountView.swift +++ /dev/null @@ -1,274 +0,0 @@ -// -// AddAccountView.swift -// NetNewsWire -// -// Created by Stuart Breckenridge on 28/10/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -enum AddAccountSections: Int, CaseIterable { - case local = 0 - case icloud - case web - case selfhosted - case allOrdered - - var sectionHeader: String { - switch self { - case .local: - return NSLocalizedString("Local", comment: "Local Account") - case .icloud: - return NSLocalizedString("iCloud", comment: "iCloud Account") - case .web: - return NSLocalizedString("Web", comment: "Web Account") - case .selfhosted: - return NSLocalizedString("Self-hosted", comment: "Self hosted Account") - case .allOrdered: - return "" - } - } - - var sectionFooter: String { - switch self { - case .local: - return NSLocalizedString("Local accounts do not sync feeds across devices.", comment: "Local Account") - case .icloud: - return NSLocalizedString("Your iCloud account syncs your feeds across your Mac and iOS devices.", comment: "iCloud Account") - case .web: - return NSLocalizedString("Web accounts sync your feeds across all your devices.", comment: "Web Account") - case .selfhosted: - return NSLocalizedString("Self-hosted accounts sync your feeds across all your devices.", comment: "Self hosted Account") - case .allOrdered: - return "" - } - } - - var sectionContent: [AccountType] { - switch self { - case .local: - return [.onMyMac] - case .icloud: - return [.cloudKit] - case .web: - #if DEBUG - return [.bazQux, .feedbin, .feedly, .feedWrangler, .inoreader, .newsBlur, .theOldReader] - #else - return [.bazQux, .feedbin, .feedly, .feedWrangler, .inoreader, .newsBlur, .theOldReader] - #endif - case .selfhosted: - return [.freshRSS] - case .allOrdered: - return AddAccountSections.local.sectionContent + - AddAccountSections.icloud.sectionContent + - AddAccountSections.web.sectionContent + - AddAccountSections.selfhosted.sectionContent - } - } -} - -struct AddAccountView: View { - - @State private var selectedAccount: AccountType = .onMyMac - @Binding public var accountToAdd: AccountConfigurationSheets - @Environment(\.presentationMode) var presentationMode - - var body: some View { - VStack(alignment: .leading, spacing: 8) { - Text("Choose an account type to add...") - .font(.headline) - .padding() - - localAccount - icloudAccount - webAccounts - selfhostedAccounts - - HStack(spacing: 12) { - Spacer() - if #available(OSX 11.0, *) { - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - .frame(width: 80) - }) - .help("Cancel") - .keyboardShortcut(.cancelAction) - - } else { - Button(action: { - presentationMode.wrappedValue.dismiss() - }, label: { - Text("Cancel") - .frame(width: 80) - }) - .accessibility(label: Text("Add Account")) - } - if #available(OSX 11.0, *) { - Button(action: { - presentationMode.wrappedValue.dismiss() - DispatchQueue.main.asyncAfter(deadline: .now() + 0.5, execute: { - accountToAdd = AccountConfigurationSheets.addSelectedAccount(selectedAccount) - }) - }, label: { - Text("Continue") - .frame(width: 80) - }) - .help("Add Account") - .keyboardShortcut(.defaultAction) - - } else { - Button(action: { - accountToAdd = AccountConfigurationSheets.addSelectedAccount(selectedAccount) - presentationMode.wrappedValue.dismiss() - - }, label: { - Text("Continue") - .frame(width: 80) - }) - } - } - .padding(.top, 12) - .padding(.bottom, 4) - } - .pickerStyle(RadioGroupPickerStyle()) - .fixedSize(horizontal: false, vertical: true) - .frame(width: 420) - .padding() - } - - var localAccount: some View { - VStack(alignment: .leading) { - Text("Local") - .font(.headline) - .padding(.horizontal) - - Picker(selection: $selectedAccount, label: Text(""), content: { - ForEach(AddAccountSections.local.sectionContent, id: \.self, content: { account in - HStack(alignment: .center) { - account.image() - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 25, height: 25, alignment: .center) - .padding(.leading, 4) - Text(account.localizedAccountName()) - } - .tag(account) - }) - }) - .pickerStyle(RadioGroupPickerStyle()) - .offset(x: 7.5, y: 0) - - Text(AddAccountSections.local.sectionFooter).foregroundColor(.gray) - .font(.caption) - .padding(.horizontal) - - } - - } - - var icloudAccount: some View { - VStack(alignment: .leading) { - Text("iCloud") - .font(.headline) - .padding(.horizontal) - .padding(.top, 8) - - Picker(selection: $selectedAccount, label: Text(""), content: { - ForEach(AddAccountSections.icloud.sectionContent, id: \.self, content: { account in - HStack(alignment: .center) { - account.image() - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 25, height: 25, alignment: .center) - .padding(.leading, 4) - - Text(account.localizedAccountName()) - } - .tag(account) - }) - }) - .offset(x: 7.5, y: 0) - .disabled(isCloudInUse()) - - Text(AddAccountSections.icloud.sectionFooter).foregroundColor(.gray) - .font(.caption) - .padding(.horizontal) - } - } - - var webAccounts: some View { - VStack(alignment: .leading) { - Text("Web") - .font(.headline) - .padding(.horizontal) - .padding(.top, 8) - - Picker(selection: $selectedAccount, label: Text(""), content: { - ForEach(AddAccountSections.web.sectionContent, id: \.self, content: { account in - - HStack(alignment: .center) { - account.image() - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 25, height: 25, alignment: .center) - .padding(.leading, 4) - - Text(account.localizedAccountName()) - } - .tag(account) - - }) - }) - .offset(x: 7.5, y: 0) - - Text(AddAccountSections.web.sectionFooter).foregroundColor(.gray) - .font(.caption) - .padding(.horizontal) - } - } - - var selfhostedAccounts: some View { - VStack(alignment: .leading) { - Text("Self-hosted") - .font(.headline) - .padding(.horizontal) - .padding(.top, 8) - - Picker(selection: $selectedAccount, label: Text(""), content: { - ForEach(AddAccountSections.selfhosted.sectionContent, id: \.self, content: { account in - HStack(alignment: .center) { - account.image() - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 25, height: 25, alignment: .center) - .padding(.leading, 4) - - Text(account.localizedAccountName()) - }.tag(account) - }) - }) - .offset(x: 7.5, y: 0) - - Text(AddAccountSections.selfhosted.sectionFooter).foregroundColor(.gray) - .font(.caption) - .padding(.horizontal) - } - } - - private func isCloudInUse() -> Bool { - AccountManager.shared.accounts.contains(where: { $0.type == .cloudKit }) - } - - private func isRestricted(_ accountType: AccountType) -> Bool { - if AppDefaults.shared.isDeveloperBuild && (accountType == .feedly || accountType == .feedWrangler || accountType == .inoreader) { - return true - } - return false - } -} - - diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/ConfiguredAccountRow.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/ConfiguredAccountRow.swift deleted file mode 100644 index baee404ba..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/ConfiguredAccountRow.swift +++ /dev/null @@ -1,29 +0,0 @@ -// -// ConfiguredAccountRow.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 13/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account - -struct ConfiguredAccountRow: View { - - var account: Account - - var body: some View { - HStack(alignment: .center) { - if let img = account.smallIcon?.image { - Image(rsImage: img) - .resizable() - .frame(width: 20, height: 20) - .aspectRatio(contentMode: .fit) - } - Text(account.nameForDisplay) - }.padding(.vertical, 4) - } - -} - diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/AccountDetailView.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/AccountDetailView.swift deleted file mode 100644 index f979159ec..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/AccountDetailView.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// AccountDetailView.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 14/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Account -import Combine - -struct AccountDetailView: View { - - @ObservedObject var viewModel: AccountsPreferencesModel - - var body: some View { - ZStack { - RoundedRectangle(cornerRadius: 8, style: .circular) - .foregroundColor(Color.secondary.opacity(0.1)) - .padding(.top, 8) - - VStack { - editAccountHeader - if viewModel.account != nil { - editAccountForm - } - Spacer() - } - } - } - - var editAccountHeader: some View { - HStack { - Spacer() - Button("Account Information", action: {}) - Spacer() - } - .padding([.leading, .trailing, .bottom], 4) - } - - var editAccountForm: some View { - Form(content: { - HStack(alignment: .top) { - Text("Type: ") - .frame(width: 50) - VStack(alignment: .leading) { - Text(viewModel.account!.defaultName) - Toggle("Active", isOn: $viewModel.accountIsActive) - } - } - HStack(alignment: .top) { - Text("Name: ") - .frame(width: 50) - VStack(alignment: .leading) { - TextField(viewModel.account!.name ?? "", text: $viewModel.accountName) - .textFieldStyle(RoundedBorderTextFieldStyle()) - Text("The name appears in the sidebar. It can be anything you want. You can even use emoji. 🎸") - .foregroundColor(.secondary) - } - } - Spacer() - if viewModel.account?.type != .onMyMac { - HStack { - Spacer() - Button("Credentials", action: { - viewModel.sheetToShow = .credentials - }) - Spacer() - } - } - }) - .padding() - - } - -} - -struct AccountDetailView_Previews: PreviewProvider { - static var previews: some View { - AccountDetailView(viewModel: AccountsPreferencesModel()) - } -} diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/EditAccountCredentialsModel.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/EditAccountCredentialsModel.swift deleted file mode 100644 index a96b8e27e..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/EditAccountCredentialsModel.swift +++ /dev/null @@ -1,284 +0,0 @@ -// -// EditAccountCredentialsModel.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 14/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation -import Account -import Secrets -import RSCore - -class EditAccountCredentialsModel: ObservableObject { - - @Published var userName: String = "" - @Published var password: String = "" - @Published var apiUrl: String = "" - @Published var accountIsUpdatingCredentials: Bool = false - @Published var accountCredentialsWereUpdated: Bool = false - @Published var error: AccountUpdateErrors = .none { - didSet { - if error == .none { - showError = false - } else { - showError = true - } - } - } - @Published var showError: Bool = false - - func updateAccountCredentials(_ account: Account) { - switch account.type { - case .onMyMac: - return - case .feedbin: - updateFeedbin(account) - case .cloudKit: - return - case .feedWrangler: - updateFeedWrangler(account) - case .feedly: - updateFeedly(account) - case .freshRSS: - updateReaderAccount(account) - case .newsBlur: - updateNewsblur(account) - case .inoreader: - updateReaderAccount(account) - case .bazQux: - updateReaderAccount(account) - case .theOldReader: - updateReaderAccount(account) - } - } - - func retrieveCredentials(_ account: Account) { - switch account.type { - case .feedbin: - let credentials = try? account.retrieveCredentials(type: .basic) - userName = credentials?.username ?? "" - case .feedWrangler: - let credentials = try? account.retrieveCredentials(type: .feedWranglerBasic) - userName = credentials?.username ?? "" - case .feedly: - return - case .freshRSS: - let credentials = try? account.retrieveCredentials(type: .readerBasic) - userName = credentials?.username ?? "" - case .newsBlur: - let credentials = try? account.retrieveCredentials(type: .newsBlurBasic) - userName = credentials?.username ?? "" - default: - return - } - } - -} - -// MARK:- Update API -extension EditAccountCredentialsModel { - - func updateFeedbin(_ account: Account) { - accountIsUpdatingCredentials = true - let credentials = Credentials(type: .basic, username: userName, secret: password) - - Account.validateCredentials(type: .feedbin, credentials: credentials) { [weak self] result in - - guard let self = self else { return } - - self.accountIsUpdatingCredentials = false - - switch result { - case .success(let validatedCredentials): - - guard let validatedCredentials = validatedCredentials else { - self.error = .invalidUsernamePassword - return - } - - do { - try account.removeCredentials(type: .basic) - try account.storeCredentials(validatedCredentials) - self.accountCredentialsWereUpdated = true - account.refreshAll(completion: { result in - switch result { - case .success: - break - case .failure(let error): - self.error = .other(error: error) - } - }) - - } catch { - self.error = .keyChainError - } - - case .failure: - self.error = .networkError - } - } - } - - func updateFeedWrangler(_ account: Account) { - accountIsUpdatingCredentials = true - let credentials = Credentials(type: .feedWranglerBasic, username: userName, secret: password) - - Account.validateCredentials(type: .feedWrangler, credentials: credentials) { [weak self] result in - - guard let self = self else { return } - - self.accountIsUpdatingCredentials = false - - switch result { - case .success(let validatedCredentials): - - guard let validatedCredentials = validatedCredentials else { - self.error = .invalidUsernamePassword - return - } - - do { - try account.removeCredentials(type: .feedWranglerBasic) - try account.removeCredentials(type: .feedWranglerToken) - try account.storeCredentials(credentials) - try account.storeCredentials(validatedCredentials) - self.accountCredentialsWereUpdated = true - account.refreshAll(completion: { result in - switch result { - case .success: - break - case .failure(let error): - self.error = .other(error: error) - } - }) - - } catch { - self.error = .keyChainError - } - - case .failure: - self.error = .networkError - } - } - } - - func updateFeedly(_ account: Account) { - accountIsUpdatingCredentials = true - let updateAccount = OAuthAccountAuthorizationOperation(accountType: .feedly) - updateAccount.delegate = self - #if os(macOS) - updateAccount.presentationAnchor = NSApplication.shared.windows.last - #endif - MainThreadOperationQueue.shared.add(updateAccount) - } - - func updateReaderAccount(_ account: Account) { - accountIsUpdatingCredentials = true - let credentials = Credentials(type: .readerBasic, username: userName, secret: password) - - Account.validateCredentials(type: account.type, credentials: credentials) { [weak self] result in - - guard let self = self else { return } - - self.accountIsUpdatingCredentials = false - - switch result { - case .success(let validatedCredentials): - - guard let validatedCredentials = validatedCredentials else { - self.error = .invalidUsernamePassword - return - } - - do { - try account.removeCredentials(type: .readerBasic) - try account.removeCredentials(type: .readerAPIKey) - try account.storeCredentials(credentials) - try account.storeCredentials(validatedCredentials) - self.accountCredentialsWereUpdated = true - account.refreshAll(completion: { result in - switch result { - case .success: - break - case .failure(let error): - self.error = .other(error: error) - } - }) - - } catch { - self.error = .keyChainError - } - - case .failure: - self.error = .networkError - } - } - } - - func updateNewsblur(_ account: Account) { - accountIsUpdatingCredentials = true - let credentials = Credentials(type: .newsBlurBasic, username: userName, secret: password) - - Account.validateCredentials(type: .newsBlur, credentials: credentials) { [weak self] result in - - guard let self = self else { return } - - self.accountIsUpdatingCredentials = false - - switch result { - case .success(let validatedCredentials): - - guard let validatedCredentials = validatedCredentials else { - self.error = .invalidUsernamePassword - return - } - - do { - try account.removeCredentials(type: .newsBlurBasic) - try account.removeCredentials(type: .newsBlurSessionId) - try account.storeCredentials(credentials) - try account.storeCredentials(validatedCredentials) - self.accountCredentialsWereUpdated = true - account.refreshAll(completion: { result in - switch result { - case .success: - break - case .failure(let error): - self.error = .other(error: error) - } - }) - - } catch { - self.error = .keyChainError - } - - case .failure: - self.error = .networkError - } - } - } - -} - -// MARK:- OAuthAccountAuthorizationOperationDelegate -extension EditAccountCredentialsModel: OAuthAccountAuthorizationOperationDelegate { - func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) { - accountIsUpdatingCredentials = false - accountCredentialsWereUpdated = true - account.refreshAll { [weak self] result in - switch result { - case .success: - break - case .failure(let error): - self?.error = .other(error: error) - } - } - } - - func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) { - accountIsUpdatingCredentials = false - self.error = .other(error: error) - } -} diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/EditAccountCredentialsView.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/EditAccountCredentialsView.swift deleted file mode 100644 index 34b5dbe04..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/Account Preferences/Edit Account/EditAccountCredentialsView.swift +++ /dev/null @@ -1,94 +0,0 @@ -// -// EditAccountCredentialsView.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 14/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI -import Secrets - -struct EditAccountCredentialsView: View { - - @Environment(\.presentationMode) var presentationMode - @StateObject private var editModel = EditAccountCredentialsModel() - @ObservedObject var viewModel: AccountsPreferencesModel - - var body: some View { - Form { - HStack { - Spacer() - Image(rsImage: viewModel.account!.smallIcon!.image) - .resizable() - .frame(width: 30, height: 30) - Text(viewModel.account?.nameForDisplay ?? "") - Spacer() - }.padding() - - HStack(alignment: .center) { - VStack(alignment: .trailing, spacing: 12) { - Text("Username: ") - Text("Password: ") - if viewModel.account?.type == .freshRSS { - Text("API URL: ") - } - }.frame(width: 75) - - VStack(alignment: .leading, spacing: 12) { - TextField("Username", text: $editModel.userName) - SecureField("Password", text: $editModel.password) - if viewModel.account?.type == .freshRSS { - TextField("API URL", text: $editModel.apiUrl) - } - } - }.textFieldStyle(RoundedBorderTextFieldStyle()) - - Spacer() - HStack{ - if editModel.accountIsUpdatingCredentials { - ProgressView("Updating") - } - Spacer() - Button("Cancel", action: { - presentationMode.wrappedValue.dismiss() - }) - if viewModel.account?.type != .freshRSS { - Button("Update", action: { - editModel.updateAccountCredentials(viewModel.account!) - }).disabled(editModel.userName.count == 0 || editModel.password.count == 0) - } else { - Button("Update", action: { - editModel.updateAccountCredentials(viewModel.account!) - }).disabled(editModel.userName.count == 0 || editModel.password.count == 0 || editModel.apiUrl.count == 0) - } - - } - }.onAppear { - editModel.retrieveCredentials(viewModel.account!) - } - .onChange(of: editModel.accountCredentialsWereUpdated) { value in - if value == true { - viewModel.sheetToShow = .none - presentationMode.wrappedValue.dismiss() - } - } - .alert(isPresented: $editModel.showError) { - Alert(title: Text("Error Adding Account"), - message: Text(editModel.error.description), - dismissButton: .default(Text("Dismiss"), - action: { - editModel.error = .none - })) - } - .frame(idealWidth: 300, idealHeight: 200, alignment: .top) - .padding() - } -} - -struct EditAccountCredentials_Previews: PreviewProvider { - static var previews: some View { - EditAccountCredentialsView(viewModel: AccountsPreferencesModel()) - } -} - diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountUpdateErrors.swift b/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountUpdateErrors.swift deleted file mode 100644 index 0ecec3500..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/Accounts/AccountUpdateErrors.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// AccountUpdateErrors.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 14/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation - -enum AccountUpdateErrors: CustomStringConvertible { - case invalidUsernamePassword, invalidUsernamePasswordAPI, networkError, keyChainError, other(error: Error) , none - - var description: String { - switch self { - case .invalidUsernamePassword: - return NSLocalizedString("Invalid email or password combination.", comment: "Invalid email/password combination.") - case .invalidUsernamePasswordAPI: - return NSLocalizedString("Invalid email, password, or API URL combination.", comment: "Invalid email/password/API combination.") - case .networkError: - return NSLocalizedString("Network Error. Please try later.", comment: "Network Error. Please try later.") - case .keyChainError: - return NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error") - case .other(let error): - return NSLocalizedString(error.localizedDescription, comment: "Other add account error") - default: - return NSLocalizedString("N/A", comment: "N/A") - } - } - - static func ==(lhs: AccountUpdateErrors, rhs: AccountUpdateErrors) -> Bool { - switch (lhs, rhs) { - case (.other(let lhsError), .other(let rhsError)): - return lhsError.localizedDescription == rhsError.localizedDescription - default: - return false - } - } -} diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Advanced/AdvancedPreferencesModel.swift b/Multiplatform/macOS/Preferences/Preference Panes/Advanced/AdvancedPreferencesModel.swift deleted file mode 100644 index 5e6688172..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/Advanced/AdvancedPreferencesModel.swift +++ /dev/null @@ -1,32 +0,0 @@ -// -// AdvancedPreferencesModel.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 16/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation - -class AdvancedPreferencesModel: ObservableObject { - - let releaseBuildsURL = Bundle.main.infoDictionary!["SUFeedURL"]! as! String - let testBuildsURL = Bundle.main.infoDictionary!["FeedURLForTestBuilds"]! as! String - let appcastDefaultsKey = "SUFeedURL" - - init() { - if AppDefaults.shared.downloadTestBuilds == false { - AppDefaults.store.setValue(releaseBuildsURL, forKey: appcastDefaultsKey) - } else { - AppDefaults.store.setValue(testBuildsURL, forKey: appcastDefaultsKey) - } - } - - func updateAppcast() { - if AppDefaults.shared.downloadTestBuilds == false { - AppDefaults.store.setValue(releaseBuildsURL, forKey: appcastDefaultsKey) - } else { - AppDefaults.store.setValue(testBuildsURL, forKey: appcastDefaultsKey) - } - } -} diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Advanced/AdvancedPreferencesView.swift b/Multiplatform/macOS/Preferences/Preference Panes/Advanced/AdvancedPreferencesView.swift deleted file mode 100644 index f065eeefb..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/Advanced/AdvancedPreferencesView.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// AdvancedPreferencesView.swift -// macOS -// -// Created by Stuart Breckenridge on 27/6/20. -// - -import SwiftUI - -struct AdvancedPreferencesView: View { - - @StateObject private var preferences = AppDefaults.shared - @StateObject private var viewModel = AdvancedPreferencesModel() - - var body: some View { - Form { - Toggle("Check for app updates automatically", isOn: $preferences.checkForUpdatesAutomatically) - Toggle("Download Test Builds", isOn: $preferences.downloadTestBuilds) - Text("If you’re not sure, don’t enable test builds. Test builds may have bugs, which may include crashing bugs and data loss.") - .foregroundColor(.secondary) - HStack { - Spacer() - Button("Check for Updates") { - appDelegate.softwareUpdater.checkForUpdates() - } - Spacer() - } - Toggle("Send Crash Logs Automatically", isOn: $preferences.sendCrashLogs) - Divider() - HStack { - Spacer() - Button("Privacy Policy", action: { - NSWorkspace.shared.open(URL(string: "https://netnewswire.com/privacypolicy")!) - }) - Spacer() - } - } - .onChange(of: preferences.downloadTestBuilds, perform: { _ in - viewModel.updateAppcast() - }) - .frame(width: 400, alignment: .center) - .lineLimit(3) - } - -} - diff --git a/Multiplatform/macOS/Preferences/Preference Panes/General/GeneralPreferencesModel.swift b/Multiplatform/macOS/Preferences/Preference Panes/General/GeneralPreferencesModel.swift deleted file mode 100644 index 886d58169..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/General/GeneralPreferencesModel.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// GeneralPreferencesModel.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 12/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import Foundation - - - -class GeneralPreferencesModel: ObservableObject { - - @Published var rssReaders = [RSSReader]() - @Published var readerSelection: Int = 0 { - willSet { - if newValue != readerSelection { - registerAppWithBundleID(rssReaders[newValue].bundleID) - } - } - } - - private let readerInfo = RSSReaderInfo() - - init() { - prepareRSSReaders() - } - -} - -// MARK:- RSS Readers - -private extension GeneralPreferencesModel { - - func prepareRSSReaders() { - - // Populate rssReaders - var thisApp = RSSReader(bundleID: Bundle.main.bundleIdentifier!) - thisApp?.nameMinusAppSuffix.append(" (this app—multiplatform)") - - let otherRSSReaders = readerInfo.rssReaders.filter { $0.bundleID != Bundle.main.bundleIdentifier! }.sorted(by: { $0.nameMinusAppSuffix < $1.nameMinusAppSuffix }) - rssReaders.append(thisApp!) - rssReaders.append(contentsOf: otherRSSReaders) - - if readerInfo.defaultRSSReaderBundleID != nil { - let defaultReader = rssReaders.filter({ $0.bundleID == readerInfo.defaultRSSReaderBundleID }) - if defaultReader.count == 1 { - let reader = defaultReader[0] - readerSelection = rssReaders.firstIndex(of: reader)! - } - } - } - - func registerAppWithBundleID(_ bundleID: String) { - NSWorkspace.shared.setDefaultAppBundleID(forURLScheme: "feed", to: bundleID) - NSWorkspace.shared.setDefaultAppBundleID(forURLScheme: "feeds", to: bundleID) - } - -} - - -// MARK: - RSSReaderInfo - -struct RSSReaderInfo { - - var defaultRSSReaderBundleID: String? { - NSWorkspace.shared.defaultAppBundleID(forURLScheme: RSSReaderInfo.feedURLScheme) - } - let rssReaders: Set - static let feedURLScheme = "feed:" - - init() { - self.rssReaders = RSSReaderInfo.fetchRSSReaders() - } - - static func fetchRSSReaders() -> Set { - let rssReaderBundleIDs = NSWorkspace.shared.bundleIDsForApps(forURLScheme: feedURLScheme) - - var rssReaders = Set() - rssReaderBundleIDs.forEach { (bundleID) in - if let reader = RSSReader(bundleID: bundleID) { - rssReaders.insert(reader) - } - } - return rssReaders - } -} - - -// MARK: - RSSReader - -struct RSSReader: Hashable { - - let bundleID: String - let name: String - var nameMinusAppSuffix: String - let path: String - - init?(bundleID: String) { - guard let path = NSWorkspace.shared.urlForApplication(withBundleIdentifier: bundleID) else { - return nil - } - - self.path = path.path - self.bundleID = bundleID - - let name = (self.path as NSString).lastPathComponent - self.name = name - if name.hasSuffix(".app") { - self.nameMinusAppSuffix = name.stripping(suffix: ".app") - } - else { - self.nameMinusAppSuffix = name - } - } - - // MARK: - Hashable - - func hash(into hasher: inout Hasher) { - hasher.combine(bundleID) - } - - // MARK: - Equatable - - static func ==(lhs: RSSReader, rhs: RSSReader) -> Bool { - return lhs.bundleID == rhs.bundleID - } -} diff --git a/Multiplatform/macOS/Preferences/Preference Panes/General/GeneralPreferencesView.swift b/Multiplatform/macOS/Preferences/Preference Panes/General/GeneralPreferencesView.swift deleted file mode 100644 index 2ad76de9e..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/General/GeneralPreferencesView.swift +++ /dev/null @@ -1,54 +0,0 @@ -// -// GeneralPreferencesView.swift -// macOS -// -// Created by Stuart Breckenridge on 27/6/20. -// - -import SwiftUI - -struct GeneralPreferencesView: View { - - @StateObject private var defaults = AppDefaults.shared - @StateObject private var preferences = GeneralPreferencesModel() - - var body: some View { - Form { - Picker("Refresh feeds:", - selection: $defaults.interval, - content: { - ForEach(RefreshInterval.allCases, content: { interval in - Text(interval.description()) - .tag(interval.rawValue) - }) - }) - - Picker("Default RSS reader:", selection: $preferences.readerSelection, content: { - ForEach(0.. 0 && preferences.rssReaders[index].nameMinusAppSuffix.contains("NetNewsWire") { - Text(preferences.rssReaders[index].nameMinusAppSuffix.appending(" (old version)")) - - } else { - Text(preferences.rssReaders[index].nameMinusAppSuffix) - .tag(index) - } - }) - }) - - Toggle("Confirm when deleting feeds and folders", isOn: $defaults.sidebarConfirmDelete) - - Toggle("Open webpages in background in browser", isOn: $defaults.openInBrowserInBackground) - Toggle("Hide Unread Count in Dock", isOn: $defaults.hideDockUnreadCount) - - Picker("Safari Extension:", - selection: $defaults.subscribeToFeedsInDefaultBrowser, - content: { - Text("Open feeds in NetNewsWire").tag(false) - Text("Open feeds in default news reader").tag(true) - }).pickerStyle(RadioGroupPickerStyle()) - } - .frame(width: 400, alignment: .center) - .lineLimit(2) - } - -} diff --git a/Multiplatform/macOS/Preferences/Preference Panes/Viewing/LayoutPreferencesView.swift b/Multiplatform/macOS/Preferences/Preference Panes/Viewing/LayoutPreferencesView.swift deleted file mode 100644 index abc9e5a2c..000000000 --- a/Multiplatform/macOS/Preferences/Preference Panes/Viewing/LayoutPreferencesView.swift +++ /dev/null @@ -1,99 +0,0 @@ -// -// LayoutPreferencesView.swift -// Multiplatform macOS -// -// Created by Stuart Breckenridge on 17/7/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import SwiftUI - -struct LayoutPreferencesView: View { - - @StateObject private var defaults = AppDefaults.shared - private let colorPalettes = UserInterfaceColorPalette.allCases - private let sampleTitle = "Lorem dolor sed viverra ipsum. Gravida rutrum quisque non tellus. Rutrum tellus pellentesque eu tincidunt tortor. Sed blandit libero volutpat sed cras ornare. Et netus et malesuada fames ac. Ultrices eros in cursus turpis massa tincidunt dui ut ornare. Lacus sed viverra tellus in. Sollicitudin ac orci phasellus egestas. Purus in mollis nunc sed. Sollicitudin ac orci phasellus egestas tellus rutrum tellus pellentesque. Interdum consectetur libero id faucibus nisl tincidunt eget." - - var body: some View { - - VStack { - Form { - Picker("Appearance", selection: $defaults.userInterfaceColorPalette, content: { - ForEach(colorPalettes, id: \.self, content: { - Text($0.description) - }) - }) - - Divider() - - Text("Timeline: ") - Picker("Number of Lines", selection: $defaults.timelineNumberOfLines, content: { - ForEach(1..<6, content: { i in - Text(String(i)) - .tag(Double(i)) - }) - }).padding(.leading, 16) - Slider(value: $defaults.timelineIconDimensions, in: 20...60, step: 10, minimumValueLabel: Text("Small"), maximumValueLabel: Text("Large"), label: { - Text("Icon size") - }).padding(.leading, 16) - - } - - timelineRowPreview - .frame(width: 300) - .padding() - .overlay( - RoundedRectangle(cornerRadius: 8, style: .continuous) - .stroke(Color.gray, lineWidth: 1) - ) - .animation(.default) - - Text("PREVIEW") - .font(.caption) - .tracking(0.3) - Spacer() - - }.frame(width: 400, height: 300, alignment: .center) - - - - } - - - var timelineRowPreview: some View { - HStack(alignment: .top) { - Image(systemName: "circle.fill") - .resizable() - .frame(width: 10, height: 10, alignment: .top) - .foregroundColor(.accentColor) - - Image(systemName: "paperplane.circle") - .resizable() - .frame(width: CGFloat(defaults.timelineIconDimensions), height: CGFloat(defaults.timelineIconDimensions), alignment: .top) - .foregroundColor(.accentColor) - - VStack(alignment: .leading, spacing: 4) { - Text(sampleTitle) - .font(.headline) - .lineLimit(Int(defaults.timelineNumberOfLines)) - HStack { - Text("Feed Name") - .foregroundColor(.secondary) - .font(.footnote) - Spacer() - Text("10:31") - .font(.footnote) - .foregroundColor(.secondary) - } - } - } - } - - -} - -struct SwiftUIView_Previews: PreviewProvider { - static var previews: some View { - LayoutPreferencesView() - } -} diff --git a/Multiplatform/macOS/macOS-dev.entitlements b/Multiplatform/macOS/macOS-dev.entitlements deleted file mode 100644 index 4b0c04750..000000000 --- a/Multiplatform/macOS/macOS-dev.entitlements +++ /dev/null @@ -1,18 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.automation.apple-events - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.temporary-exception.apple-events - - com.red-sweater.marsedit4 - - - diff --git a/Multiplatform/macOS/macOS.entitlements b/Multiplatform/macOS/macOS.entitlements deleted file mode 100644 index cfbbe8b53..000000000 --- a/Multiplatform/macOS/macOS.entitlements +++ /dev/null @@ -1,28 +0,0 @@ - - - - - com.apple.developer.aps-environment - development - com.apple.developer.icloud-container-identifiers - - iCloud.$(ORGANIZATION_IDENTIFIER).NetNewsWire - - com.apple.developer.icloud-services - - CloudKit - - com.apple.security.app-sandbox - - com.apple.security.automation.apple-events - - com.apple.security.files.user-selected.read-write - - com.apple.security.network.client - - com.apple.security.temporary-exception.apple-events - - com.red-sweater.marsedit4 - - - diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 45291f1d1..10d84d58b 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -9,69 +9,19 @@ /* Begin PBXBuildFile section */ 1701E1B52568983D009453D8 /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 1701E1B72568983D009453D8 /* Localizable.strings */; }; 1701E1E725689D1E009453D8 /* Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1701E1E625689D1E009453D8 /* Localized.swift */; }; - 1704053424E5985A00A00787 /* SceneNavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1704053324E5985A00A00787 /* SceneNavigationModel.swift */; }; - 1704053524E5985A00A00787 /* SceneNavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1704053324E5985A00A00787 /* SceneNavigationModel.swift */; }; + 17071EF026F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17071EEF26F8137400F5E71D /* ArticleTheme+Notifications.swift */; }; + 17071EF126F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17071EEF26F8137400F5E71D /* ArticleTheme+Notifications.swift */; }; 1710B9132552354E00679C0D /* AddAccountHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1710B9122552354E00679C0D /* AddAccountHelpView.swift */; }; 1710B9142552354E00679C0D /* AddAccountHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1710B9122552354E00679C0D /* AddAccountHelpView.swift */; }; 1710B929255246F900679C0D /* EnableExtensionPointHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1710B928255246F900679C0D /* EnableExtensionPointHelpView.swift */; }; 1710B92A255246F900679C0D /* EnableExtensionPointHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1710B928255246F900679C0D /* EnableExtensionPointHelpView.swift */; }; - 1717535624BADF33004498C6 /* GeneralPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1717535524BADF33004498C6 /* GeneralPreferencesModel.swift */; }; 17192ADA2567B3D500AAEACA /* RSSparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 17192AD92567B3D500AAEACA /* RSSparkle */; }; 17192AE52567B3FE00AAEACA /* org.sparkle-project.Downloader.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 17192AE12567B3FE00AAEACA /* org.sparkle-project.Downloader.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 17192AE62567B3FE00AAEACA /* org.sparkle-project.InstallerConnection.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 17192AE22567B3FE00AAEACA /* org.sparkle-project.InstallerConnection.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 17192AE72567B3FE00AAEACA /* org.sparkle-project.InstallerLauncher.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 17192AE32567B3FE00AAEACA /* org.sparkle-project.InstallerLauncher.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; 17192AE82567B3FE00AAEACA /* org.sparkle-project.InstallerStatus.xpc in Embed XPC Services */ = {isa = PBXBuildFile; fileRef = 17192AE42567B3FE00AAEACA /* org.sparkle-project.InstallerStatus.xpc */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 171BCB8C24CB08A3006E22D9 /* FixAccountCredentialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BCB8B24CB08A3006E22D9 /* FixAccountCredentialView.swift */; }; - 171BCB8D24CB08A3006E22D9 /* FixAccountCredentialView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171BCB8B24CB08A3006E22D9 /* FixAccountCredentialView.swift */; }; - 171BCBAF24CBBFD8006E22D9 /* EditAccountCredentialsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E33524BD9621000E1E8E /* EditAccountCredentialsModel.swift */; }; - 171BCBB024CBBFFD006E22D9 /* AccountUpdateErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E33724BD97CB000E1E8E /* AccountUpdateErrors.swift */; }; - 172199C924AB228900A31D04 /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172199C824AB228900A31D04 /* SettingsView.swift */; }; - 172199ED24AB2E0100A31D04 /* SafariView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172199EC24AB2E0100A31D04 /* SafariView.swift */; }; - 172199F124AB716900A31D04 /* SidebarToolbarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172199F024AB716900A31D04 /* SidebarToolbarModifier.swift */; }; - 17241249257B8A8A00ACCEBC /* AddFeedlyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17241248257B8A8A00ACCEBC /* AddFeedlyAccountView.swift */; }; - 1724126A257BBEBB00ACCEBC /* AddFeedbinViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17241269257BBEBB00ACCEBC /* AddFeedbinViewModel.swift */; }; - 17241278257BBEE700ACCEBC /* AddFeedlyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17241277257BBEE700ACCEBC /* AddFeedlyViewModel.swift */; }; - 17241280257BBF3E00ACCEBC /* AddFeedWranglerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1724127F257BBF3E00ACCEBC /* AddFeedWranglerViewModel.swift */; }; - 17241288257BBF7000ACCEBC /* AddNewsBlurViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17241287257BBF7000ACCEBC /* AddNewsBlurViewModel.swift */; }; - 17241290257BBFAD00ACCEBC /* AddReaderAPIViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1724128F257BBFAD00ACCEBC /* AddReaderAPIViewModel.swift */; }; - 1724129D257BC01C00ACCEBC /* AddNewsBlurViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17241287257BBF7000ACCEBC /* AddNewsBlurViewModel.swift */; }; - 1724129E257BC01C00ACCEBC /* AddReaderAPIViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1724128F257BBFAD00ACCEBC /* AddReaderAPIViewModel.swift */; }; - 1724129F257BC01C00ACCEBC /* AddFeedlyAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17241248257B8A8A00ACCEBC /* AddFeedlyAccountView.swift */; }; - 172412A0257BC01C00ACCEBC /* AddFeedWranglerAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF98E2AF2578AA5C00F18944 /* AddFeedWranglerAccountView.swift */; }; - 172412A1257BC01C00ACCEBC /* AddReaderAPIAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF98E2C52578AD1B00F18944 /* AddReaderAPIAccountView.swift */; }; - 172412A2257BC01C00ACCEBC /* AddLocalAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17386B792577C4BF0014C8B2 /* AddLocalAccountView.swift */; }; - 172412A3257BC01C00ACCEBC /* AddNewsBlurAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF98E2BD2578AC0000F18944 /* AddNewsBlurAccountView.swift */; }; - 172412A4257BC01C00ACCEBC /* AddFeedlyViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17241277257BBEE700ACCEBC /* AddFeedlyViewModel.swift */; }; - 172412A5257BC01C00ACCEBC /* AddCloudKitAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF98E2992578A73A00F18944 /* AddCloudKitAccountView.swift */; }; - 172412A6257BC01C00ACCEBC /* AddFeedbinViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17241269257BBEBB00ACCEBC /* AddFeedbinViewModel.swift */; }; - 172412A7257BC01C00ACCEBC /* AddFeedbinAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17386BC32577CC600014C8B2 /* AddFeedbinAccountView.swift */; }; - 172412A8257BC01C00ACCEBC /* AddFeedWranglerViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1724127F257BBF3E00ACCEBC /* AddFeedWranglerViewModel.swift */; }; - 172412AF257BC0C300ACCEBC /* AccountType+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173A64162547BE0900267F6E /* AccountType+Helpers.swift */; }; - 1727B39924C1368D00A4DBDC /* LayoutPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1727B39824C1368D00A4DBDC /* LayoutPreferencesView.swift */; }; - 1729529324AA1CAA00D65E66 /* AccountsPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1729529024AA1CAA00D65E66 /* AccountsPreferencesView.swift */; }; - 1729529424AA1CAA00D65E66 /* AdvancedPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1729529124AA1CAA00D65E66 /* AdvancedPreferencesView.swift */; }; - 1729529524AA1CAA00D65E66 /* GeneralPreferencesView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1729529224AA1CAA00D65E66 /* GeneralPreferencesView.swift */; }; - 1729529724AA1CD000D65E66 /* MacPreferencePanes.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1729529624AA1CD000D65E66 /* MacPreferencePanes.swift */; }; - 1729529B24AA1FD200D65E66 /* MacSearchField.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1729529A24AA1FD200D65E66 /* MacSearchField.swift */; }; - 17386B5E2577BC820014C8B2 /* AccountType+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173A64162547BE0900267F6E /* AccountType+Helpers.swift */; }; - 17386B6C2577BD820014C8B2 /* RSSparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 17386B6B2577BD820014C8B2 /* RSSparkle */; }; - 17386B7A2577C4BF0014C8B2 /* AddLocalAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17386B792577C4BF0014C8B2 /* AddLocalAccountView.swift */; }; - 17386B952577C6240014C8B2 /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 17386B942577C6240014C8B2 /* RSCore */; }; - 17386B962577C6240014C8B2 /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17386B942577C6240014C8B2 /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 17386B982577C6240014C8B2 /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 17386B972577C6240014C8B2 /* RSTree */; }; - 17386B992577C6240014C8B2 /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17386B972577C6240014C8B2 /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 17386B9B2577C6240014C8B2 /* RSWeb in Frameworks */ = {isa = PBXBuildFile; productRef = 17386B9A2577C6240014C8B2 /* RSWeb */; }; - 17386B9C2577C6240014C8B2 /* RSWeb in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17386B9A2577C6240014C8B2 /* RSWeb */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 17386B9E2577C6240014C8B2 /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 17386B9D2577C6240014C8B2 /* RSDatabase */; }; - 17386B9F2577C6240014C8B2 /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17386B9D2577C6240014C8B2 /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 17386BA42577C6240014C8B2 /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = 17386BA32577C6240014C8B2 /* RSParser */; }; - 17386BA52577C6240014C8B2 /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17386BA32577C6240014C8B2 /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 17386BB62577C7340014C8B2 /* RSCoreResources in Frameworks */ = {isa = PBXBuildFile; productRef = 17386BB52577C7340014C8B2 /* RSCoreResources */; }; - 17386BC42577CC600014C8B2 /* AddFeedbinAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17386BC32577CC600014C8B2 /* AddFeedbinAccountView.swift */; }; 173A64172547BE0900267F6E /* AccountType+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173A64162547BE0900267F6E /* AccountType+Helpers.swift */; }; 173A642C2547BE9600267F6E /* AccountType+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173A64162547BE0900267F6E /* AccountType+Helpers.swift */; }; - 175942AA24AD533200585066 /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; }; - 175942AB24AD533200585066 /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; }; 176813D02564BA5900D98635 /* WidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176813B62564B9F800D98635 /* WidgetData.swift */; }; 176813D12564BA5900D98635 /* WidgetDataDecoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176813C92564BA5400D98635 /* WidgetDataDecoder.swift */; }; 176813D22564BA5900D98635 /* WidgetDataEncoder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176813BD2564BA2800D98635 /* WidgetDataEncoder.swift */; }; @@ -92,56 +42,23 @@ 176814652564BD7F00D98635 /* WidgetData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176813B62564B9F800D98635 /* WidgetData.swift */; }; 1768146C2564BD8100D98635 /* WidgetDeepLinks.swift in Sources */ = {isa = PBXBuildFile; fileRef = 176813D82564BA8700D98635 /* WidgetDeepLinks.swift */; }; 1768147B2564BE5400D98635 /* widget-sample.json in Resources */ = {isa = PBXBuildFile; fileRef = 1768147A2564BE5400D98635 /* widget-sample.json */; }; - 1769E32224BC5925000E1E8E /* AccountsPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E32124BC5925000E1E8E /* AccountsPreferencesModel.swift */; }; - 1769E32524BC5A65000E1E8E /* AddAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E32424BC5A65000E1E8E /* AddAccountView.swift */; }; - 1769E32B24BCB030000E1E8E /* ConfiguredAccountRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E32A24BCB030000E1E8E /* ConfiguredAccountRow.swift */; }; - 1769E32D24BD20A0000E1E8E /* AccountDetailView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E32C24BD20A0000E1E8E /* AccountDetailView.swift */; }; - 1769E33024BD6271000E1E8E /* EditAccountCredentialsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E32F24BD6271000E1E8E /* EditAccountCredentialsView.swift */; }; - 1769E33624BD9621000E1E8E /* EditAccountCredentialsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E33524BD9621000E1E8E /* EditAccountCredentialsModel.swift */; }; - 1769E33824BD97CB000E1E8E /* AccountUpdateErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E33724BD97CB000E1E8E /* AccountUpdateErrors.swift */; }; - 1776E88E24AC5F8A00E78166 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1776E88D24AC5F8A00E78166 /* AppDefaults.swift */; }; - 1776E88F24AC5F8A00E78166 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1776E88D24AC5F8A00E78166 /* AppDefaults.swift */; }; 177A0C2D25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177A0C2C25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift */; }; - 17897ACA24C281A40014BA03 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17897AC924C281A40014BA03 /* InspectorView.swift */; }; - 17897ACB24C281A40014BA03 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17897AC924C281A40014BA03 /* InspectorView.swift */; }; 178A9F9D2549449F00AB7E9D /* AddAccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178A9F9C2549449F00AB7E9D /* AddAccountsView.swift */; }; 178A9F9E2549449F00AB7E9D /* AddAccountsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 178A9F9C2549449F00AB7E9D /* AddAccountsView.swift */; }; - 17930ED424AF10EE00A9BA52 /* AddWebFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17930ED324AF10EE00A9BA52 /* AddWebFeedView.swift */; }; - 17930ED524AF10EE00A9BA52 /* AddWebFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17930ED324AF10EE00A9BA52 /* AddWebFeedView.swift */; }; - 1799E6A924C2F93F00511E91 /* InspectorPlatformModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1799E6A824C2F93F00511E91 /* InspectorPlatformModifier.swift */; }; - 1799E6AA24C2F93F00511E91 /* InspectorPlatformModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1799E6A824C2F93F00511E91 /* InspectorPlatformModifier.swift */; }; - 1799E6CD24C320D600511E91 /* InspectorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1799E6CC24C320D600511E91 /* InspectorModel.swift */; }; - 1799E6CE24C320D600511E91 /* InspectorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1799E6CC24C320D600511E91 /* InspectorModel.swift */; }; + 179C39EA26F76B0500D4E741 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = 179C39E926F76B0500D4E741 /* Zip */; }; + 179C39EB26F76B3800D4E741 /* ArticleThemePlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */; }; + 179D280B26F6F93D003B2E0A /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = 179D280A26F6F93D003B2E0A /* Zip */; }; + 179D280D26F73D83003B2E0A /* ArticleThemePlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */; }; 179DB1DFBCF9177104B12E0F /* AccountsNewsBlurWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DBBA2B22A659F81EED6F9 /* AccountsNewsBlurWindowController.swift */; }; 179DB3CE822BFCC2D774D9F4 /* AccountsNewsBlurWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DBBA2B22A659F81EED6F9 /* AccountsNewsBlurWindowController.swift */; }; - 17A1597C24E3DEDD005DA32A /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 17A1597B24E3DEDD005DA32A /* RSCore */; }; - 17A1597D24E3DEDD005DA32A /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17A1597B24E3DEDD005DA32A /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 17A1597F24E3DEDD005DA32A /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 17A1597E24E3DEDD005DA32A /* RSTree */; }; - 17A1598024E3DEDD005DA32A /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17A1597E24E3DEDD005DA32A /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 17A1598224E3DEDD005DA32A /* RSWeb in Frameworks */ = {isa = PBXBuildFile; productRef = 17A1598124E3DEDD005DA32A /* RSWeb */; }; - 17A1598324E3DEDD005DA32A /* RSWeb in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17A1598124E3DEDD005DA32A /* RSWeb */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 17A1598524E3DEDD005DA32A /* RSDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 17A1598424E3DEDD005DA32A /* RSDatabase */; }; - 17A1598624E3DEDD005DA32A /* RSDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17A1598424E3DEDD005DA32A /* RSDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 17A1598824E3DEDD005DA32A /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = 17A1598724E3DEDD005DA32A /* RSParser */; }; - 17A1598924E3DEDD005DA32A /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17A1598724E3DEDD005DA32A /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 17D0682C2564F47E00C0B37E /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = 17D0682B2564F47E00C0B37E /* Localizable.stringsdict */; }; - 17D232A824AFF10A0005F075 /* AddWebFeedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D232A724AFF10A0005F075 /* AddWebFeedModel.swift */; }; - 17D232A924AFF10A0005F075 /* AddWebFeedModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D232A724AFF10A0005F075 /* AddWebFeedModel.swift */; }; - 17D3CEE3257C4D2300E74939 /* AddAccountSignUp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D3CEE2257C4D2300E74939 /* AddAccountSignUp.swift */; }; - 17D3CEE4257C4D2300E74939 /* AddAccountSignUp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D3CEE2257C4D2300E74939 /* AddAccountSignUp.swift */; }; - 17D5F17124B0BC6700375168 /* SidebarToolbarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */; }; - 17D5F17224B0BC6700375168 /* SidebarToolbarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */; }; - 17D5F19524B0C1DD00375168 /* SidebarToolbarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172199F024AB716900A31D04 /* SidebarToolbarModifier.swift */; }; + 17D643B126F8A436008D4C05 /* ArticleThemeDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D643B026F8A436008D4C05 /* ArticleThemeDownloader.swift */; }; + 17D643B226F8A436008D4C05 /* ArticleThemeDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D643B026F8A436008D4C05 /* ArticleThemeDownloader.swift */; }; 17D7586F2679C21800B17787 /* OnePasswordExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = 17D7586E2679C21800B17787 /* OnePasswordExtension.m */; }; 17E0084625941887000C23F0 /* SizeCategories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E0084525941887000C23F0 /* SizeCategories.swift */; }; - 17E4DBD624BFC53E00FE462A /* AdvancedPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E4DBD524BFC53E00FE462A /* AdvancedPreferencesModel.swift */; }; 17EF6A2125C4E5B4002C9F81 /* RSWeb in Frameworks */ = {isa = PBXBuildFile; productRef = 17EF6A2025C4E5B4002C9F81 /* RSWeb */; }; 17EF6A2225C4E5B4002C9F81 /* RSWeb in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 17EF6A2025C4E5B4002C9F81 /* RSWeb */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 27B86EEB25A53AAB00264340 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F4A24D343A500E90810 /* Account */; }; - 27B86EEC25A53AAB00264340 /* Articles in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0080E25936DF6000C23F0 /* Articles */; }; - 27B86EED25A53AAB00264340 /* ArticlesDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0081125936DF6000C23F0 /* ArticlesDatabase */; }; - 27B86EEE25A53AAB00264340 /* Secrets in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0081425936DFF000C23F0 /* Secrets */; }; - 27B86EEF25A53AAB00264340 /* SyncDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 17E0081725936DFF000C23F0 /* SyncDatabase */; }; 3B3A32A5238B820900314204 /* FeedWranglerAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B3A328B238B820900314204 /* FeedWranglerAccountViewController.swift */; }; 3B826DCB2385C84800FC1ADB /* AccountsFeedWrangler.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3B826DB02385C84800FC1ADB /* AccountsFeedWrangler.xib */; }; 3B826DCC2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B826DCA2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift */; }; @@ -186,6 +103,7 @@ 510C418624E5D1B4008226FD /* ExtensionFeedAddRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */; }; 510C43F7243D035C009F70C3 /* ExtensionPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510C43F6243D035C009F70C3 /* ExtensionPoint.swift */; }; 510C43F8243D035C009F70C3 /* ExtensionPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510C43F6243D035C009F70C3 /* ExtensionPoint.swift */; }; + 510FFAB326EEA22C00F32265 /* ArticleThemesTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510FFAB226EEA22C00F32265 /* ArticleThemesTableViewController.swift */; }; 51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */; }; 51107746243BEE2500D97C8C /* ExtensionPointPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51107745243BEE2500D97C8C /* ExtensionPointPreferencesViewController.swift */; }; 51107747243BEE2500D97C8C /* ExtensionPointPreferencesViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51107745243BEE2500D97C8C /* ExtensionPointPreferencesViewController.swift */; }; @@ -215,7 +133,6 @@ 512392C424E3451400F11704 /* TwitterSelectTypeTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510289D12451BC1F00426DDF /* TwitterSelectTypeTableViewController.swift */; }; 512392C524E3451400F11704 /* TwitterEnterDetailTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BEB22C2451E8340066DEDD /* TwitterEnterDetailTableViewController.swift */; }; 512392C624E3451400F11704 /* TwitterSelectAccountTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510289D52451DDD100426DDF /* TwitterSelectAccountTableViewController.swift */; }; - 5125E6CA24AE461D002A7562 /* TimelineLayoutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17B223DB24AC24D2001E4592 /* TimelineLayoutView.swift */; }; 5126EE97226CB48A00C22AFC /* SceneCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5126EE96226CB48A00C22AFC /* SceneCoordinator.swift */; }; 5127B238222B4849006D641D /* DetailKeyboardDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5127B236222B4849006D641D /* DetailKeyboardDelegate.swift */; }; 5127B23A222B4849006D641D /* DetailKeyboardShortcuts.plist in Resources */ = {isa = PBXBuildFile; fileRef = 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */; }; @@ -247,6 +164,10 @@ 51333D1724685D2E00EB5C91 /* AddRedditFeedWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51333D1524685D2E00EB5C91 /* AddRedditFeedWindowController.swift */; }; 51333D3B2468615D00EB5C91 /* AddRedditFeedSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51333D392468615D00EB5C91 /* AddRedditFeedSheet.xib */; }; 51333D3C2468615D00EB5C91 /* AddRedditFeedSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51333D392468615D00EB5C91 /* AddRedditFeedSheet.xib */; }; + 5137C2E426F3F52D009EFEDB /* Sepia.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 5137C2E326F3F52D009EFEDB /* Sepia.nnwtheme */; }; + 5137C2E526F3F52D009EFEDB /* Sepia.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 5137C2E326F3F52D009EFEDB /* Sepia.nnwtheme */; }; + 5137C2E626F3F52D009EFEDB /* Sepia.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 5137C2E326F3F52D009EFEDB /* Sepia.nnwtheme */; }; + 5137C2EA26F63AE6009EFEDB /* ArticleThemeImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5137C2E926F63AE6009EFEDB /* ArticleThemeImporter.swift */; }; 51386A8E25673277005F3762 /* AccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51386A8D25673276005F3762 /* AccountCell.swift */; }; 51386A8F25673277005F3762 /* AccountCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51386A8D25673276005F3762 /* AccountCell.swift */; }; 5138E93A24D33E5600AFF0FE /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E93924D33E5600AFF0FE /* RSTree */; }; @@ -259,12 +180,9 @@ 5138E95324D3418100AFF0FE /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E95124D3418100AFF0FE /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 5138E95824D3419000AFF0FE /* RSWeb in Frameworks */ = {isa = PBXBuildFile; productRef = 5138E95724D3419000AFF0FE /* RSWeb */; }; 5138E95924D3419000AFF0FE /* RSWeb in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 5138E95724D3419000AFF0FE /* RSWeb */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 51392D1B24AC19A000BE0D35 /* SidebarExpandedContainers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51392D1A24AC19A000BE0D35 /* SidebarExpandedContainers.swift */; }; - 51392D1C24AC19A000BE0D35 /* SidebarExpandedContainers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51392D1A24AC19A000BE0D35 /* SidebarExpandedContainers.swift */; }; 513C5CE9232571C2003D4054 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513C5CE8232571C2003D4054 /* ShareViewController.swift */; }; 513C5CEC232571C2003D4054 /* MainInterface.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 513C5CEA232571C2003D4054 /* MainInterface.storyboard */; }; 513C5CF0232571C2003D4054 /* NetNewsWire iOS Share Extension.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 513CCF2524880C1500C55709 /* MasterFeedTableViewIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 513CCF08248808BA00C55709 /* MasterFeedTableViewIdentifier.swift */; }; 513F325C2593ECF40003048F /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 513F325B2593ECF40003048F /* RSCore */; }; 513F325D2593ECF40003048F /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 513F325B2593ECF40003048F /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 513F32712593EE6F0003048F /* Articles in Frameworks */ = {isa = PBXBuildFile; productRef = 513F32702593EE6F0003048F /* Articles */; }; @@ -278,8 +196,6 @@ 513F32812593EF180003048F /* Account in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 516B695E24D2F33B00B5702F /* Account */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 513F32882593EF8F0003048F /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 513F32872593EF8F0003048F /* RSCore */; }; 513F32892593EF8F0003048F /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 513F32872593EF8F0003048F /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 51408B7E24A9EC6F0073CF4E /* SidebarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51408B7D24A9EC6F0073CF4E /* SidebarItem.swift */; }; - 51408B7F24A9EC6F0073CF4E /* SidebarItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51408B7D24A9EC6F0073CF4E /* SidebarItem.swift */; }; 5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */; }; 5142192A23522B5500E07E2C /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5142192923522B5500E07E2C /* ImageViewController.swift */; }; 514219372352510100E07E2C /* ImageScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514219362352510100E07E2C /* ImageScrollView.swift */; }; @@ -303,16 +219,6 @@ 514C16DE24D2EF15009A3AFA /* RSTree in Frameworks */ = {isa = PBXBuildFile; productRef = 514C16DD24D2EF15009A3AFA /* RSTree */; }; 514C16DF24D2EF15009A3AFA /* RSTree in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 514C16DD24D2EF15009A3AFA /* RSTree */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 514C16E124D2EF38009A3AFA /* RSCoreResources in Frameworks */ = {isa = PBXBuildFile; productRef = 514C16E024D2EF38009A3AFA /* RSCoreResources */; }; - 514E6BDA24ACEA0400AC6F6E /* TimelineItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */; }; - 514E6BDB24ACEA0400AC6F6E /* TimelineItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */; }; - 514E6BFF24AD255D00AC6F6E /* PreviewArticles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6BFE24AD255D00AC6F6E /* PreviewArticles.swift */; }; - 514E6C0024AD255D00AC6F6E /* PreviewArticles.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6BFE24AD255D00AC6F6E /* PreviewArticles.swift */; }; - 514E6C0224AD29A300AC6F6E /* TimelineItemStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6C0124AD29A300AC6F6E /* TimelineItemStatusView.swift */; }; - 514E6C0324AD29A300AC6F6E /* TimelineItemStatusView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6C0124AD29A300AC6F6E /* TimelineItemStatusView.swift */; }; - 514E6C0624AD2B5F00AC6F6E /* Image-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6C0524AD2B5F00AC6F6E /* Image-Extensions.swift */; }; - 514E6C0724AD2B5F00AC6F6E /* Image-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6C0524AD2B5F00AC6F6E /* Image-Extensions.swift */; }; - 514E6C0924AD39AD00AC6F6E /* ArticleIconImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6C0824AD39AD00AC6F6E /* ArticleIconImageLoader.swift */; }; - 514E6C0A24AD39AD00AC6F6E /* ArticleIconImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 514E6C0824AD39AD00AC6F6E /* ArticleIconImageLoader.swift */; }; 5154368B229404D1005E1CDF /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; }; 515A50E6243D07A90089E588 /* ExtensionPointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */; }; 515A50E7243D07A90089E588 /* ExtensionPointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */; }; @@ -335,7 +241,6 @@ 516244E3241E19F000B61C47 /* ColorPaletteTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516244E2241E19F000B61C47 /* ColorPaletteTableViewController.swift */; }; 51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */; }; 51627A6923861DED007B3B4B /* MasterFeedViewController+Drop.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */; }; - 51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */; }; 51627A93238A3836007B3B4B /* CroppingPreviewParameters.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */; }; 516A093723609A3600EAE89B /* SettingsComboTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 516A091D23609A3600EAE89B /* SettingsComboTableViewCell.xib */; }; 516A09392360A2AE00EAE89B /* SettingsComboTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516A09382360A2AE00EAE89B /* SettingsComboTableViewCell.swift */; }; @@ -345,52 +250,12 @@ 516AE9B32371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE9B22371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift */; }; 516AE9DF2372269A007DEEAA /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE9DE2372269A007DEEAA /* IconImage.swift */; }; 516AE9E02372269A007DEEAA /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE9DE2372269A007DEEAA /* IconImage.swift */; }; - 516B695B24D2F28600B5702F /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 516B695A24D2F28600B5702F /* Account */; }; - 516B695D24D2F28E00B5702F /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 516B695C24D2F28E00B5702F /* Account */; }; 516B695F24D2F33B00B5702F /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 516B695E24D2F33B00B5702F /* Account */; }; 51707439232AA97100A461A3 /* ShareFolderPickerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */; }; - 5171B4D424B7BABA00FB8D3B /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; - 5171B4F624B7BABA00FB8D3B /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; 517630042336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; 517630052336215100E15FFF /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; 517630232336657E00E15FFF /* WebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517630222336657E00E15FFF /* WebViewProvider.swift */; }; - 5177470324B2657F00EB0F74 /* TimelineToolbarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177470224B2657F00EB0F74 /* TimelineToolbarModifier.swift */; }; - 5177470424B2657F00EB0F74 /* TimelineToolbarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177470224B2657F00EB0F74 /* TimelineToolbarModifier.swift */; }; - 5177470624B2910300EB0F74 /* ArticleToolbarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177470524B2910300EB0F74 /* ArticleToolbarModifier.swift */; }; - 5177470724B2910300EB0F74 /* ArticleToolbarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177470524B2910300EB0F74 /* ArticleToolbarModifier.swift */; }; - 5177470924B2F87600EB0F74 /* SidebarListStyleModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177470824B2F87600EB0F74 /* SidebarListStyleModifier.swift */; }; - 5177470A24B2F87600EB0F74 /* SidebarListStyleModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177470824B2F87600EB0F74 /* SidebarListStyleModifier.swift */; }; - 5177470E24B2FF6F00EB0F74 /* ArticleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177470D24B2FF6F00EB0F74 /* ArticleView.swift */; }; - 5177471024B3029400EB0F74 /* ArticleViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177470F24B3029400EB0F74 /* ArticleViewController.swift */; }; - 5177471224B37C5400EB0F74 /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177471124B37C5400EB0F74 /* WebViewController.swift */; }; - 5177471424B37D4000EB0F74 /* PreloadedWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177471324B37D4000EB0F74 /* PreloadedWebView.swift */; }; - 5177471624B37D9700EB0F74 /* ArticleIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177471524B37D9700EB0F74 /* ArticleIconSchemeHandler.swift */; }; - 5177471824B3812200EB0F74 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177471724B3812200EB0F74 /* IconView.swift */; }; - 5177471A24B3863000EB0F74 /* WebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177471924B3863000EB0F74 /* WebViewProvider.swift */; }; - 5177471C24B387AC00EB0F74 /* ImageScrollView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177471B24B387AC00EB0F74 /* ImageScrollView.swift */; }; - 5177471E24B387E100EB0F74 /* ImageTransition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177471D24B387E100EB0F74 /* ImageTransition.swift */; }; - 5177472024B3882600EB0F74 /* ImageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177471F24B3882600EB0F74 /* ImageViewController.swift */; }; - 5177472224B38CAE00EB0F74 /* ArticleExtractorButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177472124B38CAE00EB0F74 /* ArticleExtractorButtonState.swift */; }; - 5177475C24B39AD500EB0F74 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 5177475824B39AD400EB0F74 /* Credits.rtf */; }; - 5177475D24B39AD500EB0F74 /* Dedication.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 5177475924B39AD400EB0F74 /* Dedication.rtf */; }; - 5177475E24B39AD500EB0F74 /* Thanks.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 5177475A24B39AD500EB0F74 /* Thanks.rtf */; }; - 5177475F24B39AD500EB0F74 /* About.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 5177475B24B39AD500EB0F74 /* About.rtf */; }; - 5177476224B3BC4700EB0F74 /* SettingsAboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177476024B3BC4700EB0F74 /* SettingsAboutView.swift */; }; - 5177476524B3BDAE00EB0F74 /* AttributedStringView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177476424B3BDAE00EB0F74 /* AttributedStringView.swift */; }; - 5177476724B3BE3400EB0F74 /* SettingsAboutModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177476624B3BE3400EB0F74 /* SettingsAboutModel.swift */; }; 517A745B2443665000B553B9 /* UIPageViewController-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517A745A2443665000B553B9 /* UIPageViewController-Extensions.swift */; }; - 517B2EBC24B3E62A001AC46C /* WrapperScriptMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517B2EBB24B3E62A001AC46C /* WrapperScriptMessageHandler.swift */; }; - 517B2EE224B3E8FE001AC46C /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 517B2EDE24B3E8FE001AC46C /* page.html */; }; - 517B2EE324B3E8FE001AC46C /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 517B2EDE24B3E8FE001AC46C /* page.html */; }; - 517B2EE424B3E8FE001AC46C /* blank.html in Resources */ = {isa = PBXBuildFile; fileRef = 517B2EDF24B3E8FE001AC46C /* blank.html */; }; - 517B2EE524B3E8FE001AC46C /* blank.html in Resources */ = {isa = PBXBuildFile; fileRef = 517B2EDF24B3E8FE001AC46C /* blank.html */; }; - 517B2EE624B3E8FE001AC46C /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 517B2EE024B3E8FE001AC46C /* styleSheet.css */; }; - 517B2EE724B3E8FE001AC46C /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 517B2EE024B3E8FE001AC46C /* styleSheet.css */; }; - 517B2EE824B3E8FE001AC46C /* main_multiplatform.js in Resources */ = {isa = PBXBuildFile; fileRef = 517B2EE124B3E8FE001AC46C /* main_multiplatform.js */; }; - 517B2EE924B3E8FE001AC46C /* main_multiplatform.js in Resources */ = {isa = PBXBuildFile; fileRef = 517B2EE124B3E8FE001AC46C /* main_multiplatform.js */; }; - 5181C5AD24AF89B1002E0F70 /* PreferredColorSchemeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5181C5AC24AF89B1002E0F70 /* PreferredColorSchemeModifier.swift */; }; - 5181C5AE24AF89B1002E0F70 /* PreferredColorSchemeModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5181C5AC24AF89B1002E0F70 /* PreferredColorSchemeModifier.swift */; }; - 5181C66224B0C326002E0F70 /* SettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5181C66124B0C326002E0F70 /* SettingsModel.swift */; }; 5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */; }; 5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */; }; 5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; }; @@ -407,24 +272,6 @@ 518C3193237B00D9004D740F /* DetailIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7552374A2890013FF27 /* DetailIconSchemeHandler.swift */; }; 518C3194237B00DA004D740F /* DetailIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5141E7552374A2890013FF27 /* DetailIconSchemeHandler.swift */; }; 518ED21D23D0F26000E0A862 /* UIViewController-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 518ED21C23D0F26000E0A862 /* UIViewController-Extensions.swift */; }; - 51919FA624AA64B000541E64 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FA524AA64B000541E64 /* SidebarView.swift */; }; - 51919FA724AA64B000541E64 /* SidebarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FA524AA64B000541E64 /* SidebarView.swift */; }; - 51919FAC24AA8CCA00541E64 /* UnreadCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FAB24AA8CCA00541E64 /* UnreadCountView.swift */; }; - 51919FAD24AA8CCA00541E64 /* UnreadCountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FAB24AA8CCA00541E64 /* UnreadCountView.swift */; }; - 51919FAF24AA8EFA00541E64 /* SidebarItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FAE24AA8EFA00541E64 /* SidebarItemView.swift */; }; - 51919FB024AA8EFA00541E64 /* SidebarItemView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FAE24AA8EFA00541E64 /* SidebarItemView.swift */; }; - 51919FB324AAB97900541E64 /* FeedIconImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FB224AAB97900541E64 /* FeedIconImageLoader.swift */; }; - 51919FB424AAB97900541E64 /* FeedIconImageLoader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FB224AAB97900541E64 /* FeedIconImageLoader.swift */; }; - 51919FB624AABCA100541E64 /* IconImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FB524AABCA100541E64 /* IconImageView.swift */; }; - 51919FB724AABCA100541E64 /* IconImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FB524AABCA100541E64 /* IconImageView.swift */; }; - 51919FEE24AB85E400541E64 /* TimelineContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FED24AB85E400541E64 /* TimelineContainerView.swift */; }; - 51919FEF24AB85E400541E64 /* TimelineContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FED24AB85E400541E64 /* TimelineContainerView.swift */; }; - 51919FF124AB864A00541E64 /* TimelineModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FF024AB864A00541E64 /* TimelineModel.swift */; }; - 51919FF224AB864A00541E64 /* TimelineModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FF024AB864A00541E64 /* TimelineModel.swift */; }; - 51919FF424AB869C00541E64 /* TimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FF324AB869C00541E64 /* TimelineItem.swift */; }; - 51919FF524AB869C00541E64 /* TimelineItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FF324AB869C00541E64 /* TimelineItem.swift */; }; - 51919FF724AB8B7700541E64 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FF624AB8B7700541E64 /* TimelineView.swift */; }; - 51919FF824AB8B7700541E64 /* TimelineView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51919FF624AB8B7700541E64 /* TimelineView.swift */; }; 51934CCB230F599B006127BE /* InteractiveNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CC1230F5963006127BE /* InteractiveNavigationController.swift */; }; 51934CCE2310792F006127BE /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; }; 51938DF2231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; }; @@ -432,9 +279,8 @@ 5193CD58245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; }; 5193CD59245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; }; 5193CD5A245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; }; - 5194736E24BBB937001A2939 /* HiddenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5194736D24BBB937001A2939 /* HiddenModifier.swift */; }; - 5194736F24BBB937001A2939 /* HiddenModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5194736D24BBB937001A2939 /* HiddenModifier.swift */; }; - 5194737124BBCAF4001A2939 /* TimelineSortOrderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5194737024BBCAF4001A2939 /* TimelineSortOrderView.swift */; }; + 5195C1DA2720205F00888867 /* ShadowTableChanges.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5195C1D92720205F00888867 /* ShadowTableChanges.swift */; }; + 5195C1DC2720BD3000888867 /* MasterFeedRowIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5195C1DB2720BD3000888867 /* MasterFeedRowIdentifier.swift */; }; 519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; }; 519CA8E525841DB700EB079A /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 519CA8E425841DB700EB079A /* CrashReporter */; }; 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519E743422C663F900A78E47 /* SceneDelegate.swift */; }; @@ -450,8 +296,6 @@ 51A1699D235E10D700EB091F /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16993235E10D600EB091F /* SettingsViewController.swift */; }; 51A1699F235E10D700EB091F /* AboutViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16995235E10D600EB091F /* AboutViewController.swift */; }; 51A169A0235E10D700EB091F /* FeedbinAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */; }; - 51A5769624AE617200078888 /* ArticleContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A5769524AE617200078888 /* ArticleContainerView.swift */; }; - 51A5769724AE617200078888 /* ArticleContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A5769524AE617200078888 /* ArticleContainerView.swift */; }; 51A66685238075AE00CB272D /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; }; 51A737AE24DB19730015FA66 /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 51A737AD24DB19730015FA66 /* RSCore */; }; 51A737AF24DB19730015FA66 /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51A737AD24DB19730015FA66 /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; @@ -461,16 +305,6 @@ 51A737C624DB19B50015FA66 /* RSWeb in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51A737C424DB19B50015FA66 /* RSWeb */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 51A737C824DB19CC0015FA66 /* RSParser in Frameworks */ = {isa = PBXBuildFile; productRef = 51A737C724DB19CC0015FA66 /* RSParser */; }; 51A737C924DB19CC0015FA66 /* RSParser in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51A737C724DB19CC0015FA66 /* RSParser */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 51A8001224CA0FC700F41F1D /* Sink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8001124CA0FC700F41F1D /* Sink.swift */; }; - 51A8001324CA0FC700F41F1D /* Sink.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8001124CA0FC700F41F1D /* Sink.swift */; }; - 51A8001524CA0FEC00F41F1D /* DemandBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8001424CA0FEC00F41F1D /* DemandBuffer.swift */; }; - 51A8001624CA0FEC00F41F1D /* DemandBuffer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8001424CA0FEC00F41F1D /* DemandBuffer.swift */; }; - 51A8002D24CC451500F41F1D /* ShareReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8002C24CC451500F41F1D /* ShareReplay.swift */; }; - 51A8002E24CC451600F41F1D /* ShareReplay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8002C24CC451500F41F1D /* ShareReplay.swift */; }; - 51A8005124CC453C00F41F1D /* ReplaySubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8005024CC453C00F41F1D /* ReplaySubject.swift */; }; - 51A8005224CC453C00F41F1D /* ReplaySubject.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8005024CC453C00F41F1D /* ReplaySubject.swift */; }; - 51A8FFED24CA0CF400F41F1D /* WIthLatestFrom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8FFEC24CA0CF400F41F1D /* WIthLatestFrom.swift */; }; - 51A8FFEE24CA0CF400F41F1D /* WIthLatestFrom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A8FFEC24CA0CF400F41F1D /* WIthLatestFrom.swift */; }; 51A9A5E12380C4FE0033AADF /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45255226507D200C03939 /* AppDefaults.swift */; }; 51A9A5E42380C8880033AADF /* ShareFolderPickerAccountCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */; }; 51A9A5E62380C8B20033AADF /* ShareFolderPickerFolderCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51A9A5E52380C8B20033AADF /* ShareFolderPickerFolderCell.xib */; }; @@ -483,15 +317,6 @@ 51A9A5F52380F6A60033AADF /* ModalNavigationController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */; }; 51A9A60A2382FD240033AADF /* PoppableGestureRecognizerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */; }; 51AB8AB323B7F4C6008F147D /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51AB8AB223B7F4C6008F147D /* WebViewController.swift */; }; - 51B54A4324B5499B0014348B /* WebViewProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177471924B3863000EB0F74 /* WebViewProvider.swift */; }; - 51B54A6524B549B20014348B /* WrapperScriptMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 517B2EBB24B3E62A001AC46C /* WrapperScriptMessageHandler.swift */; }; - 51B54A6624B549CB0014348B /* PreloadedWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177471324B37D4000EB0F74 /* PreloadedWebView.swift */; }; - 51B54A6724B549FE0014348B /* ArticleIconSchemeHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177471524B37D9700EB0F74 /* ArticleIconSchemeHandler.swift */; }; - 51B54A6924B54A490014348B /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B54A6824B54A490014348B /* IconView.swift */; }; - 51B54AB324B5AC830014348B /* ArticleExtractorButtonState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5177472124B38CAE00EB0F74 /* ArticleExtractorButtonState.swift */; }; - 51B54AB624B5B33C0014348B /* WebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B54AB524B5B33C0014348B /* WebViewController.swift */; }; - 51B54ABC24B5BEF20014348B /* ArticleView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B54ABB24B5BEF20014348B /* ArticleView.swift */; }; - 51B54B6724B6A7960014348B /* WebStatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B54B6624B6A7960014348B /* WebStatusBarView.swift */; }; 51B5C87723F22B8200032075 /* ExtensionContainers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87623F22B8200032075 /* ExtensionContainers.swift */; }; 51B5C87B23F2317700032075 /* ExtensionFeedAddRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */; }; 51B5C87D23F2346200032075 /* ExtensionContainersFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87C23F2346200032075 /* ExtensionContainersFile.swift */; }; @@ -506,21 +331,6 @@ 51B5C8E623F4BBFA00032075 /* ExtensionFeedAddRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */; }; 51B5C8E723F4BBFA00032075 /* ExtensionFeedAddRequestFile.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */; }; 51B62E68233186730085F949 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B62E67233186730085F949 /* IconView.swift */; }; - 51B80EB824BD1F8B00C6C32D /* ActivityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80EB724BD1F8B00C6C32D /* ActivityViewController.swift */; }; - 51B80EDB24BD225200C6C32D /* OpenInSafariActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80EDA24BD225200C6C32D /* OpenInSafariActivity.swift */; }; - 51B80EDD24BD296700C6C32D /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80EDC24BD296700C6C32D /* ArticleActivityItemSource.swift */; }; - 51B80EDF24BD298900C6C32D /* TitleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80EDE24BD298900C6C32D /* TitleActivityItemSource.swift */; }; - 51B80EE124BD3E9600C6C32D /* FindInArticleActivity.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80EE024BD3E9600C6C32D /* FindInArticleActivity.swift */; }; - 51B80F1F24BE531200C6C32D /* SharingServiceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80F1E24BE531200C6C32D /* SharingServiceView.swift */; }; - 51B80F4224BE588200C6C32D /* SharingServicePickerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80F4124BE588200C6C32D /* SharingServicePickerDelegate.swift */; }; - 51B80F4424BE58BF00C6C32D /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80F4324BE58BF00C6C32D /* SharingServiceDelegate.swift */; }; - 51B80F4624BF76E700C6C32D /* Browser.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B80F4524BF76E700C6C32D /* Browser.swift */; }; - 51B8104524C0E6D200C6C32D /* TimelineTextSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B8104424C0E6D200C6C32D /* TimelineTextSizer.swift */; }; - 51B8104624C0E6D200C6C32D /* TimelineTextSizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B8104424C0E6D200C6C32D /* TimelineTextSizer.swift */; }; - 51B8BCC224C25C3E00360B00 /* SidebarContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B8BCC124C25C3E00360B00 /* SidebarContextMenu.swift */; }; - 51B8BCC324C25C3E00360B00 /* SidebarContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B8BCC124C25C3E00360B00 /* SidebarContextMenu.swift */; }; - 51B8BCE624C25F7C00360B00 /* TimelineContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B8BCE524C25F7C00360B00 /* TimelineContextMenu.swift */; }; - 51B8BCE724C25F7C00360B00 /* TimelineContextMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51B8BCE524C25F7C00360B00 /* TimelineContextMenu.swift */; }; 51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */; }; 51BB7C312335ACDE008E8144 /* page.html in Resources */ = {isa = PBXBuildFile; fileRef = 51BB7C302335ACDE008E8144 /* page.html */; }; 51BC2F3824D3439A00E90810 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 51BC2F3724D3439A00E90810 /* Account */; }; @@ -533,10 +343,6 @@ 51BC4B01247277E0000A6ED8 /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; }; 51C03081257D815A00609262 /* UnifiedWindow.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51C0307F257D815A00609262 /* UnifiedWindow.storyboard */; }; 51C03082257D815A00609262 /* UnifiedWindow.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 51C0307F257D815A00609262 /* UnifiedWindow.storyboard */; }; - 51C0515E24A77DF800194D5E /* MainApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C0513624A77DF700194D5E /* MainApp.swift */; }; - 51C0515F24A77DF800194D5E /* MainApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C0513624A77DF700194D5E /* MainApp.swift */; }; - 51C0516224A77DF800194D5E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 51C0513824A77DF800194D5E /* Assets.xcassets */; }; - 51C0516324A77DF800194D5E /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 51C0513824A77DF800194D5E /* Assets.xcassets */; }; 51C266EA238C334800F53014 /* ContextMenuPreviewViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C266E9238C334800F53014 /* ContextMenuPreviewViewController.swift */; }; 51C45258226508CF00C03939 /* AppAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45254226507D200C03939 /* AppAssets.swift */; }; 51C45259226508D300C03939 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C45255226507D200C03939 /* AppDefaults.swift */; }; @@ -565,8 +371,8 @@ 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; }; 51C45296226509D300C03939 /* OPMLExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8444C8F11FED81840051386C /* OPMLExporter.swift */; }; 51C45297226509E300C03939 /* DefaultFeedsImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */; }; - 51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */; }; - 51C4529A22650A0400C03939 /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; }; + 51C4529922650A0000C03939 /* ArticleThemesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleThemesManager.swift */; }; + 51C4529A22650A0400C03939 /* ArticleTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleTheme.swift */; }; 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */; }; 51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; }; 51C4529D22650A1000C03939 /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; }; @@ -585,181 +391,37 @@ 51C452AE2265104D00C03939 /* ArticleStringFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97731ED9EC04007D329B /* ArticleStringFormatter.swift */; }; 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; }; 51C452B42265141B00C03939 /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51C452B32265141B00C03939 /* WebKit.framework */; }; - 51C452B82265178500C03939 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 51C452B72265178500C03939 /* styleSheet.css */; }; 51C4CFF024D37D1F00AF9874 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4CFEF24D37D1F00AF9874 /* Secrets.swift */; }; 51C4CFF124D37D1F00AF9874 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4CFEF24D37D1F00AF9874 /* Secrets.swift */; }; 51C4CFF224D37D1F00AF9874 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4CFEF24D37D1F00AF9874 /* Secrets.swift */; }; - 51C4CFF324D37D1F00AF9874 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4CFEF24D37D1F00AF9874 /* Secrets.swift */; }; - 51C4CFF424D37D1F00AF9874 /* Secrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C4CFEF24D37D1F00AF9874 /* Secrets.swift */; }; 51C4CFF624D37DD500AF9874 /* Secrets in Frameworks */ = {isa = PBXBuildFile; productRef = 51C4CFF524D37DD500AF9874 /* Secrets */; }; - 51C65AFC24CCB2C9008EB3BD /* TimelineItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AFB24CCB2C9008EB3BD /* TimelineItems.swift */; }; - 51C65AFD24CCB2C9008EB3BD /* TimelineItems.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C65AFB24CCB2C9008EB3BD /* TimelineItems.swift */; }; 51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */; }; 51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */; }; 51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51CE1C0A23622006005548FC /* RefreshProgressView.swift */; }; 51CE1C712367721A005548FC /* testURLsOfCurrentArticle.applescript in Sources */ = {isa = PBXBuildFile; fileRef = 84F9EADB213660A100CF2DE4 /* testURLsOfCurrentArticle.applescript */; }; + 51D0214626ED617100FF2E0F /* core.css in Resources */ = {isa = PBXBuildFile; fileRef = 51D0214526ED617100FF2E0F /* core.css */; }; + 51D0214726ED617100FF2E0F /* core.css in Resources */ = {isa = PBXBuildFile; fileRef = 51D0214526ED617100FF2E0F /* core.css */; }; + 51D0214826ED617100FF2E0F /* core.css in Resources */ = {isa = PBXBuildFile; fileRef = 51D0214526ED617100FF2E0F /* core.css */; }; 51D5948722668EFA00DFC836 /* MarkStatusCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84702AA31FA27AC0006B8943 /* MarkStatusCommand.swift */; }; 51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */; }; 51D87EE12311D34700E63F03 /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; }; 51DC07982552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; 51DC07992552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; 51DC079A2552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; - 51DC079B2552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; - 51DC079C2552083500A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; 51DC07AC255209E200A3F79F /* ArticleTextSize.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC07972552083500A3F79F /* ArticleTextSize.swift */; }; - 51DC37072402153E0095D371 /* UpdateSelectionOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */; }; - 51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */; }; 51DC370B2405BC9A0095D371 /* PreloadedWebView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */; }; - 51E0614525A5A28E00194066 /* Articles in Frameworks */ = {isa = PBXBuildFile; productRef = 51E0614425A5A28E00194066 /* Articles */; }; - 51E0614625A5A28E00194066 /* Articles in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51E0614425A5A28E00194066 /* Articles */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 51E0614825A5A28E00194066 /* ArticlesDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 51E0614725A5A28E00194066 /* ArticlesDatabase */; }; - 51E0614925A5A28E00194066 /* ArticlesDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51E0614725A5A28E00194066 /* ArticlesDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 51E0614B25A5A28E00194066 /* Secrets in Frameworks */ = {isa = PBXBuildFile; productRef = 51E0614A25A5A28E00194066 /* Secrets */; }; - 51E0614C25A5A28E00194066 /* Secrets in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51E0614A25A5A28E00194066 /* Secrets */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 51E0614E25A5A28E00194066 /* SyncDatabase in Frameworks */ = {isa = PBXBuildFile; productRef = 51E0614D25A5A28E00194066 /* SyncDatabase */; }; - 51E0614F25A5A28E00194066 /* SyncDatabase in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 51E0614D25A5A28E00194066 /* SyncDatabase */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; - 51E0615125A5A29600194066 /* CrashReporter in Frameworks */ = {isa = PBXBuildFile; productRef = 51E0615025A5A29600194066 /* CrashReporter */; }; + 51DEE81226FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; }; + 51DEE81326FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; }; + 51DEE81426FB9233006DAA56 /* Appanoose.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */; }; + 51DEE81826FBFF84006DAA56 /* Promenade.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81726FBFF84006DAA56 /* Promenade.nnwtheme */; }; + 51DEE81926FBFF84006DAA56 /* Promenade.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81726FBFF84006DAA56 /* Promenade.nnwtheme */; }; + 51DEE81A26FBFF84006DAA56 /* Promenade.nnwtheme in Resources */ = {isa = PBXBuildFile; fileRef = 51DEE81726FBFF84006DAA56 /* Promenade.nnwtheme */; }; 51E36E71239D6610006F47A5 /* AddFeedSelectFolderTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E36E70239D6610006F47A5 /* AddFeedSelectFolderTableViewCell.swift */; }; 51E36E8C239D6765006F47A5 /* AddFeedSelectFolderTableViewCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = 51E36E8B239D6765006F47A5 /* AddFeedSelectFolderTableViewCell.xib */; }; 51E3EB33229AB02C00645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB32229AB02C00645299 /* ErrorHandler.swift */; }; 51E3EB3D229AB08300645299 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E3EB3C229AB08300645299 /* ErrorHandler.swift */; }; 51E43962238037C400015C31 /* AddFeedFolderViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E43961238037C400015C31 /* AddFeedFolderViewController.swift */; }; 51E4398023805EBC00015C31 /* AddComboTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4397F23805EBC00015C31 /* AddComboTableViewCell.swift */; }; - 51E4989724A8065700B667CB /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4989624A8065700B667CB /* CloudKit.framework */; }; - 51E4989924A8067000B667CB /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4989824A8067000B667CB /* WebKit.framework */; }; - 51E498B124A806A400B667CB /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4DAEC2425F6940091EB5B /* CloudKit.framework */; }; - 51E498B324A806AA00B667CB /* WebKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E498B224A806AA00B667CB /* WebKit.framework */; }; - 51E498C724A8085D00B667CB /* StarredFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */; }; - 51E498C824A8085D00B667CB /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; }; - 51E498C924A8085D00B667CB /* PseudoFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5351FC22FCB00998D64 /* PseudoFeed.swift */; }; - 51E498CA24A8085D00B667CB /* SmartFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DEE56422C32CA4005FC42C /* SmartFeedDelegate.swift */; }; - 51E498CB24A8085D00B667CB /* TodayFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5361FC22FCB00998D64 /* TodayFeedDelegate.swift */; }; - 51E498CC24A8085D00B667CB /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; }; - 51E498CD24A8085D00B667CB /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; }; - 51E498CE24A8085D00B667CB /* UnreadFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5391FC2308B00998D64 /* UnreadFeed.swift */; }; - 51E498CF24A8085D00B667CB /* SmartFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7C01FC2488C00854A1F /* SmartFeed.swift */; }; - 51E498F124A8085D00B667CB /* StarredFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7B01FC2366500854A1F /* StarredFeedDelegate.swift */; }; - 51E498F224A8085D00B667CB /* SmartFeedsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CC88171FE59CBF00644329 /* SmartFeedsController.swift */; }; - 51E498F324A8085D00B667CB /* PseudoFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5351FC22FCB00998D64 /* PseudoFeed.swift */; }; - 51E498F424A8085D00B667CB /* SmartFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84DEE56422C32CA4005FC42C /* SmartFeedDelegate.swift */; }; - 51E498F524A8085D00B667CB /* TodayFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5361FC22FCB00998D64 /* TodayFeedDelegate.swift */; }; - 51E498F624A8085D00B667CB /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; }; - 51E498F724A8085D00B667CB /* SearchTimelineFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */; }; - 51E498F824A8085D00B667CB /* UnreadFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F2D5391FC2308B00998D64 /* UnreadFeed.swift */; }; - 51E498F924A8085D00B667CB /* SmartFeed.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845EE7C01FC2488C00854A1F /* SmartFeed.swift */; }; - 51E498FA24A808BA00B667CB /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; }; - 51E498FB24A808BA00B667CB /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; }; - 51E498FC24A808BA00B667CB /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; }; - 51E498FD24A808BA00B667CB /* ColorHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F78227716380050506E /* ColorHash.swift */; }; - 51E498FE24A808BA00B667CB /* FaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */; }; - 51E498FF24A808BB00B667CB /* SingleFaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845A29081FC74B8E007B49E3 /* SingleFaviconDownloader.swift */; }; - 51E4990024A808BB00B667CB /* FaviconGenerator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F76227716200050506E /* FaviconGenerator.swift */; }; - 51E4990124A808BB00B667CB /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; }; - 51E4990224A808BB00B667CB /* ColorHash.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51EF0F78227716380050506E /* ColorHash.swift */; }; - 51E4990324A808BB00B667CB /* FaviconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */; }; - 51E4990424A808C300B667CB /* WebFeedIconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611891FCB67AA0086A189 /* WebFeedIconDownloader.swift */; }; - 51E4990524A808C300B667CB /* FeaturedImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119F1FCB72600086A189 /* FeaturedImageDownloader.swift */; }; - 51E4990624A808C300B667CB /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845213221FCA5B10003B6E93 /* ImageDownloader.swift */; }; - 51E4990724A808C300B667CB /* AuthorAvatarDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */; }; - 51E4990824A808C300B667CB /* RSHTMLMetadata+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611A11FCB769D0086A189 /* RSHTMLMetadata+Extension.swift */; }; - 51E4990924A808C500B667CB /* WebFeedIconDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611891FCB67AA0086A189 /* WebFeedIconDownloader.swift */; }; - 51E4990A24A808C500B667CB /* FeaturedImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119F1FCB72600086A189 /* FeaturedImageDownloader.swift */; }; - 51E4990B24A808C500B667CB /* ImageDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 845213221FCA5B10003B6E93 /* ImageDownloader.swift */; }; - 51E4990C24A808C500B667CB /* AuthorAvatarDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E850851FCB60CE0072EA88 /* AuthorAvatarDownloader.swift */; }; - 51E4990D24A808C500B667CB /* RSHTMLMetadata+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611A11FCB769D0086A189 /* RSHTMLMetadata+Extension.swift */; }; - 51E4990E24A808CC00B667CB /* HTMLMetadataDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */; }; - 51E4990F24A808CC00B667CB /* HTMLMetadataDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8426119D1FCB6ED40086A189 /* HTMLMetadataDownloader.swift */; }; - 51E4991024A808DE00B667CB /* SmallIconProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */; }; - 51E4991124A808DE00B667CB /* SmallIconProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84411E701FE5FBFA004B527F /* SmallIconProvider.swift */; }; - 51E4991224A808FB00B667CB /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; }; - 51E4991324A808FB00B667CB /* AddWebFeedDefaultContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */; }; - 51E4991424A808FF00B667CB /* ArticleStringFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97731ED9EC04007D329B /* ArticleStringFormatter.swift */; }; - 51E4991524A808FF00B667CB /* ArticleStringFormatter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97731ED9EC04007D329B /* ArticleStringFormatter.swift */; }; - 51E4991624A8090300B667CB /* ArticleUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97581ED9EB0D007D329B /* ArticleUtilities.swift */; }; - 51E4991724A8090400B667CB /* ArticleUtilities.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97581ED9EB0D007D329B /* ArticleUtilities.swift */; }; - 51E4991824A8090A00B667CB /* CacheCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6B52375E612001ABC45 /* CacheCleaner.swift */; }; - 51E4991924A8090A00B667CB /* CacheCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6B52375E612001ABC45 /* CacheCleaner.swift */; }; - 51E4991A24A8090F00B667CB /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE9DE2372269A007DEEAA /* IconImage.swift */; }; - 51E4991B24A8091000B667CB /* IconImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 516AE9DE2372269A007DEEAA /* IconImage.swift */; }; - 51E4991C24A8092000B667CB /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; }; - 51E4991D24A8092100B667CB /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; }; - 51E4991E24A8094300B667CB /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; - 51E4991F24A8094300B667CB /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; - 51E4992024A8095000B667CB /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; }; - 51E4992124A8095000B667CB /* RSImage-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51126DA3225FDE2F00722696 /* RSImage-Extensions.swift */; }; - 51E4992224A8095600B667CB /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; }; - 51E4992324A8095700B667CB /* URL-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */; }; - 51E4992424A8098400B667CB /* SmartFeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */; }; - 51E4992624A80AAB00B667CB /* AppAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4992524A80AAB00B667CB /* AppAssets.swift */; }; - 51E4992724A80AAB00B667CB /* AppAssets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4992524A80AAB00B667CB /* AppAssets.swift */; }; - 51E4992B24A8676300B667CB /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; }; - 51E4992C24A8676300B667CB /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; }; - 51E4992D24A8676300B667CB /* FetchRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAFCAE22BC8C35007694F0 /* FetchRequestOperation.swift */; }; - 51E4992E24A8676300B667CB /* FetchRequestQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */; }; - 51E4992F24A8676400B667CB /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; }; - 51E4993024A8676400B667CB /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; }; - 51E4993124A8676400B667CB /* FetchRequestOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAFCAE22BC8C35007694F0 /* FetchRequestOperation.swift */; }; - 51E4993224A8676400B667CB /* FetchRequestQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84CAFCA322BC8C08007694F0 /* FetchRequestQueue.swift */; }; - 51E4993324A867E700B667CB /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45CD1ED8C308000A8B52 /* AppNotifications.swift */; }; - 51E4993424A867E700B667CB /* UserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511B9805237DCAC90028BCAA /* UserInfoKey.swift */; }; - 51E4993524A867E800B667CB /* AppNotifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45CD1ED8C308000A8B52 /* AppNotifications.swift */; }; - 51E4993624A867E800B667CB /* UserInfoKey.swift in Sources */ = {isa = PBXBuildFile; fileRef = 511B9805237DCAC90028BCAA /* UserInfoKey.swift */; }; - 51E4993A24A8708800B667CB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4993924A8708800B667CB /* AppDelegate.swift */; }; - 51E4993C24A8709900B667CB /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4993B24A8709900B667CB /* AppDelegate.swift */; }; - 51E4993D24A870F800B667CB /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; }; - 51E4993E24A870F900B667CB /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; }; - 51E4993F24A8713B00B667CB /* ArticleStatusSyncTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */; }; - 51E4994024A8713B00B667CB /* AccountRefreshTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */; }; - 51E4994224A8713C00B667CB /* ArticleStatusSyncTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */; }; - 51E4994A24A8734C00B667CB /* ExtensionPointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */; }; - 51E4994B24A8734C00B667CB /* SendToMicroBlogCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A14FF220048CA70046AD9A /* SendToMicroBlogCommand.swift */; }; - 51E4994C24A8734C00B667CB /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; }; - 51E4994D24A8734C00B667CB /* ExtensionPointIdentifer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5176243E90200089E588 /* ExtensionPointIdentifer.swift */; }; - 51E4994E24A8734C00B667CB /* SendToMarsEditCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */; }; - 51E4994F24A8734C00B667CB /* TwitterFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */; }; - 51E4995024A8734C00B667CB /* ExtensionPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510C43F6243D035C009F70C3 /* ExtensionPoint.swift */; }; - 51E4995124A8734D00B667CB /* ExtensionPointManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */; }; - 51E4995324A8734D00B667CB /* RedditFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */; }; - 51E4995424A8734D00B667CB /* ExtensionPointIdentifer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5176243E90200089E588 /* ExtensionPointIdentifer.swift */; }; - 51E4995624A8734D00B667CB /* TwitterFeedProvider-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */; }; - 51E4995724A8734D00B667CB /* ExtensionPoint.swift in Sources */ = {isa = PBXBuildFile; fileRef = 510C43F6243D035C009F70C3 /* ExtensionPoint.swift */; }; - 51E4995924A873F900B667CB /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4995824A873F900B667CB /* ErrorHandler.swift */; }; - 51E4995A24A873F900B667CB /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E4995824A873F900B667CB /* ErrorHandler.swift */; }; - 51E4995B24A875D500B667CB /* ArticlePasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84E95D231FB1087500552D99 /* ArticlePasteboardWriter.swift */; }; - 51E4995C24A875F300B667CB /* ArticleRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */; }; - 51E4995D24A875F300B667CB /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; - 51E4995E24A875F300B667CB /* newsfoot.js in Resources */ = {isa = PBXBuildFile; fileRef = 49F40DEF2335B71000552BF4 /* newsfoot.js */; }; - 51E4995F24A875F300B667CB /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; - 51E4996024A875F300B667CB /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; }; - 51E4996124A875F400B667CB /* ArticleRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */; }; - 51E4996224A875F400B667CB /* main.js in Resources */ = {isa = PBXBuildFile; fileRef = 517630032336215100E15FFF /* main.js */; }; - 51E4996324A875F400B667CB /* newsfoot.js in Resources */ = {isa = PBXBuildFile; fileRef = 49F40DEF2335B71000552BF4 /* newsfoot.js */; }; - 51E4996424A875F400B667CB /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; - 51E4996524A875F400B667CB /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; }; - 51E4996624A8760B00B667CB /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; }; - 51E4996724A8760B00B667CB /* ArticleStylesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */; }; - 51E4996824A8760C00B667CB /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; }; - 51E4996924A8760C00B667CB /* ArticleStylesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */; }; - 51E4996A24A8762D00B667CB /* ExtractedArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A62332BE880090D516 /* ExtractedArticle.swift */; }; - 51E4996B24A8762D00B667CB /* ArticleExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A32332BE110090D516 /* ArticleExtractor.swift */; }; - 51E4996C24A8762D00B667CB /* ExtractedArticle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A62332BE880090D516 /* ExtractedArticle.swift */; }; - 51E4996D24A8762D00B667CB /* ArticleExtractor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FA73A32332BE110090D516 /* ArticleExtractor.swift */; }; - 51E4996E24A8764C00B667CB /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; }; - 51E4996F24A8764C00B667CB /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; }; - 51E4997024A8764C00B667CB /* ActivityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51934CCD2310792F006127BE /* ActivityManager.swift */; }; - 51E4997124A8764C00B667CB /* ActivityType.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51D87EE02311D34700E63F03 /* ActivityType.swift */; }; - 51E4997224A8784300B667CB /* DefaultFeedsImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */; }; - 51E4997324A8784300B667CB /* DefaultFeeds.opml in Resources */ = {isa = PBXBuildFile; fileRef = 84A3EE52223B667F00557320 /* DefaultFeeds.opml */; }; - 51E4997424A8784400B667CB /* DefaultFeedsImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */; }; - 51E4997524A8784400B667CB /* DefaultFeeds.opml in Resources */ = {isa = PBXBuildFile; fileRef = 84A3EE52223B667F00557320 /* DefaultFeeds.opml */; }; - 51E499D824A912C200B667CB /* SceneModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E499D724A912C200B667CB /* SceneModel.swift */; }; - 51E499D924A912C200B667CB /* SceneModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E499D724A912C200B667CB /* SceneModel.swift */; }; - 51E499FD24A9137600B667CB /* SidebarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E499FC24A9137600B667CB /* SidebarModel.swift */; }; - 51E499FE24A9137600B667CB /* SidebarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E499FC24A9137600B667CB /* SidebarModel.swift */; }; - 51E49A0024A91FC100B667CB /* SidebarContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E499FF24A91FC100B667CB /* SidebarContainerView.swift */; }; - 51E49A0124A91FC100B667CB /* SidebarContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E499FF24A91FC100B667CB /* SidebarContainerView.swift */; }; - 51E49A0324A91FF600B667CB /* SceneNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E49A0224A91FF600B667CB /* SceneNavigationView.swift */; }; - 51E49A0424A91FF600B667CB /* SceneNavigationView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E49A0224A91FF600B667CB /* SceneNavigationView.swift */; }; 51E4DAED2425F6940091EB5B /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4DAEC2425F6940091EB5B /* CloudKit.framework */; }; 51E4DB082425F9EB0091EB5B /* CloudKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51E4DB072425F9EB0091EB5B /* CloudKit.framework */; }; 51E595A5228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */; }; @@ -798,11 +460,6 @@ 55E15BCB229D65A900D6602A /* AccountsReaderAPI.xib in Resources */ = {isa = PBXBuildFile; fileRef = 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */; }; 55E15BCC229D65A900D6602A /* AccountsReaderAPIWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */; }; 5F323809231DF9F000706F6B /* VibrantTableViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */; }; - 65082A2F24C72AC8009FA994 /* SettingsCredentialsAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65082A2E24C72AC8009FA994 /* SettingsCredentialsAccountView.swift */; }; - 65082A5224C72B88009FA994 /* SettingsCredentialsAccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65082A5124C72B88009FA994 /* SettingsCredentialsAccountModel.swift */; }; - 65082A5424C73D2F009FA994 /* AccountCredentialsError.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65082A5324C73D2F009FA994 /* AccountCredentialsError.swift */; }; - 6535ECFC2680F9FF00C01CB5 /* IconImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */; }; - 6535ECFD2680FA0000C01CB5 /* IconImageCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8454C3F2263F2D8700E3F9C7 /* IconImageCache.swift */; }; 653813182680E152007A082C /* AccountType+Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 173A64162547BE0900267F6E /* AccountType+Helpers.swift */; }; 653813192680E15B007A082C /* CacheCleaner.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5108F6B52375E612001ABC45 /* CacheCleaner.swift */; }; 6538131A2680E16C007A082C /* ExportOPMLWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849C78912362AB04009A71E4 /* ExportOPMLWindowController.swift */; }; @@ -843,22 +500,11 @@ 653813522680E2DA007A082C /* SafariExt.js in Resources */ = {isa = PBXBuildFile; fileRef = 515D4FCB2325815A00EE1167 /* SafariExt.js */; }; 653813542680E2DA007A082C /* RSCore in Embed Frameworks */ = {isa = PBXBuildFile; productRef = 653813402680E2DA007A082C /* RSCore */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; 6538135C2680E47A007A082C /* NetNewsWire Share Extension MAS.appex in Embed App Extensions */ = {isa = PBXBuildFile; fileRef = 653813592680E2DA007A082C /* NetNewsWire Share Extension MAS.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; }; - 653A4E7924BCA5BB00EF2D7F /* SettingsCloudKitAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 653A4E7824BCA5BB00EF2D7F /* SettingsCloudKitAccountView.swift */; }; - 65422D1724B75CD1008A2FA2 /* SettingsAddAccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65422D1624B75CD1008A2FA2 /* SettingsAddAccountModel.swift */; }; 6581C73820CED60100F4AD34 /* SafariExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73720CED60100F4AD34 /* SafariExtensionHandler.swift */; }; 6581C73A20CED60100F4AD34 /* SafariExtensionViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6581C73920CED60100F4AD34 /* SafariExtensionViewController.swift */; }; 6581C73D20CED60100F4AD34 /* SafariExtensionViewController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73B20CED60100F4AD34 /* SafariExtensionViewController.xib */; }; 6581C74020CED60100F4AD34 /* netnewswire-subscribe-to-feed.js in Resources */ = {isa = PBXBuildFile; fileRef = 6581C73F20CED60100F4AD34 /* netnewswire-subscribe-to-feed.js */; }; 6581C74220CED60100F4AD34 /* ToolbarItemIcon.pdf in Resources */ = {isa = PBXBuildFile; fileRef = 6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */; }; - 6586A5F724B632F8002BCF4F /* SettingsDetailAccountModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6586A5F624B632F8002BCF4F /* SettingsDetailAccountModel.swift */; }; - 6591723124B5C35400B638E8 /* AccountHeaderImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6591723024B5C35400B638E8 /* AccountHeaderImageView.swift */; }; - 6591727F24B5D19500B638E8 /* SettingsDetailAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6591727E24B5D19500B638E8 /* SettingsDetailAccountView.swift */; }; - 6594CA3B24AF6F2A005C7D7C /* OPMLExporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8444C8F11FED81840051386C /* OPMLExporter.swift */; }; - 65ACE48424B4779B003AE06A /* SettingsAddAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65ACE48324B4779B003AE06A /* SettingsAddAccountView.swift */; }; - 65ACE48624B477C9003AE06A /* SettingsAccountLabelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65ACE48524B477C9003AE06A /* SettingsAccountLabelView.swift */; }; - 65ACE48824B48020003AE06A /* SettingsLocalAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65ACE48724B48020003AE06A /* SettingsLocalAccountView.swift */; }; - 65C2E40124B05D8A000AFDF6 /* FeedsSettingsModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65C2E40024B05D8A000AFDF6 /* FeedsSettingsModel.swift */; }; - 65CBAD5A24AE03C20006DD91 /* ColorPaletteContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 65CBAD5924AE03C20006DD91 /* ColorPaletteContainerView.swift */; }; 65ED3FB7235DEF6C0081F399 /* ArticleArray.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84F204DF1FAACBB30076E152 /* ArticleArray.swift */; }; 65ED3FB8235DEF6C0081F399 /* CrashReporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 848B937121C8C5540038DC0D /* CrashReporter.swift */; }; 65ED3FB9235DEF6C0081F399 /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* IconView.swift */; }; @@ -904,14 +550,14 @@ 65ED3FE5235DEF6C0081F399 /* DefaultFeedsImporter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97591ED9EB0D007D329B /* DefaultFeedsImporter.swift */; }; 65ED3FE6235DEF6C0081F399 /* RenameWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A37CB4201ECD610087C5AF /* RenameWindowController.swift */; }; 65ED3FE7235DEF6C0081F399 /* SendToMicroBlogCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A14FF220048CA70046AD9A /* SendToMicroBlogCommand.swift */; }; - 65ED3FE8235DEF6C0081F399 /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; }; + 65ED3FE8235DEF6C0081F399 /* ArticleTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleTheme.swift */; }; 65ED3FE9235DEF6C0081F399 /* FaviconURLFinder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84FF69B01FC3793300DC198E /* FaviconURLFinder.swift */; }; 65ED3FEA235DEF6C0081F399 /* SidebarViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */; }; 65ED3FEC235DEF6C0081F399 /* RSHTMLMetadata+Extension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842611A11FCB769D0086A189 /* RSHTMLMetadata+Extension.swift */; }; 65ED3FED235DEF6C0081F399 /* SendToMarsEditCommand.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */; }; 65ED3FEE235DEF6C0081F399 /* UserNotificationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 51FE10022345529D0056195D /* UserNotificationManager.swift */; }; 65ED3FEF235DEF6C0081F399 /* ScriptingObjectContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5907DB12004BB37005947E5 /* ScriptingObjectContainer.swift */; }; - 65ED3FF0235DEF6C0081F399 /* ArticleStylesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */; }; + 65ED3FF0235DEF6C0081F399 /* ArticleThemesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleThemesManager.swift */; }; 65ED3FF1235DEF6C0081F399 /* DetailContainerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8405DD892213E0E3008CE1BF /* DetailContainerView.swift */; }; 65ED3FF2235DEF6C0081F399 /* SharingServiceDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 519B8D322143397200FA689C /* SharingServiceDelegate.swift */; }; 65ED3FF3235DEF6C0081F399 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; }; @@ -1008,7 +654,6 @@ 65ED405E235DEF6C0081F399 /* DefaultFeeds.opml in Resources */ = {isa = PBXBuildFile; fileRef = 84A3EE52223B667F00557320 /* DefaultFeeds.opml */; }; 65ED405F235DEF6C0081F399 /* Preferences.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 84C9FC8022629E4800D921D6 /* Preferences.storyboard */; }; 65ED4061235DEF6C0081F399 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 849C64671ED37A5D003D8FC0 /* Assets.xcassets */; }; - 65ED4062235DEF6C0081F399 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 848362FC2262A30800DA1D35 /* styleSheet.css */; }; 65ED4063235DEF6C0081F399 /* RenameSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 848363092262A3F000DA1D35 /* RenameSheet.xib */; }; 65ED4064235DEF6C0081F399 /* AddFolderSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 848363032262A3CC00DA1D35 /* AddFolderSheet.xib */; }; 65ED4065235DEF6C0081F399 /* AccountsFeedbin.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5144EA50227B8E4500D19003 /* AccountsFeedbin.xib */; }; @@ -1069,7 +714,6 @@ 8477ACBE22238E9500DF7F37 /* SearchFeedDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */; }; 847CD6CA232F4CBF00FAC46D /* IconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847CD6C9232F4CBF00FAC46D /* IconView.swift */; }; 847E64A02262783000E00365 /* NSAppleEventDescriptor+UserRecordFields.swift in Sources */ = {isa = PBXBuildFile; fileRef = 847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */; }; - 848362FD2262A30800DA1D35 /* styleSheet.css in Resources */ = {isa = PBXBuildFile; fileRef = 848362FC2262A30800DA1D35 /* styleSheet.css */; }; 848362FF2262A30E00DA1D35 /* template.html in Resources */ = {isa = PBXBuildFile; fileRef = 848362FE2262A30E00DA1D35 /* template.html */; }; 848363022262A3BD00DA1D35 /* AddWebFeedSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 848363002262A3BC00DA1D35 /* AddWebFeedSheet.xib */; }; 848363052262A3CC00DA1D35 /* AddFolderSheet.xib in Resources */ = {isa = PBXBuildFile; fileRef = 848363032262A3CC00DA1D35 /* AddFolderSheet.xib */; }; @@ -1101,8 +745,8 @@ 849A977F1ED9EC42007D329B /* ArticleRenderer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */; }; 849A97801ED9EC42007D329B /* DetailViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A977E1ED9EC42007D329B /* DetailViewController.swift */; }; 849A97831ED9EC63007D329B /* SidebarStatusBarView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97821ED9EC63007D329B /* SidebarStatusBarView.swift */; }; - 849A97891ED9ECEF007D329B /* ArticleStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleStyle.swift */; }; - 849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */; }; + 849A97891ED9ECEF007D329B /* ArticleTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97871ED9ECEF007D329B /* ArticleTheme.swift */; }; + 849A978A1ED9ECEF007D329B /* ArticleThemesManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97881ED9ECEF007D329B /* ArticleThemesManager.swift */; }; 849A97981ED9EFAA007D329B /* Node-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97971ED9EFAA007D329B /* Node-Extensions.swift */; }; 849A979F1ED9F130007D329B /* SidebarCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A979E1ED9F130007D329B /* SidebarCell.swift */; }; 849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */; }; @@ -1181,9 +825,9 @@ B24E9ADC245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; }; B24E9ADD245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; }; B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */ = {isa = PBXBuildFile; fileRef = B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */; }; - B27EEBF9244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; - B27EEBFA244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; - B27EEBFB244D15F3000932E6 /* shared.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* shared.css */; }; + B27EEBF9244D15F3000932E6 /* stylesheet.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* stylesheet.css */; }; + B27EEBFA244D15F3000932E6 /* stylesheet.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* stylesheet.css */; }; + B27EEBFB244D15F3000932E6 /* stylesheet.css in Resources */ = {isa = PBXBuildFile; fileRef = B27EEBDF244D15F2000932E6 /* stylesheet.css */; }; B2B8075E239C49D300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; B2B80779239C4C7300F191E0 /* RSImage-AppIcons.swift in Sources */ = {isa = PBXBuildFile; fileRef = B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */; }; @@ -1206,19 +850,9 @@ D5F4EDB720074D6500B9E363 /* WebFeed+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB620074D6500B9E363 /* WebFeed+Scriptability.swift */; }; D5F4EDB920074D7C00B9E363 /* Folder+Scriptability.swift in Sources */ = {isa = PBXBuildFile; fileRef = D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */; }; DD82AB0A231003F6002269DF /* SharingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DD82AB09231003F6002269DF /* SharingTests.swift */; }; - DF98E29A2578A73A00F18944 /* AddCloudKitAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF98E2992578A73A00F18944 /* AddCloudKitAccountView.swift */; }; - DF98E2B02578AA5C00F18944 /* AddFeedWranglerAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF98E2AF2578AA5C00F18944 /* AddFeedWranglerAccountView.swift */; }; - DF98E2BE2578AC0000F18944 /* AddNewsBlurAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF98E2BD2578AC0000F18944 /* AddNewsBlurAccountView.swift */; }; - DF98E2C62578AD1B00F18944 /* AddReaderAPIAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF98E2C52578AD1B00F18944 /* AddReaderAPIAccountView.swift */; }; - FA80C11724B0728000974098 /* AddFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA80C11624B0728000974098 /* AddFolderView.swift */; }; - FA80C11824B0728000974098 /* AddFolderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA80C11624B0728000974098 /* AddFolderView.swift */; }; - FA80C13E24B072AA00974098 /* AddFolderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA80C13D24B072AA00974098 /* AddFolderModel.swift */; }; - FA80C13F24B072AB00974098 /* AddFolderModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FA80C13D24B072AA00974098 /* AddFolderModel.swift */; }; FF3ABF13232599810074C542 /* ArticleSorterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF09232599450074C542 /* ArticleSorterTests.swift */; }; FF3ABF1523259DDB0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; }; FF3ABF162325AF5D0074C542 /* ArticleSorter.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */; }; - FF64D0E724AF53EE0084080A /* RefreshProgressModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF64D0C424AF53EE0084080A /* RefreshProgressModel.swift */; }; - FF64D0E824AF53EE0084080A /* RefreshProgressModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = FF64D0C424AF53EE0084080A /* RefreshProgressModel.swift */; }; FFD43E412340F488009E5CA3 /* MarkAsReadAlertController.swift in Sources */ = {isa = PBXBuildFile; fileRef = FFD43E372340F320009E5CA3 /* MarkAsReadAlertController.swift */; }; /* End PBXBuildFile section */ @@ -1379,50 +1013,6 @@ name = "Embed Frameworks"; runOnlyForDeploymentPostprocessing = 0; }; - 51E4989524A8061400B667CB /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 17A1598924E3DEDD005DA32A /* RSParser in Embed Frameworks */, - 17A1598624E3DEDD005DA32A /* RSDatabase in Embed Frameworks */, - 17A1598324E3DEDD005DA32A /* RSWeb in Embed Frameworks */, - 17A1597D24E3DEDD005DA32A /* RSCore in Embed Frameworks */, - 17A1598024E3DEDD005DA32A /* RSTree in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; - 51E498B024A8069300B667CB /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - 51E0614C25A5A28E00194066 /* Secrets in Embed Frameworks */, - 17386BA52577C6240014C8B2 /* RSParser in Embed Frameworks */, - 17386B9F2577C6240014C8B2 /* RSDatabase in Embed Frameworks */, - 17386B9C2577C6240014C8B2 /* RSWeb in Embed Frameworks */, - 51E0614F25A5A28E00194066 /* SyncDatabase in Embed Frameworks */, - 51E0614625A5A28E00194066 /* Articles in Embed Frameworks */, - 17386B962577C6240014C8B2 /* RSCore in Embed Frameworks */, - 17386B992577C6240014C8B2 /* RSTree in Embed Frameworks */, - 51E0614925A5A28E00194066 /* ArticlesDatabase in Embed Frameworks */, - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; - 51E4994924A872AD00B667CB /* Embed XPC Services */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = "$(CONTENTS_FOLDER_PATH)/XPCServices"; - dstSubfolderSpec = 16; - files = ( - ); - name = "Embed XPC Services"; - runOnlyForDeploymentPostprocessing = 0; - }; 653813532680E2DA007A082C /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; buildActionMask = 2147483647; @@ -1525,32 +1115,13 @@ /* Begin PBXFileReference section */ 1701E1B62568983D009453D8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 1701E1E625689D1E009453D8 /* Localized.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localized.swift; sourceTree = ""; }; - 1704053324E5985A00A00787 /* SceneNavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneNavigationModel.swift; sourceTree = ""; }; + 17071EEF26F8137400F5E71D /* ArticleTheme+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ArticleTheme+Notifications.swift"; sourceTree = ""; }; 1710B9122552354E00679C0D /* AddAccountHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountHelpView.swift; sourceTree = ""; }; 1710B928255246F900679C0D /* EnableExtensionPointHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableExtensionPointHelpView.swift; sourceTree = ""; }; - 1717535524BADF33004498C6 /* GeneralPreferencesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPreferencesModel.swift; sourceTree = ""; }; 17192AE12567B3FE00AAEACA /* org.sparkle-project.Downloader.xpc */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.xpc-service"; path = "org.sparkle-project.Downloader.xpc"; sourceTree = ""; }; 17192AE22567B3FE00AAEACA /* org.sparkle-project.InstallerConnection.xpc */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.xpc-service"; path = "org.sparkle-project.InstallerConnection.xpc"; sourceTree = ""; }; 17192AE32567B3FE00AAEACA /* org.sparkle-project.InstallerLauncher.xpc */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.xpc-service"; path = "org.sparkle-project.InstallerLauncher.xpc"; sourceTree = ""; }; 17192AE42567B3FE00AAEACA /* org.sparkle-project.InstallerStatus.xpc */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.xpc-service"; path = "org.sparkle-project.InstallerStatus.xpc"; sourceTree = ""; }; - 171BCB8B24CB08A3006E22D9 /* FixAccountCredentialView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FixAccountCredentialView.swift; sourceTree = ""; }; - 172199C824AB228900A31D04 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = ""; }; - 172199EC24AB2E0100A31D04 /* SafariView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafariView.swift; sourceTree = ""; }; - 172199F024AB716900A31D04 /* SidebarToolbarModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarToolbarModifier.swift; sourceTree = ""; }; - 17241248257B8A8A00ACCEBC /* AddFeedlyAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedlyAccountView.swift; sourceTree = ""; }; - 17241269257BBEBB00ACCEBC /* AddFeedbinViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedbinViewModel.swift; sourceTree = ""; }; - 17241277257BBEE700ACCEBC /* AddFeedlyViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedlyViewModel.swift; sourceTree = ""; }; - 1724127F257BBF3E00ACCEBC /* AddFeedWranglerViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedWranglerViewModel.swift; sourceTree = ""; }; - 17241287257BBF7000ACCEBC /* AddNewsBlurViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddNewsBlurViewModel.swift; sourceTree = ""; }; - 1724128F257BBFAD00ACCEBC /* AddReaderAPIViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddReaderAPIViewModel.swift; sourceTree = ""; }; - 1727B39824C1368D00A4DBDC /* LayoutPreferencesView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LayoutPreferencesView.swift; sourceTree = ""; }; - 1729529024AA1CAA00D65E66 /* AccountsPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsPreferencesView.swift; sourceTree = ""; }; - 1729529124AA1CAA00D65E66 /* AdvancedPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AdvancedPreferencesView.swift; sourceTree = ""; }; - 1729529224AA1CAA00D65E66 /* GeneralPreferencesView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneralPreferencesView.swift; sourceTree = ""; }; - 1729529624AA1CD000D65E66 /* MacPreferencePanes.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MacPreferencePanes.swift; sourceTree = ""; }; - 1729529A24AA1FD200D65E66 /* MacSearchField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacSearchField.swift; sourceTree = ""; }; - 17386B792577C4BF0014C8B2 /* AddLocalAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddLocalAccountView.swift; sourceTree = ""; }; - 17386BC32577CC600014C8B2 /* AddFeedbinAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedbinAccountView.swift; sourceTree = ""; }; 173A64162547BE0900267F6E /* AccountType+Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AccountType+Helpers.swift"; sourceTree = ""; }; 176813B62564B9F800D98635 /* WidgetData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetData.swift; sourceTree = ""; }; 176813BD2564BA2800D98635 /* WidgetDataEncoder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WidgetDataEncoder.swift; sourceTree = ""; }; @@ -1571,31 +1142,16 @@ 176814562564BD0600D98635 /* ArticleItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleItemView.swift; sourceTree = ""; }; 1768147A2564BE5400D98635 /* widget-sample.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "widget-sample.json"; sourceTree = ""; }; 176814822564C02A00D98635 /* NetNewsWire_iOS_WidgetExtension.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = NetNewsWire_iOS_WidgetExtension.entitlements; sourceTree = ""; }; - 1769E32124BC5925000E1E8E /* AccountsPreferencesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsPreferencesModel.swift; sourceTree = ""; }; - 1769E32424BC5A65000E1E8E /* AddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountView.swift; sourceTree = ""; }; - 1769E32A24BCB030000E1E8E /* ConfiguredAccountRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConfiguredAccountRow.swift; sourceTree = ""; }; - 1769E32C24BD20A0000E1E8E /* AccountDetailView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDetailView.swift; sourceTree = ""; }; - 1769E32F24BD6271000E1E8E /* EditAccountCredentialsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccountCredentialsView.swift; sourceTree = ""; }; - 1769E33524BD9621000E1E8E /* EditAccountCredentialsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccountCredentialsModel.swift; sourceTree = ""; }; - 1769E33724BD97CB000E1E8E /* AccountUpdateErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountUpdateErrors.swift; sourceTree = ""; }; - 1776E88D24AC5F8A00E78166 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDefaults.swift; sourceTree = ""; }; 177A0C2C25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderAPIAccountViewController.swift; sourceTree = ""; }; - 17897AC924C281A40014BA03 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; }; 178A9F9C2549449F00AB7E9D /* AddAccountsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountsView.swift; sourceTree = ""; }; - 17930ED324AF10EE00A9BA52 /* AddWebFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedView.swift; sourceTree = ""; }; - 1799E6A824C2F93F00511E91 /* InspectorPlatformModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorPlatformModifier.swift; sourceTree = ""; }; - 1799E6CC24C320D600511E91 /* InspectorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorModel.swift; sourceTree = ""; }; + 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleThemePlist.swift; sourceTree = ""; }; 179DBBA2B22A659F81EED6F9 /* AccountsNewsBlurWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsNewsBlurWindowController.swift; sourceTree = ""; }; - 17B223DB24AC24D2001E4592 /* TimelineLayoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLayoutView.swift; sourceTree = ""; }; 17D0682B2564F47E00C0B37E /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; - 17D232A724AFF10A0005F075 /* AddWebFeedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedModel.swift; sourceTree = ""; }; - 17D3CEE2257C4D2300E74939 /* AddAccountSignUp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountSignUp.swift; sourceTree = ""; }; - 17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarToolbarModel.swift; sourceTree = ""; }; + 17D643B026F8A436008D4C05 /* ArticleThemeDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleThemeDownloader.swift; sourceTree = ""; }; 17D7586C2679C21700B17787 /* NetNewsWire-iOS-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-iOS-Bridging-Header.h"; sourceTree = ""; }; 17D7586D2679C21800B17787 /* OnePasswordExtension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OnePasswordExtension.h; sourceTree = ""; }; 17D7586E2679C21800B17787 /* OnePasswordExtension.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OnePasswordExtension.m; sourceTree = ""; }; 17E0084525941887000C23F0 /* SizeCategories.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SizeCategories.swift; sourceTree = ""; }; - 17E4DBD524BFC53E00FE462A /* AdvancedPreferencesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AdvancedPreferencesModel.swift; sourceTree = ""; }; 3B3A328B238B820900314204 /* FeedWranglerAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerAccountViewController.swift; sourceTree = ""; }; 3B826DB02385C84800FC1ADB /* AccountsFeedWrangler.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsFeedWrangler.xib; sourceTree = ""; }; 3B826DCA2385C84800FC1ADB /* AccountsFeedWranglerWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsFeedWranglerWindowController.swift; sourceTree = ""; }; @@ -1619,6 +1175,7 @@ 510C416624E5CDE3008226FD /* ShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ShareExtension.entitlements; sourceTree = ""; }; 510C418724E5D2E3008226FD /* NetNewsWire_shareextension_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_shareextension_target.xcconfig; sourceTree = ""; }; 510C43F6243D035C009F70C3 /* ExtensionPoint.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPoint.swift; sourceTree = ""; }; + 510FFAB226EEA22C00F32265 /* ArticleThemesTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleThemesTableViewController.swift; sourceTree = ""; }; 51102164233A7D6C0007A5F7 /* ArticleExtractorButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButton.swift; sourceTree = ""; }; 51107745243BEE2500D97C8C /* ExtensionPointPreferencesViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPointPreferencesViewController.swift; sourceTree = ""; }; 5110C37C2373A8D100A9C04F /* InspectorIconHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorIconHeaderView.swift; sourceTree = ""; }; @@ -1648,14 +1205,13 @@ 5132779E2591034D0064F1E7 /* icon.icns */ = {isa = PBXFileReference; lastKnownFileType = image.icns; path = icon.icns; sourceTree = ""; }; 51333D1524685D2E00EB5C91 /* AddRedditFeedWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddRedditFeedWindowController.swift; sourceTree = ""; }; 51333D3A2468615D00EB5C91 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Mac/Base.lproj/AddRedditFeedSheet.xib; sourceTree = SOURCE_ROOT; }; + 5137C2E326F3F52D009EFEDB /* Sepia.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Sepia.nnwtheme; sourceTree = ""; }; + 5137C2E926F63AE6009EFEDB /* ArticleThemeImporter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleThemeImporter.swift; sourceTree = ""; }; 51386A8D25673276005F3762 /* AccountCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCell.swift; sourceTree = ""; }; - 51392D1A24AC19A000BE0D35 /* SidebarExpandedContainers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarExpandedContainers.swift; sourceTree = ""; }; 513C5CE6232571C2003D4054 /* NetNewsWire iOS Share Extension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NetNewsWire iOS Share Extension.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 513C5CE8232571C2003D4054 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = ""; }; 513C5CEB232571C2003D4054 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/MainInterface.storyboard; sourceTree = ""; }; 513C5CED232571C2003D4054 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 513CCF08248808BA00C55709 /* MasterFeedTableViewIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedTableViewIdentifier.swift; sourceTree = ""; }; - 51408B7D24A9EC6F0073CF4E /* SidebarItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarItem.swift; sourceTree = ""; }; 5141E7382373C18B0013FF27 /* WebFeedInspectorViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebFeedInspectorViewController.swift; sourceTree = ""; }; 5141E7552374A2890013FF27 /* DetailIconSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetailIconSchemeHandler.swift; sourceTree = ""; }; 5142192923522B5500E07E2C /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = ""; }; @@ -1674,11 +1230,6 @@ 514A8980244FD63F0085E65D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Mac/Base.lproj/AddTwitterFeedSheet.xib; sourceTree = SOURCE_ROOT; }; 514A89A4244FD6640085E65D /* AddTwitterFeedWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = AddTwitterFeedWindowController.swift; path = AddFeed/AddTwitterFeedWindowController.swift; sourceTree = ""; }; 514B7C8223205EFB00BAC947 /* RootSplitViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RootSplitViewController.swift; sourceTree = ""; }; - 514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemView.swift; sourceTree = ""; }; - 514E6BFE24AD255D00AC6F6E /* PreviewArticles.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreviewArticles.swift; sourceTree = ""; }; - 514E6C0124AD29A300AC6F6E /* TimelineItemStatusView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItemStatusView.swift; sourceTree = ""; }; - 514E6C0524AD2B5F00AC6F6E /* Image-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Image-Extensions.swift"; sourceTree = ""; }; - 514E6C0824AD39AD00AC6F6E /* ArticleIconImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleIconImageLoader.swift; sourceTree = ""; }; 515A50E5243D07A90089E588 /* ExtensionPointManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPointManager.swift; sourceTree = ""; }; 515A5106243D0CCD0089E588 /* TwitterFeedProvider-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "TwitterFeedProvider-Extensions.swift"; sourceTree = ""; }; 515A5147243E64BA0089E588 /* ExtensionPointEnableWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionPointEnableWindowController.swift; sourceTree = ""; }; @@ -1691,7 +1242,6 @@ 516244E2241E19F000B61C47 /* ColorPaletteTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPaletteTableViewController.swift; sourceTree = ""; }; 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drag.swift"; sourceTree = ""; }; 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MasterFeedViewController+Drop.swift"; sourceTree = ""; }; - 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSource.swift; sourceTree = ""; }; 51627A92238A3836007B3B4B /* CroppingPreviewParameters.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CroppingPreviewParameters.swift; sourceTree = ""; }; 516A091D23609A3600EAE89B /* SettingsComboTableViewCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = SettingsComboTableViewCell.xib; sourceTree = ""; }; 516A09382360A2AE00EAE89B /* SettingsComboTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsComboTableViewCell.swift; sourceTree = ""; }; @@ -1708,35 +1258,7 @@ 51707438232AA97100A461A3 /* ShareFolderPickerController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerController.swift; sourceTree = ""; }; 517630032336215100E15FFF /* main.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = main.js; sourceTree = ""; }; 517630222336657E00E15FFF /* WebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProvider.swift; sourceTree = ""; }; - 5177470224B2657F00EB0F74 /* TimelineToolbarModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineToolbarModifier.swift; sourceTree = ""; }; - 5177470524B2910300EB0F74 /* ArticleToolbarModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleToolbarModifier.swift; sourceTree = ""; }; - 5177470824B2F87600EB0F74 /* SidebarListStyleModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarListStyleModifier.swift; sourceTree = ""; }; - 5177470D24B2FF6F00EB0F74 /* ArticleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleView.swift; sourceTree = ""; }; - 5177470F24B3029400EB0F74 /* ArticleViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleViewController.swift; sourceTree = ""; }; - 5177471124B37C5400EB0F74 /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; - 5177471324B37D4000EB0F74 /* PreloadedWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloadedWebView.swift; sourceTree = ""; }; - 5177471524B37D9700EB0F74 /* ArticleIconSchemeHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleIconSchemeHandler.swift; sourceTree = ""; }; - 5177471724B3812200EB0F74 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = ""; }; - 5177471924B3863000EB0F74 /* WebViewProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewProvider.swift; sourceTree = ""; }; - 5177471B24B387AC00EB0F74 /* ImageScrollView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageScrollView.swift; sourceTree = ""; }; - 5177471D24B387E100EB0F74 /* ImageTransition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageTransition.swift; sourceTree = ""; }; - 5177471F24B3882600EB0F74 /* ImageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageViewController.swift; sourceTree = ""; }; - 5177472124B38CAE00EB0F74 /* ArticleExtractorButtonState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleExtractorButtonState.swift; sourceTree = ""; }; - 5177475824B39AD400EB0F74 /* Credits.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = Credits.rtf; sourceTree = ""; }; - 5177475924B39AD400EB0F74 /* Dedication.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = Dedication.rtf; sourceTree = ""; }; - 5177475A24B39AD500EB0F74 /* Thanks.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = Thanks.rtf; sourceTree = ""; }; - 5177475B24B39AD500EB0F74 /* About.rtf */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.rtf; path = About.rtf; sourceTree = ""; }; - 5177476024B3BC4700EB0F74 /* SettingsAboutView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsAboutView.swift; sourceTree = ""; }; - 5177476424B3BDAE00EB0F74 /* AttributedStringView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AttributedStringView.swift; sourceTree = ""; }; - 5177476624B3BE3400EB0F74 /* SettingsAboutModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAboutModel.swift; sourceTree = ""; }; 517A745A2443665000B553B9 /* UIPageViewController-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIPageViewController-Extensions.swift"; sourceTree = ""; }; - 517B2EBB24B3E62A001AC46C /* WrapperScriptMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapperScriptMessageHandler.swift; sourceTree = ""; }; - 517B2EDE24B3E8FE001AC46C /* page.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page.html; sourceTree = ""; }; - 517B2EDF24B3E8FE001AC46C /* blank.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = blank.html; sourceTree = ""; }; - 517B2EE024B3E8FE001AC46C /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = ""; }; - 517B2EE124B3E8FE001AC46C /* main_multiplatform.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = main_multiplatform.js; sourceTree = ""; }; - 5181C5AC24AF89B1002E0F70 /* PreferredColorSchemeModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferredColorSchemeModifier.swift; sourceTree = ""; }; - 5181C66124B0C326002E0F70 /* SettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsModel.swift; sourceTree = ""; }; 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicLabel.swift; sourceTree = ""; }; 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicImageView.swift; sourceTree = ""; }; 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshInterval.swift; sourceTree = ""; }; @@ -1749,21 +1271,12 @@ 518B2ED22351B3DD00400001 /* NetNewsWire-iOSTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "NetNewsWire-iOSTests.xctest"; sourceTree = BUILT_PRODUCTS_DIR; }; 518B2EE92351B4C200400001 /* NetNewsWire_iOSTests_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_iOSTests_target.xcconfig; sourceTree = ""; }; 518ED21C23D0F26000E0A862 /* UIViewController-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIViewController-Extensions.swift"; sourceTree = ""; }; - 51919FA524AA64B000541E64 /* SidebarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarView.swift; sourceTree = ""; }; - 51919FAB24AA8CCA00541E64 /* UnreadCountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UnreadCountView.swift; sourceTree = ""; }; - 51919FAE24AA8EFA00541E64 /* SidebarItemView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarItemView.swift; sourceTree = ""; }; - 51919FB224AAB97900541E64 /* FeedIconImageLoader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedIconImageLoader.swift; sourceTree = ""; }; - 51919FB524AABCA100541E64 /* IconImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconImageView.swift; sourceTree = ""; }; - 51919FED24AB85E400541E64 /* TimelineContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineContainerView.swift; sourceTree = ""; }; - 51919FF024AB864A00541E64 /* TimelineModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineModel.swift; sourceTree = ""; }; - 51919FF324AB869C00541E64 /* TimelineItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItem.swift; sourceTree = ""; }; - 51919FF624AB8B7700541E64 /* TimelineView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineView.swift; sourceTree = ""; }; 51934CC1230F5963006127BE /* InteractiveNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractiveNavigationController.swift; sourceTree = ""; }; 51934CCD2310792F006127BE /* ActivityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityManager.swift; sourceTree = ""; }; 51938DF1231AFC660055A1A0 /* SearchTimelineFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchTimelineFeedDelegate.swift; sourceTree = ""; }; 5193CD57245E44A90092735E /* RedditFeedProvider-Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RedditFeedProvider-Extensions.swift"; sourceTree = ""; }; - 5194736D24BBB937001A2939 /* HiddenModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HiddenModifier.swift; sourceTree = ""; }; - 5194737024BBCAF4001A2939 /* TimelineSortOrderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineSortOrderView.swift; sourceTree = ""; }; + 5195C1D92720205F00888867 /* ShadowTableChanges.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShadowTableChanges.swift; sourceTree = ""; }; + 5195C1DB2720BD3000888867 /* MasterFeedRowIdentifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedRowIdentifier.swift; sourceTree = ""; }; 519B8D322143397200FA689C /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = ""; }; 519E743422C663F900A78E47 /* SceneDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddExtensionPointViewController.swift; sourceTree = ""; }; @@ -1777,58 +1290,24 @@ 51A16993235E10D600EB091F /* SettingsViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = ""; }; 51A16995235E10D600EB091F /* AboutViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AboutViewController.swift; sourceTree = ""; }; 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = ""; }; - 51A5769524AE617200078888 /* ArticleContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleContainerView.swift; sourceTree = ""; }; 51A66684238075AE00CB272D /* AddWebFeedDefaultContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedDefaultContainer.swift; sourceTree = ""; }; - 51A8001124CA0FC700F41F1D /* Sink.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Sink.swift; sourceTree = ""; }; - 51A8001424CA0FEC00F41F1D /* DemandBuffer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DemandBuffer.swift; sourceTree = ""; }; - 51A8002C24CC451500F41F1D /* ShareReplay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareReplay.swift; sourceTree = ""; }; - 51A8005024CC453C00F41F1D /* ReplaySubject.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReplaySubject.swift; sourceTree = ""; }; - 51A8FFEC24CA0CF400F41F1D /* WIthLatestFrom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WIthLatestFrom.swift; sourceTree = ""; }; 51A9A5E32380C8870033AADF /* ShareFolderPickerAccountCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareFolderPickerAccountCell.xib; sourceTree = ""; }; 51A9A5E52380C8B20033AADF /* ShareFolderPickerFolderCell.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = ShareFolderPickerFolderCell.xib; sourceTree = ""; }; 51A9A5E72380CA130033AADF /* ShareFolderPickerCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareFolderPickerCell.swift; sourceTree = ""; }; 51A9A5F42380F6A60033AADF /* ModalNavigationController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModalNavigationController.swift; sourceTree = ""; }; 51A9A6092382FD240033AADF /* PoppableGestureRecognizerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoppableGestureRecognizerDelegate.swift; sourceTree = ""; }; 51AB8AB223B7F4C6008F147D /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; - 51B54A6824B54A490014348B /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = ""; }; - 51B54AB524B5B33C0014348B /* WebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebViewController.swift; sourceTree = ""; }; - 51B54ABB24B5BEF20014348B /* ArticleView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleView.swift; sourceTree = ""; }; - 51B54B6624B6A7960014348B /* WebStatusBarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebStatusBarView.swift; sourceTree = ""; }; 51B5C87623F22B8200032075 /* ExtensionContainers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionContainers.swift; sourceTree = ""; }; 51B5C87A23F2317700032075 /* ExtensionFeedAddRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionFeedAddRequest.swift; sourceTree = ""; }; 51B5C87C23F2346200032075 /* ExtensionContainersFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionContainersFile.swift; sourceTree = ""; }; 51B5C8BC23F3780900032075 /* ShareDefaultContainer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareDefaultContainer.swift; sourceTree = ""; }; 51B5C8BF23F3866C00032075 /* ExtensionFeedAddRequestFile.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExtensionFeedAddRequestFile.swift; sourceTree = ""; }; 51B62E67233186730085F949 /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = ""; }; - 51B80EB724BD1F8B00C6C32D /* ActivityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityViewController.swift; sourceTree = ""; }; - 51B80EDA24BD225200C6C32D /* OpenInSafariActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OpenInSafariActivity.swift; sourceTree = ""; }; - 51B80EDC24BD296700C6C32D /* ArticleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleActivityItemSource.swift; sourceTree = ""; }; - 51B80EDE24BD298900C6C32D /* TitleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TitleActivityItemSource.swift; sourceTree = ""; }; - 51B80EE024BD3E9600C6C32D /* FindInArticleActivity.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FindInArticleActivity.swift; sourceTree = ""; }; - 51B80F1E24BE531200C6C32D /* SharingServiceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceView.swift; sourceTree = ""; }; - 51B80F4124BE588200C6C32D /* SharingServicePickerDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServicePickerDelegate.swift; sourceTree = ""; }; - 51B80F4324BE58BF00C6C32D /* SharingServiceDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SharingServiceDelegate.swift; sourceTree = ""; }; - 51B80F4524BF76E700C6C32D /* Browser.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Browser.swift; sourceTree = ""; }; - 51B8104424C0E6D200C6C32D /* TimelineTextSizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTextSizer.swift; sourceTree = ""; }; - 51B8BCC124C25C3E00360B00 /* SidebarContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarContextMenu.swift; sourceTree = ""; }; - 51B8BCE524C25F7C00360B00 /* TimelineContextMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineContextMenu.swift; sourceTree = ""; }; 51BB7C262335A8E5008E8144 /* ArticleActivityItemSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleActivityItemSource.swift; sourceTree = ""; }; 51BB7C302335ACDE008E8144 /* page.html */ = {isa = PBXFileReference; lastKnownFileType = text.html; path = page.html; sourceTree = ""; }; 51BC4ADD247277DF000A6ED8 /* URL-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "URL-Extensions.swift"; sourceTree = ""; }; 51BEB22C2451E8340066DEDD /* TwitterEnterDetailTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TwitterEnterDetailTableViewController.swift; sourceTree = ""; }; 51C03080257D815A00609262 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Mac/Base.lproj/UnifiedWindow.storyboard; sourceTree = SOURCE_ROOT; }; - 51C0513624A77DF700194D5E /* MainApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainApp.swift; sourceTree = ""; }; - 51C0513824A77DF800194D5E /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 51C0513D24A77DF800194D5E /* NetNewsWire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetNewsWire.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 51C0513F24A77DF800194D5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 51C0514424A77DF800194D5E /* NetNewsWire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetNewsWire.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 51C0514624A77DF800194D5E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - 51C0514724A77DF800194D5E /* macOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = macOS.entitlements; sourceTree = ""; }; - 51C0519724A7808F00194D5E /* NetNewsWire_multiplatform_iOSapp_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_multiplatform_iOSapp_target.xcconfig; sourceTree = ""; }; - 51C0519824A7808F00194D5E /* NetNewsWire_multiplatform_macOSapp_target.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_multiplatform_macOSapp_target.xcconfig; sourceTree = ""; }; - 51C051CD24A7A6DB00194D5E /* macOS-dev.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "macOS-dev.entitlements"; sourceTree = ""; }; - 51C051CE24A7A72100194D5E /* iOS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = iOS.entitlements; sourceTree = ""; }; - 51C051CF24A7A72100194D5E /* iOS-dev.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = "iOS-dev.entitlements"; sourceTree = ""; }; 51C266E9238C334800F53014 /* ContextMenuPreviewViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContextMenuPreviewViewController.swift; sourceTree = ""; }; 51C4524E226506F400C03939 /* UIStoryboard-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "UIStoryboard-Extensions.swift"; sourceTree = ""; }; 51C45250226506F400C03939 /* String-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "String-Extensions.swift"; sourceTree = ""; }; @@ -1849,9 +1328,7 @@ 51C452842265093600C03939 /* AddFeedViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFeedViewController.swift; sourceTree = ""; }; 51C4528B2265095F00C03939 /* AddFolderViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderViewController.swift; sourceTree = ""; }; 51C452B32265141B00C03939 /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS12.2.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; }; - 51C452B72265178500C03939 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = ""; }; 51C4CFEF24D37D1F00AF9874 /* Secrets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Secrets.swift; sourceTree = ""; }; - 51C65AFB24CCB2C9008EB3BD /* TimelineItems.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineItems.swift; sourceTree = ""; }; 51C9DE5723EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WrapperScriptMessageHandler.swift; sourceTree = ""; }; 51CD32A824D2CB25009ABAEF /* SyncDatabase */ = {isa = PBXFileReference; lastKnownFileType = folder; path = SyncDatabase; sourceTree = ""; }; 51CD32C324D2CD57009ABAEF /* ArticlesDatabase */ = {isa = PBXFileReference; lastKnownFileType = folder; path = ArticlesDatabase; sourceTree = ""; }; @@ -1860,12 +1337,13 @@ 51CD32C724D2E06C009ABAEF /* Secrets */ = {isa = PBXFileReference; lastKnownFileType = folder; path = Secrets; sourceTree = ""; }; 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = RefreshProgressView.xib; sourceTree = ""; }; 51CE1C0A23622006005548FC /* RefreshProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshProgressView.swift; sourceTree = ""; }; + 51D0214526ED617100FF2E0F /* core.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = core.css; sourceTree = ""; }; 51D6A5BB23199C85001C27D8 /* MasterTimelineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterTimelineDataSource.swift; sourceTree = ""; }; 51D87EE02311D34700E63F03 /* ActivityType.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ActivityType.swift; sourceTree = ""; }; 51DC07972552083500A3F79F /* ArticleTextSize.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleTextSize.swift; sourceTree = ""; }; - 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UpdateSelectionOperation.swift; sourceTree = ""; }; - 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterFeedDataSourceOperation.swift; sourceTree = ""; }; 51DC370A2405BC9A0095D371 /* PreloadedWebView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreloadedWebView.swift; sourceTree = ""; }; + 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Appanoose.nnwtheme; sourceTree = ""; }; + 51DEE81726FBFF84006DAA56 /* Promenade.nnwtheme */ = {isa = PBXFileReference; lastKnownFileType = wrapper; path = Promenade.nnwtheme; sourceTree = ""; }; 51E36E70239D6610006F47A5 /* AddFeedSelectFolderTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedSelectFolderTableViewCell.swift; sourceTree = ""; }; 51E36E8B239D6765006F47A5 /* AddFeedSelectFolderTableViewCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = AddFeedSelectFolderTableViewCell.xib; sourceTree = ""; }; 51E3EB32229AB02C00645299 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; @@ -1875,14 +1353,6 @@ 51E4989624A8065700B667CB /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/CloudKit.framework; sourceTree = DEVELOPER_DIR; }; 51E4989824A8067000B667CB /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS14.0.sdk/System/Library/Frameworks/WebKit.framework; sourceTree = DEVELOPER_DIR; }; 51E498B224A806AA00B667CB /* WebKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WebKit.framework; path = System/Library/Frameworks/WebKit.framework; sourceTree = SDKROOT; }; - 51E4992524A80AAB00B667CB /* AppAssets.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppAssets.swift; sourceTree = ""; }; - 51E4993924A8708800B667CB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 51E4993B24A8709900B667CB /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 51E4995824A873F900B667CB /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; - 51E499D724A912C200B667CB /* SceneModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneModel.swift; sourceTree = ""; }; - 51E499FC24A9137600B667CB /* SidebarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarModel.swift; sourceTree = ""; }; - 51E499FF24A91FC100B667CB /* SidebarContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarContainerView.swift; sourceTree = ""; }; - 51E49A0224A91FF600B667CB /* SceneNavigationView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneNavigationView.swift; sourceTree = ""; }; 51E4DAEC2425F6940091EB5B /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = System/Library/Frameworks/CloudKit.framework; sourceTree = SDKROOT; }; 51E4DB072425F9EB0091EB5B /* CloudKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CloudKit.framework; path = Platforms/iPhoneOS.platform/Developer/SDKs/iPhoneOS13.2.sdk/System/Library/Frameworks/CloudKit.framework; sourceTree = DEVELOPER_DIR; }; 51E595A4228CC36500FCC42B /* ArticleStatusSyncTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleStatusSyncTimer.swift; sourceTree = ""; }; @@ -1915,13 +1385,8 @@ 55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsReaderAPI.xib; sourceTree = ""; }; 55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsReaderAPIWindowController.swift; sourceTree = ""; }; 5F323808231DF9F000706F6B /* VibrantTableViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VibrantTableViewCell.swift; sourceTree = ""; }; - 65082A2E24C72AC8009FA994 /* SettingsCredentialsAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCredentialsAccountView.swift; sourceTree = ""; }; - 65082A5124C72B88009FA994 /* SettingsCredentialsAccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCredentialsAccountModel.swift; sourceTree = ""; }; - 65082A5324C73D2F009FA994 /* AccountCredentialsError.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCredentialsError.swift; sourceTree = ""; }; 653813592680E2DA007A082C /* NetNewsWire Share Extension MAS.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "NetNewsWire Share Extension MAS.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 6538135B2680E3A9007A082C /* NetNewsWire_shareextension_target_macappstore.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = NetNewsWire_shareextension_target_macappstore.xcconfig; sourceTree = ""; }; - 653A4E7824BCA5BB00EF2D7F /* SettingsCloudKitAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsCloudKitAccountView.swift; sourceTree = ""; }; - 65422D1624B75CD1008A2FA2 /* SettingsAddAccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAddAccountModel.swift; sourceTree = ""; }; 6543108B2322D90900658221 /* common */ = {isa = PBXFileReference; lastKnownFileType = folder; path = common; sourceTree = ""; }; 6581C73320CED60000F4AD34 /* Subscribe to Feed.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Subscribe to Feed.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 6581C73420CED60100F4AD34 /* Cocoa.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Cocoa.framework; path = System/Library/Frameworks/Cocoa.framework; sourceTree = SDKROOT; }; @@ -1932,14 +1397,6 @@ 6581C73F20CED60100F4AD34 /* netnewswire-subscribe-to-feed.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "netnewswire-subscribe-to-feed.js"; sourceTree = ""; }; 6581C74120CED60100F4AD34 /* ToolbarItemIcon.pdf */ = {isa = PBXFileReference; lastKnownFileType = image.pdf; path = ToolbarItemIcon.pdf; sourceTree = ""; }; 6581C74320CED60100F4AD34 /* Subscribe_to_Feed.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Subscribe_to_Feed.entitlements; sourceTree = ""; }; - 6586A5F624B632F8002BCF4F /* SettingsDetailAccountModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDetailAccountModel.swift; sourceTree = ""; }; - 6591723024B5C35400B638E8 /* AccountHeaderImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountHeaderImageView.swift; sourceTree = ""; }; - 6591727E24B5D19500B638E8 /* SettingsDetailAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsDetailAccountView.swift; sourceTree = ""; }; - 65ACE48324B4779B003AE06A /* SettingsAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAddAccountView.swift; sourceTree = ""; }; - 65ACE48524B477C9003AE06A /* SettingsAccountLabelView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAccountLabelView.swift; sourceTree = ""; }; - 65ACE48724B48020003AE06A /* SettingsLocalAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLocalAccountView.swift; sourceTree = ""; }; - 65C2E40024B05D8A000AFDF6 /* FeedsSettingsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedsSettingsModel.swift; sourceTree = ""; }; - 65CBAD5924AE03C20006DD91 /* ColorPaletteContainerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ColorPaletteContainerView.swift; sourceTree = ""; }; 65ED4083235DEF6C0081F399 /* NetNewsWire.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = NetNewsWire.app; sourceTree = BUILT_PRODUCTS_DIR; }; 65ED409D235DEF770081F399 /* Subscribe to Feed MAS.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = "Subscribe to Feed MAS.appex"; sourceTree = BUILT_PRODUCTS_DIR; }; 65ED409F235DEFF00081F399 /* container-migration.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = "container-migration.plist"; sourceTree = ""; }; @@ -1989,7 +1446,6 @@ 8477ACBD22238E9500DF7F37 /* SearchFeedDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchFeedDelegate.swift; sourceTree = ""; }; 847CD6C9232F4CBF00FAC46D /* IconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IconView.swift; sourceTree = ""; }; 847E64942262782F00E00365 /* NSAppleEventDescriptor+UserRecordFields.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAppleEventDescriptor+UserRecordFields.swift"; sourceTree = ""; }; - 848362FC2262A30800DA1D35 /* styleSheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = styleSheet.css; sourceTree = ""; }; 848362FE2262A30E00DA1D35 /* template.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = template.html; sourceTree = ""; }; 848363012262A3BC00DA1D35 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Mac/Base.lproj/AddWebFeedSheet.xib; sourceTree = SOURCE_ROOT; }; 848363042262A3CC00DA1D35 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Mac/Base.lproj/AddFolderSheet.xib; sourceTree = SOURCE_ROOT; }; @@ -2021,8 +1477,8 @@ 849A977D1ED9EC42007D329B /* ArticleRenderer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleRenderer.swift; sourceTree = ""; }; 849A977E1ED9EC42007D329B /* DetailViewController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DetailViewController.swift; sourceTree = ""; }; 849A97821ED9EC63007D329B /* SidebarStatusBarView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarStatusBarView.swift; sourceTree = ""; }; - 849A97871ED9ECEF007D329B /* ArticleStyle.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleStyle.swift; sourceTree = ""; }; - 849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleStylesManager.swift; sourceTree = ""; }; + 849A97871ED9ECEF007D329B /* ArticleTheme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleTheme.swift; sourceTree = ""; }; + 849A97881ED9ECEF007D329B /* ArticleThemesManager.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ArticleThemesManager.swift; sourceTree = ""; }; 849A97971ED9EFAA007D329B /* Node-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "Node-Extensions.swift"; sourceTree = ""; }; 849A979E1ED9F130007D329B /* SidebarCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SidebarCell.swift; sourceTree = ""; }; 849A97A11ED9F180007D329B /* FolderTreeControllerDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FolderTreeControllerDelegate.swift; sourceTree = ""; }; @@ -2107,7 +1563,7 @@ B24E9ABA245AB88300DA5718 /* NSAttributedString+NetNewsWire.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSAttributedString+NetNewsWire.swift"; sourceTree = ""; }; B24EFD482330FF99006C6242 /* NetNewsWire-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-Bridging-Header.h"; sourceTree = ""; }; B24EFD5923310109006C6242 /* WKPreferencesPrivate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = WKPreferencesPrivate.h; sourceTree = ""; }; - B27EEBDF244D15F2000932E6 /* shared.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = shared.css; sourceTree = ""; }; + B27EEBDF244D15F2000932E6 /* stylesheet.css */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.css; path = stylesheet.css; sourceTree = ""; }; B2B8075D239C49D300F191E0 /* RSImage-AppIcons.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RSImage-AppIcons.swift"; sourceTree = ""; }; B528F81D23333C7E00E735DD /* page.html */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.html; path = page.html; sourceTree = ""; }; BDCB514D24282C8A00102A80 /* AccountsNewsBlur.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsNewsBlur.xib; sourceTree = ""; }; @@ -2133,15 +1589,8 @@ D5F4EDB620074D6500B9E363 /* WebFeed+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "WebFeed+Scriptability.swift"; sourceTree = ""; }; D5F4EDB820074D7C00B9E363 /* Folder+Scriptability.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "Folder+Scriptability.swift"; sourceTree = ""; }; DD82AB09231003F6002269DF /* SharingTests.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SharingTests.swift; sourceTree = ""; }; - DF98E2992578A73A00F18944 /* AddCloudKitAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddCloudKitAccountView.swift; sourceTree = ""; }; - DF98E2AF2578AA5C00F18944 /* AddFeedWranglerAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFeedWranglerAccountView.swift; sourceTree = ""; }; - DF98E2BD2578AC0000F18944 /* AddNewsBlurAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddNewsBlurAccountView.swift; sourceTree = ""; }; - DF98E2C52578AD1B00F18944 /* AddReaderAPIAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddReaderAPIAccountView.swift; sourceTree = ""; }; - FA80C11624B0728000974098 /* AddFolderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFolderView.swift; sourceTree = ""; }; - FA80C13D24B072AA00974098 /* AddFolderModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddFolderModel.swift; sourceTree = ""; }; FF3ABF09232599450074C542 /* ArticleSorterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorterTests.swift; sourceTree = ""; }; FF3ABF1423259DDB0074C542 /* ArticleSorter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleSorter.swift; sourceTree = ""; }; - FF64D0C424AF53EE0084080A /* RefreshProgressModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RefreshProgressModel.swift; sourceTree = ""; }; FFD43E372340F320009E5CA3 /* MarkAsReadAlertController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkAsReadAlertController.swift; sourceTree = ""; }; /* End PBXFileReference section */ @@ -2192,47 +1641,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 51C0513A24A77DF800194D5E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 17A1598224E3DEDD005DA32A /* RSWeb in Frameworks */, - 17A1597F24E3DEDD005DA32A /* RSTree in Frameworks */, - 17A1598824E3DEDD005DA32A /* RSParser in Frameworks */, - 17A1597C24E3DEDD005DA32A /* RSCore in Frameworks */, - 516B695D24D2F28E00B5702F /* Account in Frameworks */, - 17A1598524E3DEDD005DA32A /* RSDatabase in Frameworks */, - 27B86EEC25A53AAB00264340 /* Articles in Frameworks */, - 27B86EEE25A53AAB00264340 /* Secrets in Frameworks */, - 51E4989724A8065700B667CB /* CloudKit.framework in Frameworks */, - 27B86EED25A53AAB00264340 /* ArticlesDatabase in Frameworks */, - 27B86EEF25A53AAB00264340 /* SyncDatabase in Frameworks */, - 51E4989924A8067000B667CB /* WebKit.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51C0514124A77DF800194D5E /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 51E0614E25A5A28E00194066 /* SyncDatabase in Frameworks */, - 51E0614825A5A28E00194066 /* ArticlesDatabase in Frameworks */, - 17386BA42577C6240014C8B2 /* RSParser in Frameworks */, - 17386B952577C6240014C8B2 /* RSCore in Frameworks */, - 51E0614B25A5A28E00194066 /* Secrets in Frameworks */, - 17386B6C2577BD820014C8B2 /* RSSparkle in Frameworks */, - 51E0615125A5A29600194066 /* CrashReporter in Frameworks */, - 516B695B24D2F28600B5702F /* Account in Frameworks */, - 17386B9B2577C6240014C8B2 /* RSWeb in Frameworks */, - 17386B9E2577C6240014C8B2 /* RSDatabase in Frameworks */, - 17386BB62577C7340014C8B2 /* RSCoreResources in Frameworks */, - 51E0614525A5A28E00194066 /* Articles in Frameworks */, - 51E498B124A806A400B667CB /* CloudKit.framework in Frameworks */, - 51E498B324A806AA00B667CB /* WebKit.framework in Frameworks */, - 17386B982577C6240014C8B2 /* RSTree in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 6538134C2680E2DA007A082C /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -2281,6 +1689,7 @@ files = ( 5138E94924D3416D00AFF0FE /* RSCore in Frameworks */, 5138E95824D3419000AFF0FE /* RSWeb in Frameworks */, + 179D280B26F6F93D003B2E0A /* Zip in Frameworks */, 516B695F24D2F33B00B5702F /* Account in Frameworks */, 5138E95224D3418100AFF0FE /* RSParser in Frameworks */, 5138E94C24D3417A00AFF0FE /* RSDatabase in Frameworks */, @@ -2307,6 +1716,7 @@ 51C4CFF624D37DD500AF9874 /* Secrets in Frameworks */, 51A737AE24DB19730015FA66 /* RSCore in Frameworks */, 51A737C824DB19CC0015FA66 /* RSParser in Frameworks */, + 179C39EA26F76B0500D4E741 /* Zip in Frameworks */, 51E4DAED2425F6940091EB5B /* CloudKit.framework in Frameworks */, 514C16E124D2EF38009A3AFA /* RSCoreResources in Frameworks */, 514C16CE24D2E63F009A3AFA /* Account in Frameworks */, @@ -2327,83 +1737,6 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ - 171BCBB124CBD569006E22D9 /* Account Management */ = { - isa = PBXGroup; - children = ( - 171BCB8B24CB08A3006E22D9 /* FixAccountCredentialView.swift */, - ); - path = "Account Management"; - sourceTree = ""; - }; - 172199EB24AB228E00A31D04 /* Settings */ = { - isa = PBXGroup; - children = ( - 65C2E40024B05D8A000AFDF6 /* FeedsSettingsModel.swift */, - 65CBAD5924AE03C20006DD91 /* ColorPaletteContainerView.swift */, - 5181C66124B0C326002E0F70 /* SettingsModel.swift */, - 172199C824AB228900A31D04 /* SettingsView.swift */, - 17B223DB24AC24D2001E4592 /* TimelineLayoutView.swift */, - 65ACE46124B47770003AE06A /* Accounts */, - 5177475724B399B500EB0F74 /* About */, - ); - path = Settings; - sourceTree = ""; - }; - 17241268257BBE7B00ACCEBC /* Add Account Models */ = { - isa = PBXGroup; - children = ( - 17241269257BBEBB00ACCEBC /* AddFeedbinViewModel.swift */, - 17241277257BBEE700ACCEBC /* AddFeedlyViewModel.swift */, - 1724127F257BBF3E00ACCEBC /* AddFeedWranglerViewModel.swift */, - 17241287257BBF7000ACCEBC /* AddNewsBlurViewModel.swift */, - 1724128F257BBFAD00ACCEBC /* AddReaderAPIViewModel.swift */, - 17D3CEE2257C4D2300E74939 /* AddAccountSignUp.swift */, - ); - path = "Add Account Models"; - sourceTree = ""; - }; - 1727B37624C1365300A4DBDC /* Viewing */ = { - isa = PBXGroup; - children = ( - 1727B39824C1368D00A4DBDC /* LayoutPreferencesView.swift */, - ); - path = Viewing; - sourceTree = ""; - }; - 1729528F24AA1A4F00D65E66 /* Preferences */ = { - isa = PBXGroup; - children = ( - 1729529624AA1CD000D65E66 /* MacPreferencePanes.swift */, - 1729529924AA1CE100D65E66 /* Preference Panes */, - ); - path = Preferences; - sourceTree = ""; - }; - 1729529924AA1CE100D65E66 /* Preference Panes */ = { - isa = PBXGroup; - children = ( - 1769E2FD24BC589E000E1E8E /* General */, - 1769E31F24BC58A4000E1E8E /* Accounts */, - 1727B37624C1365300A4DBDC /* Viewing */, - 1769E32024BC58AD000E1E8E /* Advanced */, - ); - path = "Preference Panes"; - sourceTree = ""; - }; - 17386B812577C4C60014C8B2 /* Add Account Sheets */ = { - isa = PBXGroup; - children = ( - 17386B792577C4BF0014C8B2 /* AddLocalAccountView.swift */, - DF98E2992578A73A00F18944 /* AddCloudKitAccountView.swift */, - 17386BC32577CC600014C8B2 /* AddFeedbinAccountView.swift */, - 17241248257B8A8A00ACCEBC /* AddFeedlyAccountView.swift */, - DF98E2AF2578AA5C00F18944 /* AddFeedWranglerAccountView.swift */, - DF98E2BD2578AC0000F18944 /* AddNewsBlurAccountView.swift */, - DF98E2C52578AD1B00F18944 /* AddReaderAPIAccountView.swift */, - ); - path = "Add Account Sheets"; - sourceTree = ""; - }; 176813A22564B9D100D98635 /* Widget */ = { isa = PBXGroup; children = ( @@ -2461,86 +1794,6 @@ path = Resources; sourceTree = ""; }; - 1769E2FD24BC589E000E1E8E /* General */ = { - isa = PBXGroup; - children = ( - 1717535524BADF33004498C6 /* GeneralPreferencesModel.swift */, - 1729529224AA1CAA00D65E66 /* GeneralPreferencesView.swift */, - ); - path = General; - sourceTree = ""; - }; - 1769E31F24BC58A4000E1E8E /* Accounts */ = { - isa = PBXGroup; - children = ( - 1769E33724BD97CB000E1E8E /* AccountUpdateErrors.swift */, - 1769E33924BD97E5000E1E8E /* Account Preferences */, - ); - path = Accounts; - sourceTree = ""; - }; - 1769E32024BC58AD000E1E8E /* Advanced */ = { - isa = PBXGroup; - children = ( - 17E4DBD524BFC53E00FE462A /* AdvancedPreferencesModel.swift */, - 1729529124AA1CAA00D65E66 /* AdvancedPreferencesView.swift */, - ); - path = Advanced; - sourceTree = ""; - }; - 1769E32324BC5A50000E1E8E /* Add Account */ = { - isa = PBXGroup; - children = ( - 1769E32424BC5A65000E1E8E /* AddAccountView.swift */, - ); - path = "Add Account"; - sourceTree = ""; - }; - 1769E32E24BD5F22000E1E8E /* Edit Account */ = { - isa = PBXGroup; - children = ( - 1769E32C24BD20A0000E1E8E /* AccountDetailView.swift */, - 1769E32F24BD6271000E1E8E /* EditAccountCredentialsView.swift */, - 1769E33524BD9621000E1E8E /* EditAccountCredentialsModel.swift */, - ); - path = "Edit Account"; - sourceTree = ""; - }; - 1769E33924BD97E5000E1E8E /* Account Preferences */ = { - isa = PBXGroup; - children = ( - 1769E32124BC5925000E1E8E /* AccountsPreferencesModel.swift */, - 1729529024AA1CAA00D65E66 /* AccountsPreferencesView.swift */, - 1769E32A24BCB030000E1E8E /* ConfiguredAccountRow.swift */, - 1769E32324BC5A50000E1E8E /* Add Account */, - 1769E32E24BD5F22000E1E8E /* Edit Account */, - ); - path = "Account Preferences"; - sourceTree = ""; - }; - 17897AA724C281520014BA03 /* Inspector */ = { - isa = PBXGroup; - children = ( - 17897AC924C281A40014BA03 /* InspectorView.swift */, - 1799E6A824C2F93F00511E91 /* InspectorPlatformModifier.swift */, - 1799E6CC24C320D600511E91 /* InspectorModel.swift */, - ); - path = Inspector; - sourceTree = ""; - }; - 17930ED224AF10CD00A9BA52 /* Add */ = { - isa = PBXGroup; - children = ( - 17D232A724AFF10A0005F075 /* AddWebFeedModel.swift */, - 17930ED324AF10EE00A9BA52 /* AddWebFeedView.swift */, - FA80C13D24B072AA00974098 /* AddFolderModel.swift */, - FA80C11624B0728000974098 /* AddFolderView.swift */, - 17241268257BBE7B00ACCEBC /* Add Account Models */, - 17386B812577C4C60014C8B2 /* Add Account Sheets */, - ); - path = Add; - sourceTree = ""; - }; 17D7586B2679C1DF00B17787 /* 1Password */ = { isa = PBXGroup; children = ( @@ -2607,6 +1860,9 @@ 844B5B681FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist */, 845479871FEB77C000AD8B59 /* TimelineKeyboardShortcuts.plist */, 5127B237222B4849006D641D /* DetailKeyboardShortcuts.plist */, + 51DEE81126FB9233006DAA56 /* Appanoose.nnwtheme */, + 51DEE81726FBFF84006DAA56 /* Promenade.nnwtheme */, + 5137C2E326F3F52D009EFEDB /* Sepia.nnwtheme */, ); path = Resources; sourceTree = ""; @@ -2685,24 +1941,6 @@ path = OPML; sourceTree = ""; }; - 514E6BFD24AD252400AC6F6E /* Previews */ = { - isa = PBXGroup; - children = ( - 514E6BFE24AD255D00AC6F6E /* PreviewArticles.swift */, - ); - path = Previews; - sourceTree = ""; - }; - 514E6C0424AD2B0400AC6F6E /* SwiftUI Extensions */ = { - isa = PBXGroup; - children = ( - 514E6C0524AD2B5F00AC6F6E /* Image-Extensions.swift */, - 5181C5AC24AF89B1002E0F70 /* PreferredColorSchemeModifier.swift */, - 5194736D24BBB937001A2939 /* HiddenModifier.swift */, - ); - path = "SwiftUI Extensions"; - sourceTree = ""; - }; 516A093E236123A800EAE89B /* Account */ = { isa = PBXGroup; children = ( @@ -2729,52 +1967,6 @@ path = Reddit; sourceTree = ""; }; - 5177470B24B2FF2C00EB0F74 /* Article */ = { - isa = PBXGroup; - children = ( - 51B80EB724BD1F8B00C6C32D /* ActivityViewController.swift */, - 51B80EDC24BD296700C6C32D /* ArticleActivityItemSource.swift */, - 5177470D24B2FF6F00EB0F74 /* ArticleView.swift */, - 5177470F24B3029400EB0F74 /* ArticleViewController.swift */, - 51B80EE024BD3E9600C6C32D /* FindInArticleActivity.swift */, - 5177471724B3812200EB0F74 /* IconView.swift */, - 5177471B24B387AC00EB0F74 /* ImageScrollView.swift */, - 5177471D24B387E100EB0F74 /* ImageTransition.swift */, - 5177471F24B3882600EB0F74 /* ImageViewController.swift */, - 51B80EDA24BD225200C6C32D /* OpenInSafariActivity.swift */, - 51B80EDE24BD298900C6C32D /* TitleActivityItemSource.swift */, - 5177471124B37C5400EB0F74 /* WebViewController.swift */, - ); - path = Article; - sourceTree = ""; - }; - 5177470C24B2FF3B00EB0F74 /* Article */ = { - isa = PBXGroup; - children = ( - 51B54ABB24B5BEF20014348B /* ArticleView.swift */, - 51B54A6824B54A490014348B /* IconView.swift */, - 51B80F4324BE58BF00C6C32D /* SharingServiceDelegate.swift */, - 51B80F4124BE588200C6C32D /* SharingServicePickerDelegate.swift */, - 51B80F1E24BE531200C6C32D /* SharingServiceView.swift */, - 51B54B6624B6A7960014348B /* WebStatusBarView.swift */, - 51B54AB524B5B33C0014348B /* WebViewController.swift */, - ); - path = Article; - sourceTree = ""; - }; - 5177475724B399B500EB0F74 /* About */ = { - isa = PBXGroup; - children = ( - 5177475B24B39AD500EB0F74 /* About.rtf */, - 5177475824B39AD400EB0F74 /* Credits.rtf */, - 5177475924B39AD400EB0F74 /* Dedication.rtf */, - 5177475A24B39AD500EB0F74 /* Thanks.rtf */, - 5177476624B3BE3400EB0F74 /* SettingsAboutModel.swift */, - 5177476024B3BC4700EB0F74 /* SettingsAboutView.swift */, - ); - path = About; - sourceTree = ""; - }; 5183CCEA226F70350010922C /* Timer */ = { isa = PBXGroup; children = ( @@ -2788,12 +1980,14 @@ 5183CCEB227117C70010922C /* Settings */ = { isa = PBXGroup; children = ( - 51A16990235E10D600EB091F /* Settings.storyboard */, 51A16995235E10D600EB091F /* AboutViewController.swift */, 51A16992235E10D600EB091F /* AddAccountViewController.swift */, - 519ED47924482AEB007F8E94 /* EnableExtensionPointViewController.swift */, 519ED455244828C3007F8E94 /* AddExtensionPointViewController.swift */, + 5137C2E926F63AE6009EFEDB /* ArticleThemeImporter.swift */, + 510FFAB226EEA22C00F32265 /* ArticleThemesTableViewController.swift */, 516244E2241E19F000B61C47 /* ColorPaletteTableViewController.swift */, + 519ED47924482AEB007F8E94 /* EnableExtensionPointViewController.swift */, + 51A16990235E10D600EB091F /* Settings.storyboard */, 516A09382360A2AE00EAE89B /* SettingsComboTableViewCell.swift */, 516A091D23609A3600EAE89B /* SettingsComboTableViewCell.xib */, 516A093A2360A4A000EAE89B /* SettingsTableViewCell.xib */, @@ -2815,34 +2009,6 @@ path = NNW3; sourceTree = ""; }; - 51919FB124AAB95300541E64 /* Images */ = { - isa = PBXGroup; - children = ( - 514E6C0824AD39AD00AC6F6E /* ArticleIconImageLoader.swift */, - 51919FB224AAB97900541E64 /* FeedIconImageLoader.swift */, - 51919FB524AABCA100541E64 /* IconImageView.swift */, - ); - path = Images; - sourceTree = ""; - }; - 51919FCB24AB855000541E64 /* Timeline */ = { - isa = PBXGroup; - children = ( - 51919FED24AB85E400541E64 /* TimelineContainerView.swift */, - 51B8BCE524C25F7C00360B00 /* TimelineContextMenu.swift */, - 51919FF324AB869C00541E64 /* TimelineItem.swift */, - 51C65AFB24CCB2C9008EB3BD /* TimelineItems.swift */, - 514E6C0124AD29A300AC6F6E /* TimelineItemStatusView.swift */, - 514E6BD924ACEA0400AC6F6E /* TimelineItemView.swift */, - 51919FF024AB864A00541E64 /* TimelineModel.swift */, - 5194737024BBCAF4001A2939 /* TimelineSortOrderView.swift */, - 51B8104424C0E6D200C6C32D /* TimelineTextSizer.swift */, - 5177470224B2657F00EB0F74 /* TimelineToolbarModifier.swift */, - 51919FF624AB8B7700541E64 /* TimelineView.swift */, - ); - path = Timeline; - sourceTree = ""; - }; 51934CCC231078DC006127BE /* Activity */ = { isa = PBXGroup; children = ( @@ -2852,36 +2018,6 @@ path = Activity; sourceTree = ""; }; - 51A576B924AE617B00078888 /* Article */ = { - isa = PBXGroup; - children = ( - 517B2EE024B3E8FE001AC46C /* styleSheet.css */, - 517B2EDF24B3E8FE001AC46C /* blank.html */, - 517B2EDE24B3E8FE001AC46C /* page.html */, - 517B2EE124B3E8FE001AC46C /* main_multiplatform.js */, - 51A5769524AE617200078888 /* ArticleContainerView.swift */, - 5177472124B38CAE00EB0F74 /* ArticleExtractorButtonState.swift */, - 5177471524B37D9700EB0F74 /* ArticleIconSchemeHandler.swift */, - 5177470524B2910300EB0F74 /* ArticleToolbarModifier.swift */, - 5177471324B37D4000EB0F74 /* PreloadedWebView.swift */, - 5177471924B3863000EB0F74 /* WebViewProvider.swift */, - 517B2EBB24B3E62A001AC46C /* WrapperScriptMessageHandler.swift */, - ); - path = Article; - sourceTree = ""; - }; - 51A8001024CA0FAE00F41F1D /* CombineExt */ = { - isa = PBXGroup; - children = ( - 51A8001424CA0FEC00F41F1D /* DemandBuffer.swift */, - 51A8001124CA0FC700F41F1D /* Sink.swift */, - 51A8FFEC24CA0CF400F41F1D /* WIthLatestFrom.swift */, - 51A8002C24CC451500F41F1D /* ShareReplay.swift */, - 51A8005024CC453C00F41F1D /* ReplaySubject.swift */, - ); - path = CombineExt; - sourceTree = ""; - }; 51B5C85A23F22A7A00032075 /* ShareExtension */ = { isa = PBXGroup; children = ( @@ -2895,72 +2031,6 @@ path = ShareExtension; sourceTree = ""; }; - 51C0519224A77E3500194D5E /* Multiplatform */ = { - isa = PBXGroup; - children = ( - 51C0519524A77E8B00194D5E /* Shared */, - 51C0519424A77E6D00194D5E /* macOS */, - 51C0519324A77E6600194D5E /* iOS */, - ); - path = Multiplatform; - sourceTree = ""; - }; - 51C0519324A77E6600194D5E /* iOS */ = { - isa = PBXGroup; - children = ( - 51C051CF24A7A72100194D5E /* iOS-dev.entitlements */, - 51C051CE24A7A72100194D5E /* iOS.entitlements */, - 51C0513F24A77DF800194D5E /* Info.plist */, - 51E4993B24A8709900B667CB /* AppDelegate.swift */, - 172199EC24AB2E0100A31D04 /* SafariView.swift */, - 5177476424B3BDAE00EB0F74 /* AttributedStringView.swift */, - 5177470B24B2FF2C00EB0F74 /* Article */, - 172199EB24AB228E00A31D04 /* Settings */, - ); - path = iOS; - sourceTree = ""; - }; - 51C0519424A77E6D00194D5E /* macOS */ = { - isa = PBXGroup; - children = ( - 51C051CD24A7A6DB00194D5E /* macOS-dev.entitlements */, - 51C0514724A77DF800194D5E /* macOS.entitlements */, - 51C0514624A77DF800194D5E /* Info.plist */, - 51E4993924A8708800B667CB /* AppDelegate.swift */, - 51B80F4524BF76E700C6C32D /* Browser.swift */, - 1729529A24AA1FD200D65E66 /* MacSearchField.swift */, - 5177470C24B2FF3B00EB0F74 /* Article */, - 1729528F24AA1A4F00D65E66 /* Preferences */, - ); - path = macOS; - sourceTree = ""; - }; - 51C0519524A77E8B00194D5E /* Shared */ = { - isa = PBXGroup; - children = ( - 51E4992524A80AAB00B667CB /* AppAssets.swift */, - 1776E88D24AC5F8A00E78166 /* AppDefaults.swift */, - 51E4995824A873F900B667CB /* ErrorHandler.swift */, - 51C0513624A77DF700194D5E /* MainApp.swift */, - FF64D0C424AF53EE0084080A /* RefreshProgressModel.swift */, - 51E499D724A912C200B667CB /* SceneModel.swift */, - 51E49A0224A91FF600B667CB /* SceneNavigationView.swift */, - 1704053324E5985A00A00787 /* SceneNavigationModel.swift */, - 51C0513824A77DF800194D5E /* Assets.xcassets */, - 171BCBB124CBD569006E22D9 /* Account Management */, - 17930ED224AF10CD00A9BA52 /* Add */, - 51A576B924AE617B00078888 /* Article */, - 51A8001024CA0FAE00F41F1D /* CombineExt */, - 51919FB124AAB95300541E64 /* Images */, - 17897AA724C281520014BA03 /* Inspector */, - 514E6BFD24AD252400AC6F6E /* Previews */, - 51E499FB24A9135A00B667CB /* Sidebar */, - 514E6C0424AD2B0400AC6F6E /* SwiftUI Extensions */, - 51919FCB24AB855000541E64 /* Timeline */, - ); - path = Shared; - sourceTree = ""; - }; 51C45245226506C800C03939 /* UIKit Extensions */ = { isa = PBXGroup; children = ( @@ -2999,11 +2069,9 @@ 51C45264226508F600C03939 /* MasterFeedViewController.swift */, 51627A6623861DA3007B3B4B /* MasterFeedViewController+Drag.swift */, 51627A6823861DED007B3B4B /* MasterFeedViewController+Drop.swift */, - 51627A6A238629D8007B3B4B /* MasterFeedDataSource.swift */, - 51DC37082402F1470095D371 /* MasterFeedDataSourceOperation.swift */, 51CE1C0A23622006005548FC /* RefreshProgressView.swift */, 51CE1C0823621EDA005548FC /* RefreshProgressView.xib */, - 51DC37062402153E0095D371 /* UpdateSelectionOperation.swift */, + 5195C1D92720205F00888867 /* ShadowTableChanges.swift */, 51C45260226508F600C03939 /* Cell */, ); path = MasterFeed; @@ -3012,9 +2080,9 @@ 51C45260226508F600C03939 /* Cell */ = { isa = PBXGroup; children = ( + 5195C1DB2720BD3000888867 /* MasterFeedRowIdentifier.swift */, 51C45262226508F600C03939 /* MasterFeedTableViewCell.swift */, 51C45263226508F600C03939 /* MasterFeedTableViewCellLayout.swift */, - 513CCF08248808BA00C55709 /* MasterFeedTableViewIdentifier.swift */, 512E08F722688F7C00BDCFDD /* MasterFeedTableViewSectionHeader.swift */, 516AE9B22371C372007DEEAA /* MasterFeedTableViewSectionHeaderLayout.swift */, 51C45261226508F600C03939 /* MasterFeedUnreadCountView.swift */, @@ -3092,7 +2160,8 @@ 51C452A822650DA100C03939 /* Article Rendering */ = { isa = PBXGroup; children = ( - B27EEBDF244D15F2000932E6 /* shared.css */, + 51D0214526ED617100FF2E0F /* core.css */, + B27EEBDF244D15F2000932E6 /* stylesheet.css */, 848362FE2262A30E00DA1D35 /* template.html */, 517630032336215100E15FFF /* main.js */, 49F40DEF2335B71000552BF4 /* newsfoot.js */, @@ -3128,24 +2197,6 @@ name = Frameworks; sourceTree = ""; }; - 51E499FB24A9135A00B667CB /* Sidebar */ = { - isa = PBXGroup; - children = ( - 51E499FF24A91FC100B667CB /* SidebarContainerView.swift */, - 51B8BCC124C25C3E00360B00 /* SidebarContextMenu.swift */, - 51392D1A24AC19A000BE0D35 /* SidebarExpandedContainers.swift */, - 51408B7D24A9EC6F0073CF4E /* SidebarItem.swift */, - 51919FAE24AA8EFA00541E64 /* SidebarItemView.swift */, - 5177470824B2F87600EB0F74 /* SidebarListStyleModifier.swift */, - 51E499FC24A9137600B667CB /* SidebarModel.swift */, - 17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */, - 172199F024AB716900A31D04 /* SidebarToolbarModifier.swift */, - 51919FA524AA64B000541E64 /* SidebarView.swift */, - 51919FAB24AA8CCA00541E64 /* UnreadCountView.swift */, - ); - path = Sidebar; - sourceTree = ""; - }; 51FA739A2332BDE70090D516 /* Article Extractor */ = { isa = PBXGroup; children = ( @@ -3177,24 +2228,6 @@ path = SafariExtension; sourceTree = ""; }; - 65ACE46124B47770003AE06A /* Accounts */ = { - isa = PBXGroup; - children = ( - 65ACE48324B4779B003AE06A /* SettingsAddAccountView.swift */, - 65422D1624B75CD1008A2FA2 /* SettingsAddAccountModel.swift */, - 65ACE48524B477C9003AE06A /* SettingsAccountLabelView.swift */, - 65ACE48724B48020003AE06A /* SettingsLocalAccountView.swift */, - 6591723024B5C35400B638E8 /* AccountHeaderImageView.swift */, - 6591727E24B5D19500B638E8 /* SettingsDetailAccountView.swift */, - 6586A5F624B632F8002BCF4F /* SettingsDetailAccountModel.swift */, - 653A4E7824BCA5BB00EF2D7F /* SettingsCloudKitAccountView.swift */, - 65082A2E24C72AC8009FA994 /* SettingsCredentialsAccountView.swift */, - 65082A5124C72B88009FA994 /* SettingsCredentialsAccountModel.swift */, - 65082A5324C73D2F009FA994 /* AccountCredentialsError.swift */, - ); - path = Accounts; - sourceTree = ""; - }; 840D61942029031D009BC708 /* NetNewsWire-iOSTests */ = { isa = PBXGroup; children = ( @@ -3428,7 +2461,6 @@ 5103A9972421643300410853 /* blank.html */, B528F81D23333C7E00E735DD /* page.html */, 5142194A2353C1CF00E07E2C /* main_mac.js */, - 848362FC2262A30800DA1D35 /* styleSheet.css */, 5127B235222B4849006D641D /* Keyboard */, ); path = Detail; @@ -3437,8 +2469,11 @@ 849A97861ED9ECEF007D329B /* Article Styles */ = { isa = PBXGroup; children = ( - 849A97871ED9ECEF007D329B /* ArticleStyle.swift */, - 849A97881ED9ECEF007D329B /* ArticleStylesManager.swift */, + 849A97871ED9ECEF007D329B /* ArticleTheme.swift */, + 849A97881ED9ECEF007D329B /* ArticleThemesManager.swift */, + 17D643B026F8A436008D4C05 /* ArticleThemeDownloader.swift */, + 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */, + 17071EEF26F8137400F5E71D /* ArticleTheme+Notifications.swift */, ); name = "Article Styles"; path = Shared/ArticleStyles; @@ -3452,7 +2487,6 @@ 84CBDDAE1FD3674C005A61AA /* Technotes */, 84C9FC6522629B3900D921D6 /* Mac */, 84C9FC922262A0E600D921D6 /* iOS */, - 51C0519224A77E3500194D5E /* Multiplatform */, 176813F82564BB2C00D98635 /* Widget */, 84C9FC6822629C9A00D921D6 /* Shared */, 84C9FCA52262A1E600D921D6 /* Tests */, @@ -3480,8 +2514,6 @@ 51314637235A7BBE00387FDC /* NetNewsWire iOS Intents Extension.appex */, 65ED4083235DEF6C0081F399 /* NetNewsWire.app */, 65ED409D235DEF770081F399 /* Subscribe to Feed MAS.appex */, - 51C0513D24A77DF800194D5E /* NetNewsWire.app */, - 51C0514424A77DF800194D5E /* NetNewsWire.app */, 510C415C24E5CDE3008226FD /* NetNewsWire Share Extension.appex */, 176813F32564BB2C00D98635 /* NetNewsWire iOS Widget Extension.appex */, 653813592680E2DA007A082C /* NetNewsWire Share Extension MAS.appex */, @@ -3694,7 +2726,6 @@ 5103A9B324216A4200410853 /* blank.html */, 51BB7C302335ACDE008E8144 /* page.html */, 514219572353C28900E07E2C /* main_ios.js */, - 51C452B72265178500C03939 /* styleSheet.css */, 17D7586C2679C21700B17787 /* NetNewsWire-iOS-Bridging-Header.h */, 84C9FC9B2262A1A900D921D6 /* Assets.xcassets */, 84C9FC9C2262A1A900D921D6 /* Info.plist */, @@ -3803,8 +2834,6 @@ 518B2EE92351B4C200400001 /* NetNewsWire_iOSTests_target.xcconfig */, 65ED40F2235DF5E00081F399 /* NetNewsWire_macapp_target_macappstore.xcconfig */, D5907CE02002F0FA005947E5 /* NetNewsWire_macapp_target.xcconfig */, - 51C0519724A7808F00194D5E /* NetNewsWire_multiplatform_iOSapp_target.xcconfig */, - 51C0519824A7808F00194D5E /* NetNewsWire_multiplatform_macOSapp_target.xcconfig */, D5907CDD2002F0BE005947E5 /* NetNewsWire_project_debug.xcconfig */, D5907CDC2002F0BE005947E5 /* NetNewsWire_project_release.xcconfig */, D5907CDE2002F0BE005947E5 /* NetNewsWire_project.xcconfig */, @@ -3949,70 +2978,6 @@ productReference = 518B2ED22351B3DD00400001 /* NetNewsWire-iOSTests.xctest */; productType = "com.apple.product-type.bundle.unit-test"; }; - 51C0513C24A77DF800194D5E /* Multiplatform iOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 51C0518D24A77DF800194D5E /* Build configuration list for PBXNativeTarget "Multiplatform iOS" */; - buildPhases = ( - 51C0513924A77DF800194D5E /* Sources */, - 51C0513A24A77DF800194D5E /* Frameworks */, - 51C0513B24A77DF800194D5E /* Resources */, - 51E4989524A8061400B667CB /* Embed Frameworks */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "Multiplatform iOS"; - packageProductDependencies = ( - 516B695C24D2F28E00B5702F /* Account */, - 17A1597B24E3DEDD005DA32A /* RSCore */, - 17A1597E24E3DEDD005DA32A /* RSTree */, - 17A1598124E3DEDD005DA32A /* RSWeb */, - 17A1598424E3DEDD005DA32A /* RSDatabase */, - 17A1598724E3DEDD005DA32A /* RSParser */, - 17E0080E25936DF6000C23F0 /* Articles */, - 17E0081125936DF6000C23F0 /* ArticlesDatabase */, - 17E0081425936DFF000C23F0 /* Secrets */, - 17E0081725936DFF000C23F0 /* SyncDatabase */, - ); - productName = iOS; - productReference = 51C0513D24A77DF800194D5E /* NetNewsWire.app */; - productType = "com.apple.product-type.application"; - }; - 51C0514324A77DF800194D5E /* Multiplatform macOS */ = { - isa = PBXNativeTarget; - buildConfigurationList = 51C0518E24A77DF800194D5E /* Build configuration list for PBXNativeTarget "Multiplatform macOS" */; - buildPhases = ( - 51C0514024A77DF800194D5E /* Sources */, - 51C0514124A77DF800194D5E /* Frameworks */, - 51C0514224A77DF800194D5E /* Resources */, - 51E498B024A8069300B667CB /* Embed Frameworks */, - 51E4994924A872AD00B667CB /* Embed XPC Services */, - ); - buildRules = ( - ); - dependencies = ( - ); - name = "Multiplatform macOS"; - packageProductDependencies = ( - 516B695A24D2F28600B5702F /* Account */, - 17386B6B2577BD820014C8B2 /* RSSparkle */, - 17386B942577C6240014C8B2 /* RSCore */, - 17386B972577C6240014C8B2 /* RSTree */, - 17386B9A2577C6240014C8B2 /* RSWeb */, - 17386B9D2577C6240014C8B2 /* RSDatabase */, - 17386BA32577C6240014C8B2 /* RSParser */, - 17386BB52577C7340014C8B2 /* RSCoreResources */, - 51E0614425A5A28E00194066 /* Articles */, - 51E0614725A5A28E00194066 /* ArticlesDatabase */, - 51E0614A25A5A28E00194066 /* Secrets */, - 51E0614D25A5A28E00194066 /* SyncDatabase */, - 51E0615025A5A29600194066 /* CrashReporter */, - ); - productName = macOS; - productReference = 51C0514424A77DF800194D5E /* NetNewsWire.app */; - productType = "com.apple.product-type.application"; - }; 6538133E2680E2DA007A082C /* NetNewsWire Share Extension MAS */ = { isa = PBXNativeTarget; buildConfigurationList = 653813562680E2DA007A082C /* Build configuration list for PBXNativeTarget "NetNewsWire Share Extension MAS" */; @@ -4137,6 +3102,7 @@ 513F32732593EE6F0003048F /* ArticlesDatabase */, 513F32762593EE6F0003048F /* Secrets */, 513F32792593EE6F0003048F /* SyncDatabase */, + 179D280A26F6F93D003B2E0A /* Zip */, ); productName = "NetNewsWire-iOS"; productReference = 840D617C2029031C009BC708 /* NetNewsWire.app */; @@ -4178,6 +3144,7 @@ 5132775D2590FC640064F1E7 /* Articles */, 513277602590FC640064F1E7 /* ArticlesDatabase */, 513277632590FC640064F1E7 /* SyncDatabase */, + 179C39E926F76B0500D4E741 /* Zip */, ); productName = NetNewsWire; productReference = 849C64601ED37A5D003D8FC0 /* NetNewsWire.app */; @@ -4240,16 +3207,6 @@ ProvisioningStyle = Automatic; TestTargetID = 840D617B2029031C009BC708; }; - 51C0513C24A77DF800194D5E = { - CreatedOnToolsVersion = 12.0; - DevelopmentTeam = SHJK2V3AJG; - ProvisioningStyle = Automatic; - }; - 51C0514324A77DF800194D5E = { - CreatedOnToolsVersion = 12.0; - DevelopmentTeam = SHJK2V3AJG; - ProvisioningStyle = Automatic; - }; 6581C73220CED60000F4AD34 = { DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Automatic; @@ -4309,6 +3266,7 @@ 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */, 17192AD82567B3D500AAEACA /* XCRemoteSwiftPackageReference "Sparkle-Binary" */, 519CA8E325841DB700EB079A /* XCRemoteSwiftPackageReference "plcrashreporter" */, + 179D280926F6F93D003B2E0A /* XCRemoteSwiftPackageReference "Zip" */, ); productRefGroup = 849C64611ED37A5D003D8FC0 /* Products */; projectDirPath = ""; @@ -4324,8 +3282,6 @@ 51314636235A7BBE00387FDC /* NetNewsWire iOS Intents Extension */, 176813F22564BB2C00D98635 /* NetNewsWire iOS Widget Extension */, 518B2ED12351B3DD00400001 /* NetNewsWire-iOSTests */, - 51C0513C24A77DF800194D5E /* Multiplatform iOS */, - 51C0514324A77DF800194D5E /* Multiplatform macOS */, 510C415B24E5CDE3008226FD /* NetNewsWire Share Extension */, 6538133E2680E2DA007A082C /* NetNewsWire Share Extension MAS */, ); @@ -4380,44 +3336,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 51C0513B24A77DF800194D5E /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 5177475E24B39AD500EB0F74 /* Thanks.rtf in Resources */, - 517B2EE224B3E8FE001AC46C /* page.html in Resources */, - 51E4995F24A875F300B667CB /* shared.css in Resources */, - 517B2EE824B3E8FE001AC46C /* main_multiplatform.js in Resources */, - 51E4997324A8784300B667CB /* DefaultFeeds.opml in Resources */, - 51C0516224A77DF800194D5E /* Assets.xcassets in Resources */, - 5177475F24B39AD500EB0F74 /* About.rtf in Resources */, - 51E4996024A875F300B667CB /* template.html in Resources */, - 5177475D24B39AD500EB0F74 /* Dedication.rtf in Resources */, - 51E4995E24A875F300B667CB /* newsfoot.js in Resources */, - 517B2EE424B3E8FE001AC46C /* blank.html in Resources */, - 5177475C24B39AD500EB0F74 /* Credits.rtf in Resources */, - 517B2EE624B3E8FE001AC46C /* styleSheet.css in Resources */, - 51E4995D24A875F300B667CB /* main.js in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51C0514224A77DF800194D5E /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 517B2EE324B3E8FE001AC46C /* page.html in Resources */, - 51E4996424A875F400B667CB /* shared.css in Resources */, - 51E4997524A8784400B667CB /* DefaultFeeds.opml in Resources */, - 51C0516324A77DF800194D5E /* Assets.xcassets in Resources */, - 51E4996524A875F400B667CB /* template.html in Resources */, - 517B2EE924B3E8FE001AC46C /* main_multiplatform.js in Resources */, - 517B2EE524B3E8FE001AC46C /* blank.html in Resources */, - 51E4996324A875F400B667CB /* newsfoot.js in Resources */, - 51E4996224A875F400B667CB /* main.js in Resources */, - 517B2EE724B3E8FE001AC46C /* styleSheet.css in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 6538134F2680E2DA007A082C /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -4448,6 +3366,7 @@ 65ED4051235DEF6C0081F399 /* TimelineKeyboardShortcuts.plist in Resources */, 65ED4052235DEF6C0081F399 /* template.html in Resources */, 65ED4054235DEF6C0081F399 /* Main.storyboard in Resources */, + 5137C2E526F3F52D009EFEDB /* Sepia.nnwtheme in Resources */, 65ED4056235DEF6C0081F399 /* NetNewsWire.sdef in Resources */, 65ED4057235DEF6C0081F399 /* AccountsDetail.xib in Resources */, 65ED4058235DEF6C0081F399 /* main.js in Resources */, @@ -4456,15 +3375,16 @@ 65ED405A235DEF6C0081F399 /* main_mac.js in Resources */, 65ED405B235DEF6C0081F399 /* KeyboardShortcuts.html in Resources */, 65ED405C235DEF6C0081F399 /* ImportOPMLSheet.xib in Resources */, + 51DEE81926FBFF84006DAA56 /* Promenade.nnwtheme in Resources */, 65ED405D235DEF6C0081F399 /* SidebarKeyboardShortcuts.plist in Resources */, 514A89A3244FD63F0085E65D /* AddTwitterFeedSheet.xib in Resources */, + 51D0214726ED617100FF2E0F /* core.css in Resources */, 5103A9F5242258C600410853 /* AccountsAddCloudKit.xib in Resources */, 65ED405E235DEF6C0081F399 /* DefaultFeeds.opml in Resources */, 51333D3C2468615D00EB5C91 /* AddRedditFeedSheet.xib in Resources */, 51C03082257D815A00609262 /* UnifiedWindow.storyboard in Resources */, 65ED405F235DEF6C0081F399 /* Preferences.storyboard in Resources */, 65ED4061235DEF6C0081F399 /* Assets.xcassets in Resources */, - 65ED4062235DEF6C0081F399 /* styleSheet.css in Resources */, 65ED4063235DEF6C0081F399 /* RenameSheet.xib in Resources */, 65ED4064235DEF6C0081F399 /* AddFolderSheet.xib in Resources */, 65ED4065235DEF6C0081F399 /* AccountsFeedbin.xib in Resources */, @@ -4481,7 +3401,8 @@ 65ED406C235DEF6C0081F399 /* Credits.rtf in Resources */, 65ED406D235DEF6C0081F399 /* Inspector.storyboard in Resources */, 65ED406E235DEF6C0081F399 /* AddWebFeedSheet.xib in Resources */, - B27EEBFA244D15F3000932E6 /* shared.css in Resources */, + 51DEE81326FB9233006DAA56 /* Appanoose.nnwtheme in Resources */, + B27EEBFA244D15F3000932E6 /* stylesheet.css in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4499,6 +3420,7 @@ isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( + 5137C2E626F3F52D009EFEDB /* Sepia.nnwtheme in Resources */, 517630052336215100E15FFF /* main.js in Resources */, 5148F44B2336DB4700F8CD8B /* MasterTimelineTitleView.xib in Resources */, 511D43D0231FA62500FB1562 /* TimelineKeyboardShortcuts.plist in Resources */, @@ -4511,8 +3433,10 @@ 516A093723609A3600EAE89B /* SettingsComboTableViewCell.xib in Resources */, 51F85BF32272531500C787DC /* Dedication.rtf in Resources */, 516A09422361248000EAE89B /* Inspector.storyboard in Resources */, + 51DEE81A26FBFF84006DAA56 /* Promenade.nnwtheme in Resources */, 1768140B2564BB8300D98635 /* NetNewsWire_iOSwidgetextension_target.xcconfig in Resources */, 5103A9B424216A4200410853 /* blank.html in Resources */, + 51D0214826ED617100FF2E0F /* core.css in Resources */, 84C9FCA42262A1B800D921D6 /* LaunchScreenPhone.storyboard in Resources */, 51F85BEB22724CB600C787DC /* About.rtf in Resources */, 516A093B2360A4A000EAE89B /* SettingsTableViewCell.xib in Resources */, @@ -4521,7 +3445,7 @@ 51C452AB22650DC600C03939 /* template.html in Resources */, 51F85BF12272524100C787DC /* Credits.rtf in Resources */, 84A3EE61223B667F00557320 /* DefaultFeeds.opml in Resources */, - B27EEBFB244D15F3000932E6 /* shared.css in Resources */, + B27EEBFB244D15F3000932E6 /* stylesheet.css in Resources */, 511D43CF231FA62200FB1562 /* DetailKeyboardShortcuts.plist in Resources */, 51A1699A235E10D700EB091F /* Settings.storyboard in Resources */, 49F40DF92335B71000552BF4 /* newsfoot.js in Resources */, @@ -4530,8 +3454,8 @@ 51CE1C0923621EDA005548FC /* RefreshProgressView.xib in Resources */, 84C9FC9D2262A1A900D921D6 /* Assets.xcassets in Resources */, 514219582353C28900E07E2C /* main_ios.js in Resources */, + 51DEE81426FB9233006DAA56 /* Appanoose.nnwtheme in Resources */, 51E36E8C239D6765006F47A5 /* AddFeedSelectFolderTableViewCell.xib in Resources */, - 51C452B82265178500C03939 /* styleSheet.css in Resources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -4547,12 +3471,15 @@ 848363082262A3DD00DA1D35 /* Main.storyboard in Resources */, 84C9FC8F22629E8F00D921D6 /* NetNewsWire.sdef in Resources */, 84C9FC7D22629E1200D921D6 /* AccountsDetail.xib in Resources */, + 5137C2E426F3F52D009EFEDB /* Sepia.nnwtheme in Resources */, 517630042336215100E15FFF /* main.js in Resources */, 65ED40A0235DEFF00081F399 /* container-migration.plist in Resources */, 5144EA362279FC3D00D19003 /* AccountsAddLocal.xib in Resources */, + 51D0214626ED617100FF2E0F /* core.css in Resources */, + 51DEE81826FBFF84006DAA56 /* Promenade.nnwtheme in Resources */, 5142194B2353C1CF00E07E2C /* main_mac.js in Resources */, 84C9FC8C22629E8F00D921D6 /* KeyboardShortcuts.html in Resources */, - B27EEBF9244D15F3000932E6 /* shared.css in Resources */, + B27EEBF9244D15F3000932E6 /* stylesheet.css in Resources */, 5144EA3B227A379E00D19003 /* ImportOPMLSheet.xib in Resources */, 844B5B691FEA20DF00C7C76A /* SidebarKeyboardShortcuts.plist in Resources */, 5103A9F4242258C600410853 /* AccountsAddCloudKit.xib in Resources */, @@ -4561,7 +3488,6 @@ 849C78902362AAFC009A71E4 /* ExportOPMLSheet.xib in Resources */, 84C9FC8222629E4800D921D6 /* Preferences.storyboard in Resources */, 849C64681ED37A5D003D8FC0 /* Assets.xcassets in Resources */, - 848362FD2262A30800DA1D35 /* styleSheet.css in Resources */, 8483630B2262A3F000DA1D35 /* RenameSheet.xib in Resources */, 848363052262A3CC00DA1D35 /* AddFolderSheet.xib in Resources */, 5144EA52227B8E4500D19003 /* AccountsFeedbin.xib in Resources */, @@ -4577,6 +3503,7 @@ 5103A9982421643300410853 /* blank.html in Resources */, 515A516E243E7F950089E588 /* ExtensionPointDetail.xib in Resources */, 84BAE64921CEDAF20046DB56 /* CrashReporterWindow.xib in Resources */, + 51DEE81226FB9233006DAA56 /* Appanoose.nnwtheme in Resources */, 84C9FC8E22629E8F00D921D6 /* Credits.rtf in Resources */, 84BBB12D20142A4700F054F5 /* Inspector.storyboard in Resources */, 848363022262A3BD00DA1D35 /* AddWebFeedSheet.xib in Resources */, @@ -4609,7 +3536,7 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "if ! command -v swiftgen &> /dev/null\nthen\n echo \"swiftgen could not be found\"\n exit\nfi\n\narch -x86_64 swiftgen run strings -t structured-swift5 \"$PROJECT_DIR/Widget/Resources/en.lproj/Localizable.strings\" \"$PROJECT_DIR/Widget/Resources/Localizable.stringsdict\" --output \"$PROJECT_DIR/Widget/Resources/Localized.swift\";\n"; + shellScript = "if ! command -v swiftgen &> /dev/null\nthen\n echo \"swiftgen could not be found\"\n exit\nfi\n\nswiftgen run strings -t structured-swift5 \"$PROJECT_DIR/Widget/Resources/en.lproj/Localizable.strings\" \"$PROJECT_DIR/Widget/Resources/Localizable.stringsdict\" --output \"$PROJECT_DIR/Widget/Resources/Localized.swift\";\n"; }; 513F328A2593EFCE0003048F /* Delete Unnecessary Frameworks */ = { isa = PBXShellScriptBuildPhase; @@ -4898,339 +3825,6 @@ ); runOnlyForDeploymentPostprocessing = 0; }; - 51C0513924A77DF800194D5E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 172412A6257BC01C00ACCEBC /* AddFeedbinViewModel.swift in Sources */, - 17D5F17124B0BC6700375168 /* SidebarToolbarModel.swift in Sources */, - 51E4995924A873F900B667CB /* ErrorHandler.swift in Sources */, - 51392D1B24AC19A000BE0D35 /* SidebarExpandedContainers.swift in Sources */, - 51E4992F24A8676400B667CB /* ArticleArray.swift in Sources */, - 5177471C24B387AC00EB0F74 /* ImageScrollView.swift in Sources */, - 51E498F824A8085D00B667CB /* UnreadFeed.swift in Sources */, - 6591723124B5C35400B638E8 /* AccountHeaderImageView.swift in Sources */, - 51B8104524C0E6D200C6C32D /* TimelineTextSizer.swift in Sources */, - FF64D0E724AF53EE0084080A /* RefreshProgressModel.swift in Sources */, - 51E4996A24A8762D00B667CB /* ExtractedArticle.swift in Sources */, - 51919FF124AB864A00541E64 /* TimelineModel.swift in Sources */, - 5194736E24BBB937001A2939 /* HiddenModifier.swift in Sources */, - 51DC079B2552083500A3F79F /* ArticleTextSize.swift in Sources */, - 51E498F124A8085D00B667CB /* StarredFeedDelegate.swift in Sources */, - 51E498FF24A808BB00B667CB /* SingleFaviconDownloader.swift in Sources */, - 51E4997224A8784300B667CB /* DefaultFeedsImporter.swift in Sources */, - 1704053424E5985A00A00787 /* SceneNavigationModel.swift in Sources */, - 514E6C0924AD39AD00AC6F6E /* ArticleIconImageLoader.swift in Sources */, - 172412A2257BC01C00ACCEBC /* AddLocalAccountView.swift in Sources */, - 6594CA3B24AF6F2A005C7D7C /* OPMLExporter.swift in Sources */, - FA80C13E24B072AA00974098 /* AddFolderModel.swift in Sources */, - 5177470624B2910300EB0F74 /* ArticleToolbarModifier.swift in Sources */, - 51919FAF24AA8EFA00541E64 /* SidebarItemView.swift in Sources */, - 514E6BDA24ACEA0400AC6F6E /* TimelineItemView.swift in Sources */, - 172412A1257BC01C00ACCEBC /* AddReaderAPIAccountView.swift in Sources */, - 5177471624B37D9700EB0F74 /* ArticleIconSchemeHandler.swift in Sources */, - FA80C11724B0728000974098 /* AddFolderView.swift in Sources */, - 51E4990D24A808C500B667CB /* RSHTMLMetadata+Extension.swift in Sources */, - 51919FF424AB869C00541E64 /* TimelineItem.swift in Sources */, - 514E6C0224AD29A300AC6F6E /* TimelineItemStatusView.swift in Sources */, - 51C65AFC24CCB2C9008EB3BD /* TimelineItems.swift in Sources */, - 51E49A0024A91FC100B667CB /* SidebarContainerView.swift in Sources */, - 5177471824B3812200EB0F74 /* IconView.swift in Sources */, - 51E4995C24A875F300B667CB /* ArticleRenderer.swift in Sources */, - 51E4992324A8095700B667CB /* URL-Extensions.swift in Sources */, - 51E4993624A867E800B667CB /* UserInfoKey.swift in Sources */, - 171BCB8C24CB08A3006E22D9 /* FixAccountCredentialView.swift in Sources */, - 51E4990924A808C500B667CB /* WebFeedIconDownloader.swift in Sources */, - 51E498F524A8085D00B667CB /* TodayFeedDelegate.swift in Sources */, - 171BCBAF24CBBFD8006E22D9 /* EditAccountCredentialsModel.swift in Sources */, - 51B80EDB24BD225200C6C32D /* OpenInSafariActivity.swift in Sources */, - 172199F124AB716900A31D04 /* SidebarToolbarModifier.swift in Sources */, - 65CBAD5A24AE03C20006DD91 /* ColorPaletteContainerView.swift in Sources */, - 5177470924B2F87600EB0F74 /* SidebarListStyleModifier.swift in Sources */, - 65082A5424C73D2F009FA994 /* AccountCredentialsError.swift in Sources */, - 51E4990B24A808C500B667CB /* ImageDownloader.swift in Sources */, - 51E498F424A8085D00B667CB /* SmartFeedDelegate.swift in Sources */, - 51A8001524CA0FEC00F41F1D /* DemandBuffer.swift in Sources */, - 514E6BFF24AD255D00AC6F6E /* PreviewArticles.swift in Sources */, - 51E4993024A8676400B667CB /* ArticleSorter.swift in Sources */, - 51408B7E24A9EC6F0073CF4E /* SidebarItem.swift in Sources */, - 5177476724B3BE3400EB0F74 /* SettingsAboutModel.swift in Sources */, - 65C2E40124B05D8A000AFDF6 /* FeedsSettingsModel.swift in Sources */, - 51E4990A24A808C500B667CB /* FeaturedImageDownloader.swift in Sources */, - 51E4993224A8676400B667CB /* FetchRequestQueue.swift in Sources */, - 51E4991724A8090400B667CB /* ArticleUtilities.swift in Sources */, - 51E4991B24A8091000B667CB /* IconImage.swift in Sources */, - 51E4995424A8734D00B667CB /* ExtensionPointIdentifer.swift in Sources */, - 51E4996924A8760C00B667CB /* ArticleStylesManager.swift in Sources */, - 5177471E24B387E100EB0F74 /* ImageTransition.swift in Sources */, - 51E498F324A8085D00B667CB /* PseudoFeed.swift in Sources */, - 172412A5257BC01C00ACCEBC /* AddCloudKitAccountView.swift in Sources */, - 65ACE48424B4779B003AE06A /* SettingsAddAccountView.swift in Sources */, - 51A5769624AE617200078888 /* ArticleContainerView.swift in Sources */, - 5181C5AD24AF89B1002E0F70 /* PreferredColorSchemeModifier.swift in Sources */, - 51E4996B24A8762D00B667CB /* ArticleExtractor.swift in Sources */, - 51E49A0324A91FF600B667CB /* SceneNavigationView.swift in Sources */, - 51E4990124A808BB00B667CB /* FaviconURLFinder.swift in Sources */, - 51E4991D24A8092100B667CB /* NSAttributedString+NetNewsWire.swift in Sources */, - 65082A2F24C72AC8009FA994 /* SettingsCredentialsAccountView.swift in Sources */, - 51A8FFED24CA0CF400F41F1D /* WIthLatestFrom.swift in Sources */, - 1724129E257BC01C00ACCEBC /* AddReaderAPIViewModel.swift in Sources */, - 51E499FD24A9137600B667CB /* SidebarModel.swift in Sources */, - 5181C66224B0C326002E0F70 /* SettingsModel.swift in Sources */, - 51E4995324A8734D00B667CB /* RedditFeedProvider-Extensions.swift in Sources */, - 5177471024B3029400EB0F74 /* ArticleViewController.swift in Sources */, - 17D3CEE3257C4D2300E74939 /* AddAccountSignUp.swift in Sources */, - 65082A5224C72B88009FA994 /* SettingsCredentialsAccountModel.swift in Sources */, - 172199C924AB228900A31D04 /* SettingsView.swift in Sources */, - 51B8BCC224C25C3E00360B00 /* SidebarContextMenu.swift in Sources */, - 51A8005124CC453C00F41F1D /* ReplaySubject.swift in Sources */, - 172412A7257BC01C00ACCEBC /* AddFeedbinAccountView.swift in Sources */, - 17D232A824AFF10A0005F075 /* AddWebFeedModel.swift in Sources */, - 51B80EB824BD1F8B00C6C32D /* ActivityViewController.swift in Sources */, - 51E4994224A8713C00B667CB /* ArticleStatusSyncTimer.swift in Sources */, - 51E498F624A8085D00B667CB /* SearchFeedDelegate.swift in Sources */, - 51C4CFF324D37D1F00AF9874 /* Secrets.swift in Sources */, - 51B80EE124BD3E9600C6C32D /* FindInArticleActivity.swift in Sources */, - 6586A5F724B632F8002BCF4F /* SettingsDetailAccountModel.swift in Sources */, - 51E498F224A8085D00B667CB /* SmartFeedsController.swift in Sources */, - 51919FB624AABCA100541E64 /* IconImageView.swift in Sources */, - 51919FA624AA64B000541E64 /* SidebarView.swift in Sources */, - 51E4997024A8764C00B667CB /* ActivityManager.swift in Sources */, - 5177471224B37C5400EB0F74 /* WebViewController.swift in Sources */, - 51E4990F24A808CC00B667CB /* HTMLMetadataDownloader.swift in Sources */, - 5177476524B3BDAE00EB0F74 /* AttributedStringView.swift in Sources */, - 51E4993124A8676400B667CB /* FetchRequestOperation.swift in Sources */, - 51E4992624A80AAB00B667CB /* AppAssets.swift in Sources */, - 514E6C0624AD2B5F00AC6F6E /* Image-Extensions.swift in Sources */, - 172412AF257BC0C300ACCEBC /* AccountType+Helpers.swift in Sources */, - 51E4995624A8734D00B667CB /* TwitterFeedProvider-Extensions.swift in Sources */, - 5125E6CA24AE461D002A7562 /* TimelineLayoutView.swift in Sources */, - 51E4996824A8760C00B667CB /* ArticleStyle.swift in Sources */, - 51E4990024A808BB00B667CB /* FaviconGenerator.swift in Sources */, - 51E4997124A8764C00B667CB /* ActivityType.swift in Sources */, - 51E4991E24A8094300B667CB /* RSImage-AppIcons.swift in Sources */, - 51E499D824A912C200B667CB /* SceneModel.swift in Sources */, - 5177470E24B2FF6F00EB0F74 /* ArticleView.swift in Sources */, - 5171B4F624B7BABA00FB8D3B /* MarkStatusCommand.swift in Sources */, - 65422D1724B75CD1008A2FA2 /* SettingsAddAccountModel.swift in Sources */, - 172412A3257BC01C00ACCEBC /* AddNewsBlurAccountView.swift in Sources */, - 5177471424B37D4000EB0F74 /* PreloadedWebView.swift in Sources */, - 51B80EDD24BD296700C6C32D /* ArticleActivityItemSource.swift in Sources */, - 1724129D257BC01C00ACCEBC /* AddNewsBlurViewModel.swift in Sources */, - 17897ACA24C281A40014BA03 /* InspectorView.swift in Sources */, - 517B2EBC24B3E62A001AC46C /* WrapperScriptMessageHandler.swift in Sources */, - 51919FB324AAB97900541E64 /* FeedIconImageLoader.swift in Sources */, - 5177472024B3882600EB0F74 /* ImageViewController.swift in Sources */, - 51919FB324AAB97900541E64 /* FeedIconImageLoader.swift in Sources */, - 51E4991324A808FB00B667CB /* AddWebFeedDefaultContainer.swift in Sources */, - 51E4993C24A8709900B667CB /* AppDelegate.swift in Sources */, - 6591727F24B5D19500B638E8 /* SettingsDetailAccountView.swift in Sources */, - 51E498F924A8085D00B667CB /* SmartFeed.swift in Sources */, - 65ACE48824B48020003AE06A /* SettingsLocalAccountView.swift in Sources */, - 171BCBB024CBBFFD006E22D9 /* AccountUpdateErrors.swift in Sources */, - 17930ED424AF10EE00A9BA52 /* AddWebFeedView.swift in Sources */, - 51E4995124A8734D00B667CB /* ExtensionPointManager.swift in Sources */, - 51E4990C24A808C500B667CB /* AuthorAvatarDownloader.swift in Sources */, - 1799E6A924C2F93F00511E91 /* InspectorPlatformModifier.swift in Sources */, - 5177472224B38CAE00EB0F74 /* ArticleExtractorButtonState.swift in Sources */, - 5177471A24B3863000EB0F74 /* WebViewProvider.swift in Sources */, - 51E4992124A8095000B667CB /* RSImage-Extensions.swift in Sources */, - 51A8001224CA0FC700F41F1D /* Sink.swift in Sources */, - 172412A8257BC01C00ACCEBC /* AddFeedWranglerViewModel.swift in Sources */, - 51E4990324A808BB00B667CB /* FaviconDownloader.swift in Sources */, - 172199ED24AB2E0100A31D04 /* SafariView.swift in Sources */, - 65ACE48624B477C9003AE06A /* SettingsAccountLabelView.swift in Sources */, - 51E4990224A808BB00B667CB /* ColorHash.swift in Sources */, - 51919FAC24AA8CCA00541E64 /* UnreadCountView.swift in Sources */, - 5177476224B3BC4700EB0F74 /* SettingsAboutView.swift in Sources */, - 1799E6CD24C320D600511E91 /* InspectorModel.swift in Sources */, - 51E4991924A8090A00B667CB /* CacheCleaner.swift in Sources */, - 51E498F724A8085D00B667CB /* SearchTimelineFeedDelegate.swift in Sources */, - 175942AA24AD533200585066 /* RefreshInterval.swift in Sources */, - 51E4993524A867E800B667CB /* AppNotifications.swift in Sources */, - 6535ECFC2680F9FF00C01CB5 /* IconImageCache.swift in Sources */, - 51C0515E24A77DF800194D5E /* MainApp.swift in Sources */, - 51919FF724AB8B7700541E64 /* TimelineView.swift in Sources */, - 51E4993D24A870F800B667CB /* UserNotificationManager.swift in Sources */, - 172412A0257BC01C00ACCEBC /* AddFeedWranglerAccountView.swift in Sources */, - 5177470324B2657F00EB0F74 /* TimelineToolbarModifier.swift in Sources */, - 51B80EDF24BD298900C6C32D /* TitleActivityItemSource.swift in Sources */, - 51E4991524A808FF00B667CB /* ArticleStringFormatter.swift in Sources */, - 51919FEE24AB85E400541E64 /* TimelineContainerView.swift in Sources */, - 653A4E7924BCA5BB00EF2D7F /* SettingsCloudKitAccountView.swift in Sources */, - 1724129F257BC01C00ACCEBC /* AddFeedlyAccountView.swift in Sources */, - 51E4995724A8734D00B667CB /* ExtensionPoint.swift in Sources */, - 51A8002D24CC451500F41F1D /* ShareReplay.swift in Sources */, - 51B8BCE624C25F7C00360B00 /* TimelineContextMenu.swift in Sources */, - 172412A4257BC01C00ACCEBC /* AddFeedlyViewModel.swift in Sources */, - 1776E88E24AC5F8A00E78166 /* AppDefaults.swift in Sources */, - 51E4991124A808DE00B667CB /* SmallIconProvider.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 51C0514024A77DF800194D5E /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 17930ED524AF10EE00A9BA52 /* AddWebFeedView.swift in Sources */, - 51E4993A24A8708800B667CB /* AppDelegate.swift in Sources */, - 51E498CE24A8085D00B667CB /* UnreadFeed.swift in Sources */, - 6535ECFD2680FA0000C01CB5 /* IconImageCache.swift in Sources */, - 51B8BCC324C25C3E00360B00 /* SidebarContextMenu.swift in Sources */, - 51E498C724A8085D00B667CB /* StarredFeedDelegate.swift in Sources */, - 5194736F24BBB937001A2939 /* HiddenModifier.swift in Sources */, - 51A8001624CA0FEC00F41F1D /* DemandBuffer.swift in Sources */, - 51919FB724AABCA100541E64 /* IconImageView.swift in Sources */, - 51B54A6924B54A490014348B /* IconView.swift in Sources */, - 17897ACB24C281A40014BA03 /* InspectorView.swift in Sources */, - 51E498FA24A808BA00B667CB /* SingleFaviconDownloader.swift in Sources */, - 1727B39924C1368D00A4DBDC /* LayoutPreferencesView.swift in Sources */, - 51A8FFEE24CA0CF400F41F1D /* WIthLatestFrom.swift in Sources */, - 51E4993F24A8713B00B667CB /* ArticleStatusSyncTimer.swift in Sources */, - 51B80F4424BE58BF00C6C32D /* SharingServiceDelegate.swift in Sources */, - 51B54AB624B5B33C0014348B /* WebViewController.swift in Sources */, - 51E4994B24A8734C00B667CB /* SendToMicroBlogCommand.swift in Sources */, - 51E4996F24A8764C00B667CB /* ActivityType.swift in Sources */, - 17386BC42577CC600014C8B2 /* AddFeedbinAccountView.swift in Sources */, - 51E4994E24A8734C00B667CB /* SendToMarsEditCommand.swift in Sources */, - 51919FB024AA8EFA00541E64 /* SidebarItemView.swift in Sources */, - 1769E33624BD9621000E1E8E /* EditAccountCredentialsModel.swift in Sources */, - 51919FEF24AB85E400541E64 /* TimelineContainerView.swift in Sources */, - 51E4996624A8760B00B667CB /* ArticleStyle.swift in Sources */, - 5171B4D424B7BABA00FB8D3B /* MarkStatusCommand.swift in Sources */, - 17E4DBD624BFC53E00FE462A /* AdvancedPreferencesModel.swift in Sources */, - 5177470724B2910300EB0F74 /* ArticleToolbarModifier.swift in Sources */, - FA80C11824B0728000974098 /* AddFolderView.swift in Sources */, - 17386B7A2577C4BF0014C8B2 /* AddLocalAccountView.swift in Sources */, - 51E4996C24A8762D00B667CB /* ExtractedArticle.swift in Sources */, - 51E4990824A808C300B667CB /* RSHTMLMetadata+Extension.swift in Sources */, - 51919FF824AB8B7700541E64 /* TimelineView.swift in Sources */, - 1717535624BADF33004498C6 /* GeneralPreferencesModel.swift in Sources */, - 51E4992B24A8676300B667CB /* ArticleArray.swift in Sources */, - 51B54AB324B5AC830014348B /* ArticleExtractorButtonState.swift in Sources */, - 17D5F17224B0BC6700375168 /* SidebarToolbarModel.swift in Sources */, - 514E6C0724AD2B5F00AC6F6E /* Image-Extensions.swift in Sources */, - 51E4994D24A8734C00B667CB /* ExtensionPointIdentifer.swift in Sources */, - 51C65AFD24CCB2C9008EB3BD /* TimelineItems.swift in Sources */, - DF98E2BE2578AC0000F18944 /* AddNewsBlurAccountView.swift in Sources */, - 51B54A6724B549FE0014348B /* ArticleIconSchemeHandler.swift in Sources */, - 51E4992224A8095600B667CB /* URL-Extensions.swift in Sources */, - 51E4990424A808C300B667CB /* WebFeedIconDownloader.swift in Sources */, - 51E498CB24A8085D00B667CB /* TodayFeedDelegate.swift in Sources */, - 51DC079C2552083500A3F79F /* ArticleTextSize.swift in Sources */, - 51B80F1F24BE531200C6C32D /* SharingServiceView.swift in Sources */, - 17D232A924AFF10A0005F075 /* AddWebFeedModel.swift in Sources */, - 51A8005224CC453C00F41F1D /* ReplaySubject.swift in Sources */, - 51E4993324A867E700B667CB /* AppNotifications.swift in Sources */, - 51B80F4224BE588200C6C32D /* SharingServicePickerDelegate.swift in Sources */, - 51E4990624A808C300B667CB /* ImageDownloader.swift in Sources */, - 51E4994F24A8734C00B667CB /* TwitterFeedProvider-Extensions.swift in Sources */, - 17241280257BBF3E00ACCEBC /* AddFeedWranglerViewModel.swift in Sources */, - 51E498CA24A8085D00B667CB /* SmartFeedDelegate.swift in Sources */, - DF98E2B02578AA5C00F18944 /* AddFeedWranglerAccountView.swift in Sources */, - 5177470A24B2F87600EB0F74 /* SidebarListStyleModifier.swift in Sources */, - 1769E33824BD97CB000E1E8E /* AccountUpdateErrors.swift in Sources */, - 51E4990524A808C300B667CB /* FeaturedImageDownloader.swift in Sources */, - 5181C5AE24AF89B1002E0F70 /* PreferredColorSchemeModifier.swift in Sources */, - 1769E32224BC5925000E1E8E /* AccountsPreferencesModel.swift in Sources */, - 51E4991624A8090300B667CB /* ArticleUtilities.swift in Sources */, - 51919FF224AB864A00541E64 /* TimelineModel.swift in Sources */, - 51E4991A24A8090F00B667CB /* IconImage.swift in Sources */, - 1799E6AA24C2F93F00511E91 /* InspectorPlatformModifier.swift in Sources */, - 51B8104624C0E6D200C6C32D /* TimelineTextSizer.swift in Sources */, - 51E4992724A80AAB00B667CB /* AppAssets.swift in Sources */, - 51E49A0124A91FC100B667CB /* SidebarContainerView.swift in Sources */, - 51E4995B24A875D500B667CB /* ArticlePasteboardWriter.swift in Sources */, - 51E4993424A867E700B667CB /* UserInfoKey.swift in Sources */, - 1776E88F24AC5F8A00E78166 /* AppDefaults.swift in Sources */, - 1729529724AA1CD000D65E66 /* MacPreferencePanes.swift in Sources */, - 51E4994C24A8734C00B667CB /* RedditFeedProvider-Extensions.swift in Sources */, - 1729529324AA1CAA00D65E66 /* AccountsPreferencesView.swift in Sources */, - 171BCB8D24CB08A3006E22D9 /* FixAccountCredentialView.swift in Sources */, - 1769E32D24BD20A0000E1E8E /* AccountDetailView.swift in Sources */, - 51919FAD24AA8CCA00541E64 /* UnreadCountView.swift in Sources */, - 51E498C924A8085D00B667CB /* PseudoFeed.swift in Sources */, - 51E498FC24A808BA00B667CB /* FaviconURLFinder.swift in Sources */, - 51E4991C24A8092000B667CB /* NSAttributedString+NetNewsWire.swift in Sources */, - 1769E32B24BCB030000E1E8E /* ConfiguredAccountRow.swift in Sources */, - DF98E2C62578AD1B00F18944 /* AddReaderAPIAccountView.swift in Sources */, - FF64D0E824AF53EE0084080A /* RefreshProgressModel.swift in Sources */, - 51E499D924A912C200B667CB /* SceneModel.swift in Sources */, - 51919FB424AAB97900541E64 /* FeedIconImageLoader.swift in Sources */, - 1769E32524BC5A65000E1E8E /* AddAccountView.swift in Sources */, - 51E4994A24A8734C00B667CB /* ExtensionPointManager.swift in Sources */, - 514E6C0324AD29A300AC6F6E /* TimelineItemStatusView.swift in Sources */, - 1724126A257BBEBB00ACCEBC /* AddFeedbinViewModel.swift in Sources */, - 51B54A6524B549B20014348B /* WrapperScriptMessageHandler.swift in Sources */, - 51E4996D24A8762D00B667CB /* ArticleExtractor.swift in Sources */, - 1704053524E5985A00A00787 /* SceneNavigationModel.swift in Sources */, - 17241278257BBEE700ACCEBC /* AddFeedlyViewModel.swift in Sources */, - 51E4994024A8713B00B667CB /* AccountRefreshTimer.swift in Sources */, - 51E49A0424A91FF600B667CB /* SceneNavigationView.swift in Sources */, - 17241290257BBFAD00ACCEBC /* AddReaderAPIViewModel.swift in Sources */, - 51E498CC24A8085D00B667CB /* SearchFeedDelegate.swift in Sources */, - 51E498C824A8085D00B667CB /* SmartFeedsController.swift in Sources */, - 175942AB24AD533200585066 /* RefreshInterval.swift in Sources */, - 51B80F4624BF76E700C6C32D /* Browser.swift in Sources */, - 51E4992C24A8676300B667CB /* ArticleSorter.swift in Sources */, - 514E6C0A24AD39AD00AC6F6E /* ArticleIconImageLoader.swift in Sources */, - 51E4995024A8734C00B667CB /* ExtensionPoint.swift in Sources */, - 51E4990E24A808CC00B667CB /* HTMLMetadataDownloader.swift in Sources */, - DF98E29A2578A73A00F18944 /* AddCloudKitAccountView.swift in Sources */, - 51E498FB24A808BA00B667CB /* FaviconGenerator.swift in Sources */, - 17D5F19524B0C1DD00375168 /* SidebarToolbarModifier.swift in Sources */, - 17241288257BBF7000ACCEBC /* AddNewsBlurViewModel.swift in Sources */, - 51E4996724A8760B00B667CB /* ArticleStylesManager.swift in Sources */, - 1729529B24AA1FD200D65E66 /* MacSearchField.swift in Sources */, - 51408B7F24A9EC6F0073CF4E /* SidebarItem.swift in Sources */, - 514E6BDB24ACEA0400AC6F6E /* TimelineItemView.swift in Sources */, - 51B8BCE724C25F7C00360B00 /* TimelineContextMenu.swift in Sources */, - 51E4996E24A8764C00B667CB /* ActivityManager.swift in Sources */, - 51A8002E24CC451600F41F1D /* ShareReplay.swift in Sources */, - 1769E33024BD6271000E1E8E /* EditAccountCredentialsView.swift in Sources */, - 51E4995A24A873F900B667CB /* ErrorHandler.swift in Sources */, - 5194737124BBCAF4001A2939 /* TimelineSortOrderView.swift in Sources */, - 51E4991F24A8094300B667CB /* RSImage-AppIcons.swift in Sources */, - 51A5769724AE617200078888 /* ArticleContainerView.swift in Sources */, - 51E4991224A808FB00B667CB /* AddWebFeedDefaultContainer.swift in Sources */, - 51B54B6724B6A7960014348B /* WebStatusBarView.swift in Sources */, - 51E4993E24A870F900B667CB /* UserNotificationManager.swift in Sources */, - 51E4992E24A8676300B667CB /* FetchRequestQueue.swift in Sources */, - 17386B5E2577BC820014C8B2 /* AccountType+Helpers.swift in Sources */, - 51E498CF24A8085D00B667CB /* SmartFeed.swift in Sources */, - 51E4990724A808C300B667CB /* AuthorAvatarDownloader.swift in Sources */, - 51E4997424A8784400B667CB /* DefaultFeedsImporter.swift in Sources */, - 51919FF524AB869C00541E64 /* TimelineItem.swift in Sources */, - 51E4992024A8095000B667CB /* RSImage-Extensions.swift in Sources */, - 51E499FE24A9137600B667CB /* SidebarModel.swift in Sources */, - 51E498FE24A808BA00B667CB /* FaviconDownloader.swift in Sources */, - 51919FA724AA64B000541E64 /* SidebarView.swift in Sources */, - 51E498FD24A808BA00B667CB /* ColorHash.swift in Sources */, - 51E4991824A8090A00B667CB /* CacheCleaner.swift in Sources */, - 51B54ABC24B5BEF20014348B /* ArticleView.swift in Sources */, - 51E498CD24A8085D00B667CB /* SearchTimelineFeedDelegate.swift in Sources */, - 51E4996124A875F400B667CB /* ArticleRenderer.swift in Sources */, - FA80C13F24B072AB00974098 /* AddFolderModel.swift in Sources */, - 51392D1C24AC19A000BE0D35 /* SidebarExpandedContainers.swift in Sources */, - 51C0515F24A77DF800194D5E /* MainApp.swift in Sources */, - 51B54A4324B5499B0014348B /* WebViewProvider.swift in Sources */, - 1799E6CE24C320D600511E91 /* InspectorModel.swift in Sources */, - 514E6C0024AD255D00AC6F6E /* PreviewArticles.swift in Sources */, - 51C4CFF424D37D1F00AF9874 /* Secrets.swift in Sources */, - 1729529524AA1CAA00D65E66 /* GeneralPreferencesView.swift in Sources */, - 1729529424AA1CAA00D65E66 /* AdvancedPreferencesView.swift in Sources */, - 5177470424B2657F00EB0F74 /* TimelineToolbarModifier.swift in Sources */, - 51A8001324CA0FC700F41F1D /* Sink.swift in Sources */, - 17241249257B8A8A00ACCEBC /* AddFeedlyAccountView.swift in Sources */, - 51E4992D24A8676300B667CB /* FetchRequestOperation.swift in Sources */, - 51E4992424A8098400B667CB /* SmartFeedPasteboardWriter.swift in Sources */, - 17D3CEE4257C4D2300E74939 /* AddAccountSignUp.swift in Sources */, - 51E4991424A808FF00B667CB /* ArticleStringFormatter.swift in Sources */, - 51B54A6624B549CB0014348B /* PreloadedWebView.swift in Sources */, - 51E4991024A808DE00B667CB /* SmallIconProvider.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; 653813422680E2DA007A082C /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -5320,7 +3914,7 @@ 5193CD59245E44A90092735E /* RedditFeedProvider-Extensions.swift in Sources */, 65ED3FE6235DEF6C0081F399 /* RenameWindowController.swift in Sources */, 65ED3FE7235DEF6C0081F399 /* SendToMicroBlogCommand.swift in Sources */, - 65ED3FE8235DEF6C0081F399 /* ArticleStyle.swift in Sources */, + 65ED3FE8235DEF6C0081F399 /* ArticleTheme.swift in Sources */, 65ED3FE9235DEF6C0081F399 /* FaviconURLFinder.swift in Sources */, 6538131C2680E17F007A082C /* UserInfoKey.swift in Sources */, 65ED3FEA235DEF6C0081F399 /* SidebarViewController+ContextualMenus.swift in Sources */, @@ -5331,7 +3925,7 @@ 65ED3FEE235DEF6C0081F399 /* UserNotificationManager.swift in Sources */, 653813182680E152007A082C /* AccountType+Helpers.swift in Sources */, 65ED3FEF235DEF6C0081F399 /* ScriptingObjectContainer.swift in Sources */, - 65ED3FF0235DEF6C0081F399 /* ArticleStylesManager.swift in Sources */, + 65ED3FF0235DEF6C0081F399 /* ArticleThemesManager.swift in Sources */, 65ED3FF1235DEF6C0081F399 /* DetailContainerView.swift in Sources */, 65ED3FF2235DEF6C0081F399 /* SharingServiceDelegate.swift in Sources */, 515A50E7243D07A90089E588 /* ExtensionPointManager.swift in Sources */, @@ -5454,6 +4048,7 @@ 51C452A422650A2D00C03939 /* ArticleUtilities.swift in Sources */, 51EF0F79227716380050506E /* ColorHash.swift in Sources */, 51F9F3FB23DFB25700A314FD /* Animations.swift in Sources */, + 5195C1DA2720205F00888867 /* ShadowTableChanges.swift in Sources */, 5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */, B2B80778239C4C7000F191E0 /* RSImage-AppIcons.swift in Sources */, 518ED21D23D0F26000E0A862 /* UIViewController-Extensions.swift in Sources */, @@ -5463,6 +4058,7 @@ 5186A635235EF3A800C97195 /* VibrantLabel.swift in Sources */, 51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */, 51B62E68233186730085F949 /* IconView.swift in Sources */, + 179D280D26F73D83003B2E0A /* ArticleThemePlist.swift in Sources */, 51C45296226509D300C03939 /* OPMLExporter.swift in Sources */, 51C45291226509C800C03939 /* SmartFeed.swift in Sources */, 51C452A722650A3D00C03939 /* RSImage-Extensions.swift in Sources */, @@ -5532,20 +4128,19 @@ 51FA73A82332BE880090D516 /* ExtractedArticle.swift in Sources */, 51C4527C2265091600C03939 /* MasterTimelineDefaultCellLayout.swift in Sources */, 51E4398023805EBC00015C31 /* AddComboTableViewCell.swift in Sources */, - 51C4529A22650A0400C03939 /* ArticleStyle.swift in Sources */, + 51C4529A22650A0400C03939 /* ArticleTheme.swift in Sources */, 51C4527F2265092C00C03939 /* ArticleViewController.swift in Sources */, 51C4526A226508F600C03939 /* MasterFeedTableViewCellLayout.swift in Sources */, 51C452AE2265104D00C03939 /* ArticleStringFormatter.swift in Sources */, 512E08E62268800D00BDCFDD /* FolderTreeControllerDelegate.swift in Sources */, 51A9A60A2382FD240033AADF /* PoppableGestureRecognizerDelegate.swift in Sources */, 51DC079A2552083500A3F79F /* ArticleTextSize.swift in Sources */, - 51C4529922650A0000C03939 /* ArticleStylesManager.swift in Sources */, + 51C4529922650A0000C03939 /* ArticleThemesManager.swift in Sources */, 51EF0F802277A8330050506E /* MasterTimelineCellLayout.swift in Sources */, 51F85BF722749FA100C787DC /* UIFont-Extensions.swift in Sources */, 51C452AF2265108300C03939 /* ArticleArray.swift in Sources */, 51C4528E2265099C00C03939 /* SmartFeedsController.swift in Sources */, 51C9DE5823EA2EF4003D5A6D /* WrapperScriptMessageHandler.swift in Sources */, - 51627A6B238629D8007B3B4B /* MasterFeedDataSource.swift in Sources */, 51B5C87D23F2346200032075 /* ExtensionContainersFile.swift in Sources */, 51102165233A7D6C0007A5F7 /* ArticleExtractorButton.swift in Sources */, 5141E7392373C18B0013FF27 /* WebFeedInspectorViewController.swift in Sources */, @@ -5555,6 +4150,7 @@ 512392BE24E33A3C00F11704 /* RedditSelectAccountTableViewController.swift in Sources */, 515A517B243E90260089E588 /* ExtensionPoint.swift in Sources */, 51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */, + 17D643B226F8A436008D4C05 /* ArticleThemeDownloader.swift in Sources */, 51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */, 51F9F3F723DF6DB200A314FD /* ArticleIconSchemeHandler.swift in Sources */, 512392C524E3451400F11704 /* TwitterEnterDetailTableViewController.swift in Sources */, @@ -5576,6 +4172,7 @@ 512D554423C804DE0023FFFA /* OpenInSafariActivity.swift in Sources */, 512392C224E33A3C00F11704 /* RedditEnterDetailTableViewController.swift in Sources */, 51C452762265091600C03939 /* MasterTimelineViewController.swift in Sources */, + 5195C1DC2720BD3000888867 /* MasterFeedRowIdentifier.swift in Sources */, 5108F6D823763094001ABC45 /* TickMarkSlider.swift in Sources */, 51C452882265093600C03939 /* AddFeedViewController.swift in Sources */, 51B5C8C023F3866C00032075 /* ExtensionFeedAddRequestFile.swift in Sources */, @@ -5594,7 +4191,7 @@ B24E9ADE245AB88400DA5718 /* NSAttributedString+NetNewsWire.swift in Sources */, C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */, 17D7586F2679C21800B17787 /* OnePasswordExtension.m in Sources */, - 51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */, + 17071EF126F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, 84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */, 512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */, @@ -5604,24 +4201,24 @@ 51C45268226508F600C03939 /* MasterFeedUnreadCountView.swift in Sources */, D3A39865246505DF00F9A366 /* FindInArticleActivity.swift in Sources */, 5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */, + 5137C2EA26F63AE6009EFEDB /* ArticleThemeImporter.swift in Sources */, 51C4529F22650A1900C03939 /* AuthorAvatarDownloader.swift in Sources */, 5108F6D22375EED2001ABC45 /* TimelineCustomizerViewController.swift in Sources */, 519E743D22C663F900A78E47 /* SceneDelegate.swift in Sources */, FFD43E412340F488009E5CA3 /* MarkAsReadAlertController.swift in Sources */, 51C452A322650A1E00C03939 /* HTMLMetadataDownloader.swift in Sources */, 51C4528D2265095F00C03939 /* AddFolderViewController.swift in Sources */, - 51DC37072402153E0095D371 /* UpdateSelectionOperation.swift in Sources */, 51C452782265091600C03939 /* MasterTimelineCellData.swift in Sources */, 5148F4552336DB7000F8CD8B /* MasterTimelineTitleView.swift in Sources */, 515A517C243E90260089E588 /* ExtensionPointManager.swift in Sources */, 51627A6723861DA3007B3B4B /* MasterFeedViewController+Drag.swift in Sources */, 51FFF0C4235EE8E5002762AA /* VibrantButton.swift in Sources */, 51C45259226508D300C03939 /* AppDefaults.swift in Sources */, + 510FFAB326EEA22C00F32265 /* ArticleThemesTableViewController.swift in Sources */, 51CE1C0B23622007005548FC /* RefreshProgressView.swift in Sources */, 511D4419231FC02D00FB1562 /* KeyboardManager.swift in Sources */, 51A1699D235E10D700EB091F /* SettingsViewController.swift in Sources */, 51C45293226509C800C03939 /* StarredFeedDelegate.swift in Sources */, - 513CCF2524880C1500C55709 /* MasterFeedTableViewIdentifier.swift in Sources */, 51D6A5BC23199C85001C27D8 /* MasterTimelineDataSource.swift in Sources */, 51934CCB230F599B006127BE /* InteractiveNavigationController.swift in Sources */, 769F2ED513DA03EE75B993A8 /* NewsBlurAccountViewController.swift in Sources */, @@ -5647,6 +4244,7 @@ 844B5B5B1FEA00FB00C7C76A /* TimelineKeyboardDelegate.swift in Sources */, 842E45DD1ED8C54B000A8B52 /* Browser.swift in Sources */, 84216D0322128B9D0049B9B9 /* DetailWebViewController.swift in Sources */, + 17071EF026F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */, 8444C8F21FED81840051386C /* OPMLExporter.swift in Sources */, 849A975E1ED9EB72007D329B /* MainWindowController.swift in Sources */, 84F2D53A1FC2308B00998D64 /* UnreadFeed.swift in Sources */, @@ -5683,6 +4281,7 @@ 8426118A1FCB67AA0086A189 /* WebFeedIconDownloader.swift in Sources */, 84C9FC7B22629E1200D921D6 /* PreferencesControlsBackgroundView.swift in Sources */, 84162A152038C12C00035290 /* MarkCommandValidationStatus.swift in Sources */, + 17D643B126F8A436008D4C05 /* ArticleThemeDownloader.swift in Sources */, 84E95D241FB1087500552D99 /* ArticlePasteboardWriter.swift in Sources */, 849A975B1ED9EB0D007D329B /* ArticleUtilities.swift in Sources */, 849ADEE8235981A0000E1B81 /* NNW3OpenPanelAccessoryViewController.swift in Sources */, @@ -5690,7 +4289,7 @@ 84A37CB5201ECD610087C5AF /* RenameWindowController.swift in Sources */, 84A14FF320048CA70046AD9A /* SendToMicroBlogCommand.swift in Sources */, B2B8075E239C49D300F191E0 /* RSImage-AppIcons.swift in Sources */, - 849A97891ED9ECEF007D329B /* ArticleStyle.swift in Sources */, + 849A97891ED9ECEF007D329B /* ArticleTheme.swift in Sources */, 84FF69B11FC3793300DC198E /* FaviconURLFinder.swift in Sources */, 84B7178C201E66580091657D /* SidebarViewController+ContextualMenus.swift in Sources */, 842611A21FCB769D0086A189 /* RSHTMLMetadata+Extension.swift in Sources */, @@ -5698,7 +4297,7 @@ 51FE10032345529D0056195D /* UserNotificationManager.swift in Sources */, D5907DB22004BB37005947E5 /* ScriptingObjectContainer.swift in Sources */, 51BC4AFF247277E0000A6ED8 /* URL-Extensions.swift in Sources */, - 849A978A1ED9ECEF007D329B /* ArticleStylesManager.swift in Sources */, + 849A978A1ED9ECEF007D329B /* ArticleThemesManager.swift in Sources */, 8405DD8A2213E0E3008CE1BF /* DetailContainerView.swift in Sources */, 51107746243BEE2500D97C8C /* ExtensionPointPreferencesViewController.swift in Sources */, 519B8D332143397200FA689C /* SharingServiceDelegate.swift in Sources */, @@ -5791,6 +4390,7 @@ 510C418124E5D1AE008226FD /* ExtensionContainers.swift in Sources */, 51EC114C2149FE3300B296E3 /* FolderTreeMenu.swift in Sources */, 849ADEE42359817E000E1B81 /* NNW3ImportController.swift in Sources */, + 179C39EB26F76B3800D4E741 /* ArticleThemePlist.swift in Sources */, 849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */, 51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */, 510C43F7243D035C009F70C3 /* ExtensionPoint.swift in Sources */, @@ -6094,34 +4694,6 @@ }; name = Release; }; - 51C0516424A77DF800194D5E /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 51C0519724A7808F00194D5E /* NetNewsWire_multiplatform_iOSapp_target.xcconfig */; - buildSettings = { - }; - name = Debug; - }; - 51C0516524A77DF800194D5E /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 51C0519724A7808F00194D5E /* NetNewsWire_multiplatform_iOSapp_target.xcconfig */; - buildSettings = { - }; - name = Release; - }; - 51C0516624A77DF800194D5E /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 51C0519824A7808F00194D5E /* NetNewsWire_multiplatform_macOSapp_target.xcconfig */; - buildSettings = { - }; - name = Debug; - }; - 51C0516724A77DF800194D5E /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 51C0519824A7808F00194D5E /* NetNewsWire_multiplatform_macOSapp_target.xcconfig */; - buildSettings = { - }; - name = Release; - }; 653813572680E2DA007A082C /* Debug */ = { isa = XCBuildConfiguration; baseConfigurationReference = 6538135B2680E3A9007A082C /* NetNewsWire_shareextension_target_macappstore.xcconfig */; @@ -6286,24 +4858,6 @@ defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; - 51C0518D24A77DF800194D5E /* Build configuration list for PBXNativeTarget "Multiplatform iOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 51C0516424A77DF800194D5E /* Debug */, - 51C0516524A77DF800194D5E /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 51C0518E24A77DF800194D5E /* Build configuration list for PBXNativeTarget "Multiplatform macOS" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 51C0516624A77DF800194D5E /* Debug */, - 51C0516724A77DF800194D5E /* Release */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; 653813562680E2DA007A082C /* Build configuration list for PBXNativeTarget "NetNewsWire Share Extension MAS" */ = { isa = XCConfigurationList; buildConfigurations = ( @@ -6387,6 +4941,14 @@ minimumVersion = 2.0.0; }; }; + 179D280926F6F93D003B2E0A /* XCRemoteSwiftPackageReference "Zip" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/marmelroy/Zip.git"; + requirement = { + kind = revision; + revision = 059e7346082d02de16220cd79df7db18ddeba8c3; + }; + }; 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Ranchero-Software/RSCore.git"; @@ -6432,7 +4994,7 @@ repositoryURL = "https://github.com/Ranchero-Software/RSParser.git"; requirement = { kind = upToNextMajorVersion; - minimumVersion = 2.0.0; + minimumVersion = 2.0.3; }; }; 653813412680E2DA007A082C /* XCRemoteSwiftPackageReference "RSCore" */ = { @@ -6451,81 +5013,15 @@ package = 17192AD82567B3D500AAEACA /* XCRemoteSwiftPackageReference "Sparkle-Binary" */; productName = RSSparkle; }; - 17386B6B2577BD820014C8B2 /* RSSparkle */ = { + 179C39E926F76B0500D4E741 /* Zip */ = { isa = XCSwiftPackageProductDependency; - package = 17192AD82567B3D500AAEACA /* XCRemoteSwiftPackageReference "Sparkle-Binary" */; - productName = RSSparkle; + package = 179D280926F6F93D003B2E0A /* XCRemoteSwiftPackageReference "Zip" */; + productName = Zip; }; - 17386B942577C6240014C8B2 /* RSCore */ = { + 179D280A26F6F93D003B2E0A /* Zip */ = { isa = XCSwiftPackageProductDependency; - package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */; - productName = RSCore; - }; - 17386B972577C6240014C8B2 /* RSTree */ = { - isa = XCSwiftPackageProductDependency; - package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */; - productName = RSTree; - }; - 17386B9A2577C6240014C8B2 /* RSWeb */ = { - isa = XCSwiftPackageProductDependency; - package = 51383A3024D1F90E0027E272 /* XCRemoteSwiftPackageReference "RSWeb" */; - productName = RSWeb; - }; - 17386B9D2577C6240014C8B2 /* RSDatabase */ = { - isa = XCSwiftPackageProductDependency; - package = 51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */; - productName = RSDatabase; - }; - 17386BA32577C6240014C8B2 /* RSParser */ = { - isa = XCSwiftPackageProductDependency; - package = 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */; - productName = RSParser; - }; - 17386BB52577C7340014C8B2 /* RSCoreResources */ = { - isa = XCSwiftPackageProductDependency; - package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */; - productName = RSCoreResources; - }; - 17A1597B24E3DEDD005DA32A /* RSCore */ = { - isa = XCSwiftPackageProductDependency; - package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */; - productName = RSCore; - }; - 17A1597E24E3DEDD005DA32A /* RSTree */ = { - isa = XCSwiftPackageProductDependency; - package = 510ECA4024D1DCD0001C31A6 /* XCRemoteSwiftPackageReference "RSTree" */; - productName = RSTree; - }; - 17A1598124E3DEDD005DA32A /* RSWeb */ = { - isa = XCSwiftPackageProductDependency; - package = 51383A3024D1F90E0027E272 /* XCRemoteSwiftPackageReference "RSWeb" */; - productName = RSWeb; - }; - 17A1598424E3DEDD005DA32A /* RSDatabase */ = { - isa = XCSwiftPackageProductDependency; - package = 51B0DF0D24D24E3B000AD99E /* XCRemoteSwiftPackageReference "RSDatabase" */; - productName = RSDatabase; - }; - 17A1598724E3DEDD005DA32A /* RSParser */ = { - isa = XCSwiftPackageProductDependency; - package = 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */; - productName = RSParser; - }; - 17E0080E25936DF6000C23F0 /* Articles */ = { - isa = XCSwiftPackageProductDependency; - productName = Articles; - }; - 17E0081125936DF6000C23F0 /* ArticlesDatabase */ = { - isa = XCSwiftPackageProductDependency; - productName = ArticlesDatabase; - }; - 17E0081425936DFF000C23F0 /* Secrets */ = { - isa = XCSwiftPackageProductDependency; - productName = Secrets; - }; - 17E0081725936DFF000C23F0 /* SyncDatabase */ = { - isa = XCSwiftPackageProductDependency; - productName = SyncDatabase; + package = 179D280926F6F93D003B2E0A /* XCRemoteSwiftPackageReference "Zip" */; + productName = Zip; }; 17EF6A2025C4E5B4002C9F81 /* RSWeb */ = { isa = XCSwiftPackageProductDependency; @@ -6637,14 +5133,6 @@ package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */; productName = RSCoreResources; }; - 516B695A24D2F28600B5702F /* Account */ = { - isa = XCSwiftPackageProductDependency; - productName = Account; - }; - 516B695C24D2F28E00B5702F /* Account */ = { - isa = XCSwiftPackageProductDependency; - productName = Account; - }; 516B695E24D2F33B00B5702F /* Account */ = { isa = XCSwiftPackageProductDependency; productName = Account; @@ -6696,27 +5184,6 @@ isa = XCSwiftPackageProductDependency; productName = Secrets; }; - 51E0614425A5A28E00194066 /* Articles */ = { - isa = XCSwiftPackageProductDependency; - productName = Articles; - }; - 51E0614725A5A28E00194066 /* ArticlesDatabase */ = { - isa = XCSwiftPackageProductDependency; - productName = ArticlesDatabase; - }; - 51E0614A25A5A28E00194066 /* Secrets */ = { - isa = XCSwiftPackageProductDependency; - productName = Secrets; - }; - 51E0614D25A5A28E00194066 /* SyncDatabase */ = { - isa = XCSwiftPackageProductDependency; - productName = SyncDatabase; - }; - 51E0615025A5A29600194066 /* CrashReporter */ = { - isa = XCSwiftPackageProductDependency; - package = 519CA8E325841DB700EB079A /* XCRemoteSwiftPackageReference "plcrashreporter" */; - productName = CrashReporter; - }; 6538131D2680E1CA007A082C /* Account */ = { isa = XCSwiftPackageProductDependency; productName = Account; diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b58a85312..dc8075713 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -6,8 +6,8 @@ "repositoryURL": "https://github.com/Thomvis/BrightFutures.git", "state": { "branch": null, - "revision": "9279defa6bdc21501ce740266e5a14d0119ddc63", - "version": "8.0.1" + "revision": "939858b811026f85e87847a808f0bea2f187e5c4", + "version": "8.1.0" } }, { @@ -33,8 +33,8 @@ "repositoryURL": "https://github.com/tid-kijyun/Kanna.git", "state": { "branch": null, - "revision": "c657fb9f5827ef138068215c76ad0bb62bbc92da", - "version": "5.2.4" + "revision": "f9e4922223dd0d3dfbf02ca70812cf5531fc0593", + "version": "5.2.7" } }, { @@ -51,8 +51,8 @@ "repositoryURL": "https://github.com/microsoft/plcrashreporter.git", "state": { "branch": null, - "revision": "de6b8f9db4b2a0aa859a5507550a70548e4da936", - "version": "1.8.1" + "revision": "d747ab5de269cd44022bbe96ff9609d8626694ab", + "version": "1.9.0" } }, { @@ -60,8 +60,8 @@ "repositoryURL": "https://github.com/Ranchero-Software/RSCore.git", "state": { "branch": null, - "revision": "2a13519b4d91843faa6aff4245b0e387dc64eafe", - "version": "1.0.6" + "revision": "060b12a3d3b6d27d57b2fae84160bfec91ec7118", + "version": "1.0.7" } }, { @@ -78,8 +78,8 @@ "repositoryURL": "https://github.com/Ranchero-Software/RSParser.git", "state": { "branch": null, - "revision": "7de3940c67a5fd128c8088eaa218617f2d3cc3ee", - "version": "2.0.2" + "revision": "d5b50ff78905ebfaf26dd698e0e5d3ed8269dd9b", + "version": "2.0.3" } }, { @@ -96,8 +96,8 @@ "repositoryURL": "https://github.com/Ranchero-Software/RSWeb.git", "state": { "branch": null, - "revision": "2f9ad98736c5c17dfb2be0b3cc06e71a49b061fa", - "version": "1.0.1" + "revision": "2f7849a9ad2cb461b3d6c9c920e163596e6b5d7b", + "version": "1.0.3" } }, { @@ -117,6 +117,15 @@ "revision": "9483a5d459b45c3ffd059f7b55f9638e268632fd", "version": "1.5.0" } + }, + { + "package": "Zip", + "repositoryURL": "https://github.com/marmelroy/Zip.git", + "state": { + "branch": null, + "revision": "059e7346082d02de16220cd79df7db18ddeba8c3", + "version": null + } } ] }, diff --git a/NetNewsWire.xcodeproj/xcshareddata/xcschemes/Multiplatform iOS.xcscheme b/NetNewsWire.xcodeproj/xcshareddata/xcschemes/Multiplatform iOS.xcscheme deleted file mode 100644 index e06266bca..000000000 --- a/NetNewsWire.xcodeproj/xcshareddata/xcschemes/Multiplatform iOS.xcscheme +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/NetNewsWire.xcodeproj/xcshareddata/xcschemes/Multiplatform macOS.xcscheme b/NetNewsWire.xcodeproj/xcshareddata/xcschemes/Multiplatform macOS.xcscheme deleted file mode 100644 index 9ac4c281f..000000000 --- a/NetNewsWire.xcodeproj/xcshareddata/xcschemes/Multiplatform macOS.xcscheme +++ /dev/null @@ -1,96 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/Shared/Activity/ActivityManager.swift b/Shared/Activity/ActivityManager.swift index 266911e5d..ab369c087 100644 --- a/Shared/Activity/ActivityManager.swift +++ b/Shared/Activity/ActivityManager.swift @@ -175,13 +175,14 @@ private extension ActivityManager { activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo] activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String }) + activity.persistentIdentifier = feed.feedID?.description ?? "" + #if os(iOS) activity.suggestedInvocationPhrase = title activity.isEligibleForPrediction = true - activity.persistentIdentifier = feed.feedID?.description ?? "" activity.contentAttributeSet?.relatedUniqueIdentifier = feed.feedID?.description ?? "" #endif - + return activity } @@ -200,11 +201,12 @@ private extension ActivityManager { activity.isEligibleForHandoff = true + activity.persistentIdentifier = ActivityManager.identifer(for: article) + #if os(iOS) activity.keywords = Set(makeKeywords(article)) activity.isEligibleForSearch = true activity.isEligibleForPrediction = false - activity.persistentIdentifier = ActivityManager.identifer(for: article) updateReadArticleSearchAttributes(with: article) #endif diff --git a/Shared/Article Rendering/ArticleRenderer.swift b/Shared/Article Rendering/ArticleRenderer.swift index 85fc0db30..9ecd6a7b7 100644 --- a/Shared/Article Rendering/ArticleRenderer.swift +++ b/Shared/Article Rendering/ArticleRenderer.swift @@ -37,15 +37,78 @@ struct ArticleRenderer { private let article: Article? private let extractedArticle: ExtractedArticle? - private let articleStyle: ArticleStyle + private let articleTheme: ArticleTheme private let title: String private let body: String private let baseURL: String? + + private static let longDateTimeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .long + formatter.timeStyle = .medium + return formatter + }() - private init(article: Article?, extractedArticle: ExtractedArticle?, style: ArticleStyle) { + private static let mediumDateTimeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .short + return formatter + }() + + private static let shortDateTimeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .short + return formatter + }() + + private static let longDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .long + formatter.timeStyle = .none + return formatter + }() + + private static let mediumDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .medium + formatter.timeStyle = .none + return formatter + }() + + private static let shortDateFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .short + formatter.timeStyle = .none + return formatter + }() + + private static let longTimeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .long + return formatter + }() + + private static let mediumTimeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .medium + return formatter + }() + + private static let shortTimeFormatter: DateFormatter = { + let formatter = DateFormatter() + formatter.dateStyle = .none + formatter.timeStyle = .short + return formatter + }() + + private init(article: Article?, extractedArticle: ExtractedArticle?, theme: ArticleTheme) { self.article = article self.extractedArticle = extractedArticle - self.articleStyle = style + self.articleTheme = theme self.title = article?.sanitizedTitle() ?? "" if let content = extractedArticle?.content { self.body = content @@ -58,28 +121,28 @@ struct ArticleRenderer { // MARK: - API - static func articleHTML(article: Article, extractedArticle: ExtractedArticle? = nil, style: ArticleStyle) -> Rendering { - let renderer = ArticleRenderer(article: article, extractedArticle: extractedArticle, style: style) + static func articleHTML(article: Article, extractedArticle: ExtractedArticle? = nil, theme: ArticleTheme) -> Rendering { + let renderer = ArticleRenderer(article: article, extractedArticle: extractedArticle, theme: theme) return (renderer.articleCSS, renderer.articleHTML, renderer.title, renderer.baseURL ?? "") } - static func multipleSelectionHTML(style: ArticleStyle) -> Rendering { - let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style) + static func multipleSelectionHTML(theme: ArticleTheme) -> Rendering { + let renderer = ArticleRenderer(article: nil, extractedArticle: nil, theme: theme) return (renderer.articleCSS, renderer.multipleSelectionHTML, renderer.title, renderer.baseURL ?? "") } - static func loadingHTML(style: ArticleStyle) -> Rendering { - let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style) + static func loadingHTML(theme: ArticleTheme) -> Rendering { + let renderer = ArticleRenderer(article: nil, extractedArticle: nil, theme: theme) return (renderer.articleCSS, renderer.loadingHTML, renderer.title, renderer.baseURL ?? "") } - static func noSelectionHTML(style: ArticleStyle) -> Rendering { - let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style) + static func noSelectionHTML(theme: ArticleTheme) -> Rendering { + let renderer = ArticleRenderer(article: nil, extractedArticle: nil, theme: theme) return (renderer.articleCSS, renderer.noSelectionHTML, renderer.title, renderer.baseURL ?? "") } - static func noContentHTML(style: ArticleStyle) -> Rendering { - let renderer = ArticleRenderer(article: nil, extractedArticle: nil, style: style) + static func noContentHTML(theme: ArticleTheme) -> Rendering { + let renderer = ArticleRenderer(article: nil, extractedArticle: nil, theme: theme) return (renderer.articleCSS, renderer.noContentHTML, renderer.title, renderer.baseURL ?? "") } } @@ -128,18 +191,11 @@ private extension ArticleRenderer { }() func styleString() -> String { - return articleStyle.css ?? ArticleRenderer.defaultStyleSheet + return articleTheme.css ?? ArticleRenderer.defaultStyleSheet } func template() -> String { - return articleStyle.template ?? ArticleRenderer.defaultTemplate - } - - func titleOrTitleLink() -> String { - if let link = article?.preferredLink { - return title.htmlByAddingLink(link) - } - return title + return articleTheme.template ?? ArticleRenderer.defaultTemplate } func articleSubstitutions() -> [String: String] { @@ -150,15 +206,16 @@ private extension ArticleRenderer { return d } - let title = titleOrTitleLink() d["title"] = title + d["preferred_link"] = article.preferredLink ?? "" - if let externalLink = article.externalURL, externalLink != article.preferredLink { - let displayLink = externalLink.strippingHTTPOrHTTPSScheme - let regarding = NSLocalizedString("Link", comment: "Link") - let externalLinkString = "\(regarding): \(displayLink)" - d["external_link"] = externalLinkString + if let externalLink = article.externalLink, externalLink != article.preferredLink { + d["external_link_label"] = NSLocalizedString("Link:", comment: "Link") + d["external_link_stripped"] = externalLink.strippingHTTPOrHTTPSScheme + d["external_link"] = externalLink } else { + d["external_link_label"] = "" + d["external_link_stripped"] = "" d["external_link"] = "" } @@ -172,10 +229,10 @@ private extension ArticleRenderer { components.scheme = Self.imageIconScheme components.path = article.articleID if let imageIconURLString = components.string { - d["avatars"] = "" + d["avatar_src"] = imageIconURLString } else { - d["avatars"] = "" + d["avatar_src"] = "" } if self.title.isEmpty { @@ -184,33 +241,22 @@ private extension ArticleRenderer { d["dateline_style"] = "articleDateline" } - var feedLink = "" - if let feedTitle = article.webFeed?.nameForDisplay { - feedLink = feedTitle - if let feedURL = article.webFeed?.homePageURL { - feedLink = feedLink.htmlByAddingLink(feedURL, className: "feedLink") - } - } - d["feedlink"] = feedLink - - let datePublished = article.logicalDatePublished - let longDate = dateString(datePublished, .long, .medium) - let mediumDate = dateString(datePublished, .medium, .short) - let shortDate = dateString(datePublished, .short, .short) - - if let permalink = article.url { - d["date_long"] = longDate.htmlByAddingLink(permalink) - d["date_medium"] = mediumDate.htmlByAddingLink(permalink) - d["date_short"] = shortDate.htmlByAddingLink(permalink) - } - else { - d["date_long"] = longDate - d["date_medium"] = mediumDate - d["date_short"] = shortDate - } + d["feed_link_title"] = article.webFeed?.nameForDisplay ?? "" + d["feed_link"] = article.webFeed?.homePageURL ?? "" d["byline"] = byline() + let datePublished = article.logicalDatePublished + d["datetime_long"] = Self.longDateTimeFormatter.string(from: datePublished) + d["datetime_medium"] = Self.mediumDateTimeFormatter.string(from: datePublished) + d["datetime_short"] = Self.shortDateTimeFormatter.string(from: datePublished) + d["date_long"] = Self.longDateFormatter.string(from: datePublished) + d["date_medium"] = Self.mediumDateFormatter.string(from: datePublished) + d["date_short"] = Self.shortDateFormatter.string(from: datePublished) + d["time_long"] = Self.longTimeFormatter.string(from: datePublished) + d["time_medium"] = Self.mediumTimeFormatter.string(from: datePublished) + d["time_short"] = Self.shortTimeFormatter.string(from: datePublished) + return d } @@ -265,13 +311,6 @@ private extension ArticleRenderer { return byline } - func dateString(_ date: Date, _ dateStyle: DateFormatter.Style, _ timeStyle: DateFormatter.Style) -> String { - let dateFormatter = DateFormatter() - dateFormatter.dateStyle = dateStyle - dateFormatter.timeStyle = timeStyle - return dateFormatter.string(from: date) - } - #if os(iOS) func styleSubstitutions() -> [String: String] { var d = [String: String]() @@ -292,7 +331,7 @@ private extension ArticleRenderer { private extension Article { var baseURL: URL? { - var s = url + var s = link if s == nil { s = webFeed?.homePageURL } diff --git a/Shared/Article Rendering/core.css b/Shared/Article Rendering/core.css new file mode 100644 index 000000000..64d5ed46f --- /dev/null +++ b/Shared/Article Rendering/core.css @@ -0,0 +1,137 @@ +/* This is the activity indicator (currently iOS only) that is used while image zooming */ + +.activityIndicatorWrap { + position: relative; +} + +.activityIndicator { + z-index: 1; + width: 64px; + height: 64px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} + +/* see removeWpSmiley; this rule is kept in case a wp-smiley is encountered without alt text */ + +.wp-smiley { + height: 1em; + max-height: 1em; + } + +/* Hide the external link at the bottom of Daring Fireball posts */ + +.x-netnewswire-hide { + display: none; +} + +/*Block ads and junk*/ + +iframe[src*="feedads"], +iframe[src*="doubleclick"], +iframe[src*="plusone.google"] { + display: none !important; +} + +a[href*=".ads."], +a[href*="feedads"], +a[href*="doubleclick"], +a[href*="//ads."], +a[href*="api.tweetmeme"], +a[href*="delicious.com/post?"], +a[href*="digg.com/submit?"], +a[href*="google.com/bookmarks/mark?"], +a[href*="posterous.com/share?"], +a[href*="tumblr.com/share?"], +a[href*="linkedin.com/shareArticle?"], +a[href*="facebook.com/share.php?"], +a[href*="http://twitter.com/home?"], +a[href*="addtoany.com/share_save"] { + display: none !important; +} + +img[src*=".ads."], +img[src*="//ads."], +img[src*="doubleclick"], +img[src*="feedads"], +img[src*="feedburner"], +img[src*="feedblitz"], +img[src*="share-buttons"] { + display: none !important; +} + +/* Newsfoot specific styles. Structural styles come first, theme styles second */ + +.newsfoot-footnote-container { + position: relative; + display: inline-block; + z-index: 9999; +} + +.newsfoot-footnote-popover { + position: absolute; + display: block; + padding: 0em 1em; + margin: 1em; + top: 0.75em; + max-width: none; + border-radius: 0.3em; + box-sizing: border-box; +} + +.newsfoot-footnote-popover { + left: calc(-1 * (50vw - 1em)); + right: calc(-1 * (50vw - 1em)); +} + +.newsfoot-footnote-popover-arrow { + content: ''; + display: block; + width: 1em; + position: absolute; + top: -0.5em; + left: calc(50% - 0.5em); + height: 1em !important; + transform: rotate(45deg); + z-index:0; +} + +.newsfoot-footnote-popover-inner { + border-radius: calc(0.3em - 1px); + padding: 1em; + position: relative; + z-index: 1; +} + +.newsfoot-footnote-popover-inner :first-child { + margin-top: 0; +} +.newsfoot-footnote-popover-inner :last-child { + margin-bottom: 0; +} + +.newsfoot-footnote-popover .reversefootnote, +.newsfoot-footnote-popover .footnoteBackLink, +.newsfoot-footnote-popover .footnote-return, +.newsfoot-footnote-popover a[href*='#fn'] { + display: none; +} + +sup[id^='fn'] { + vertical-align: baseline; +} + +a.footnote { + display: inline-block; + text-decoration: none; + padding: 0.05em 0.75em; + border-radius: 1em; + min-width: 1em; + text-align: center; + font-size: 0.8em; + line-height: 1em; + position:relative; + top: -0.1em; +} diff --git a/Shared/Article Rendering/main.js b/Shared/Article Rendering/main.js index 084eb0a5f..13f90b638 100644 --- a/Shared/Article Rendering/main.js +++ b/Shared/Article Rendering/main.js @@ -113,7 +113,7 @@ function flattenPreElements() { function reloadArticleImage(imageSrc) { var image = document.getElementById("nnwImageIcon"); - image.src = imageSrc; + image.src = imageSrc + "?" + new Date().getTime(); } function stopMediaPlayback() { @@ -132,12 +132,6 @@ function stopMediaPlayback() { }); } -function updateTextSize(cssClass) { - var bodyElement = document.getElementById("bodyContainer"); - bodyElement.classList.remove("smallText", "mediumText", "largeText", "xLargeText", "xxLargeText"); - bodyElement.classList.add(cssClass); -} - function error() { document.body.innerHTML = "error"; } diff --git a/Shared/Article Rendering/shared.css b/Shared/Article Rendering/stylesheet.css similarity index 70% rename from Shared/Article Rendering/shared.css rename to Shared/Article Rendering/stylesheet.css index 713924ce3..1b50b7bea 100644 --- a/Shared/Article Rendering/shared.css +++ b/Shared/Article Rendering/stylesheet.css @@ -1,3 +1,5 @@ +/* Shared iOS and macOS CSS rules. Platform specific rules are at the bottom of this file. */ + body { margin-left: auto; margin-right: auto; @@ -9,12 +11,15 @@ body { a { text-decoration: none; } + a:hover { text-decoration: underline; } + .feedlink { font-weight: bold; } + .headerTable { width: 100%; height: 68px; @@ -52,18 +57,15 @@ a:hover { } } -body { - color: var(--body-color); - background-color: var(--body-background-color) !important; -} - body .headerTable { border-bottom: 1px solid var(--header-table-border-color); color: var(--header-color); } + body .header { color: var(--header-color); } + body .header a:link, .header a:visited { color: var(--header-color); } @@ -87,9 +89,11 @@ body > .systemMessage { .feedIcon { border-radius: 4px; } + .rightAlign { text-align: end; } + .leftAlign { text-align: start; } @@ -165,6 +169,7 @@ pre code { .nnw-overflow { overflow-x: auto; } + /* Instead of the last-child bits, border-collapse: collapse could have been used. However, then the inter-cell borders @@ -176,10 +181,12 @@ pre code { border: 1px solid var(--secondary-accent-color); font-size: inherit; } + .nnw-overflow table table { margin-bottom: 0; border: none; } + .nnw-overflow td, .nnw-overflow th { -webkit-hyphens: none; word-break: normal; @@ -196,10 +203,12 @@ pre code { .nnw-overflow :matches(thead, tbody, tfoot):last-child > tr:last-child :matches(td, th) { border-bottom: none; } + .nnw-overflow td pre { border: none; padding: 0; } + .nnw-overflow table[border="0"] { border-width: 0; } @@ -272,19 +281,6 @@ blockquote { border-top: 1px solid var(--header-table-border-color); } -/* Hide the external link at the bottom of Daring Fireball posts */ - -.x-netnewswire-hide { - display: none; -} - -/* see removeWpSmiley; this rule is kept in case a wp-smiley is encountered without alt text */ - -.wp-smiley { - height: 1em; - max-height: 1em; -} - /* Twitter */ .twitterAvatar { @@ -309,126 +305,23 @@ blockquote { font-size: 66%; } -/*Block ads and junk*/ - -iframe[src*="feedads"], -iframe[src*="doubleclick"], -iframe[src*="plusone.google"] { - display: none !important; -} - -a[href*=".ads."], -a[href*="feedads"], -a[href*="doubleclick"], -a[href*="//ads."], -a[href*="api.tweetmeme"], -a[href*="delicious.com/post?"], -a[href*="digg.com/submit?"], -a[href*="google.com/bookmarks/mark?"], -a[href*="posterous.com/share?"], -a[href*="tumblr.com/share?"], -a[href*="linkedin.com/shareArticle?"], -a[href*="facebook.com/share.php?"], -a[href*="http://twitter.com/home?"], -a[href*="addtoany.com/share_save"] { - display: none !important; -} - -img[src*=".ads."], -img[src*="//ads."], -img[src*="doubleclick"], -img[src*="feedads"], -img[src*="feedburner"], -img[src*="feedblitz"], -img[src*="share-buttons"] { - display: none !important; -} - -/* Newsfoot specific styles. Structural styles come first, theme styles second */ -.newsfoot-footnote-container { - position: relative; - display: inline-block; - z-index: 9999; -} - -.newsfoot-footnote-popover { - position: absolute; - display: block; - padding: 0em 1em; - margin: 1em; - top: 0.75em; - max-width: none; - border-radius: 0.3em; - box-sizing: border-box; -} - -.newsfoot-footnote-popover { - left: calc(-1 * (50vw - 1em)); - right: calc(-1 * (50vw - 1em)); -} -.newsfoot-footnote-popover-arrow { - content: ''; - display: block; - width: 1em; - position: absolute; - top: -0.5em; - left: calc(50% - 0.5em); - height: 1em !important; - transform: rotate(45deg); - z-index:0; -} -.newsfoot-footnote-popover-inner { - border-radius: calc(0.3em - 1px); - padding: 1em; - position: relative; - z-index: 1; -} - -.newsfoot-footnote-popover-inner :first-child { - margin-top: 0; -} -.newsfoot-footnote-popover-inner :last-child { - margin-bottom: 0; -} - -.newsfoot-footnote-popover .reversefootnote, -.newsfoot-footnote-popover .footnoteBackLink, -.newsfoot-footnote-popover .footnote-return, -.newsfoot-footnote-popover a[href*='#fn'] { - display: none; -} - -sup[id^='fn'] { - vertical-align: baseline; -} - -a.footnote { - display: inline-block; - text-decoration: none; - padding: 0.05em 0.75em; - border-radius: 1em; - min-width: 1em; - text-align: center; - font-size: 0.8em; - line-height: 1em; - position:relative; - top: -0.1em; -} - -/* light / default */ +/* Newsfoot theme for light mode (default) */ .newsfoot-footnote-popover { background: #ccc; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5), 0 3px 6px rgba(0, 0, 0, 0.25); color: black; padding: 1px; } + .newsfoot-footnote-popover-arrow { background: #fafafa; border: 1px solid #ccc; } + .newsfoot-footnote-popover-inner { background: #fafafa; } + body a.footnote, body a.footnote:visited, .newsfoot-footnote-popover + a.footnote:hover { @@ -436,25 +329,29 @@ body a.footnote:visited, color: white; transition: background-color 200ms ease-out; } + a.footnote:hover, .newsfoot-footnote-popover + a.footnote { background: #666; transition: background-color 200ms ease-out; } -/* dark */ +/* Newsfoot theme for dark mode */ @media screen and (prefers-color-scheme: dark) { .newsfoot-footnote-popover { background: #444; color: rgb(224, 224, 224); } + .newsfoot-footnote-popover-arrow { background: #242424; border: 1px solid #444; } + .newsfoot-footnote-popover-inner { background: #242424; } + body a.footnote, body a.footnote:visited, .newsfoot-footnote-popover + a.footnote:hover { @@ -462,9 +359,128 @@ a.footnote:hover, color: white; transition: background-color 200ms ease-out; } + a.footnote:hover, .newsfoot-footnote-popover + a.footnote { background: #666; transition: background-color 200ms ease-out; } + +} + +/* iOS Specific */ +@supports (-webkit-touch-callout: none) { + + body { + margin-top: 3px; + margin-bottom: 20px; + padding-left: 20px; + padding-right: 20px; + + word-break: break-word; + -webkit-hyphens: auto; + -webkit-text-size-adjust: none; + } + + :root { + color-scheme: light dark; + font: -apple-system-body; + /* The font-size is replaced at runtime by the dynamic type size */ + font-size: [[font-size]]px; + --primary-accent-color: #086AEE; + --secondary-accent-color: #086AEE; + --block-quote-border-color: rgba(8, 106, 238, 0.75); + } + + @media(prefers-color-scheme: dark) { + :root { + --primary-accent-color: #2D80F1; + --secondary-accent-color: #5E9EF4; + --block-quote-border-color: rgba(94, 158, 244, 0.75); + --header-table-border-color: rgba(255, 255, 255, 0.2); + } + } + + body a, body a:visited, body a * { + color: var(--secondary-accent-color); + } + + body .header { + font: -apple-system-body; + font-size: [[font-size]]px; + } + + body .header a:link, body .header a:visited { + color: var(--primary-accent-color); + } + + pre { + border: 1px solid var(--secondary-accent-color); + padding: 5px; + } + + .nnw-overflow table { + border: 1px solid var(--secondary-accent-color); + } + +} + +/* macOS Specific */ +@supports not (-webkit-touch-callout: none) { + + body { + margin-top: 20px; + margin-bottom: 64px; + padding-left: 48px; + padding-right: 48px; + font-family: -apple-system; + } + + .smallText { + font-size: 14px; + } + + .mediumText { + font-size: 16px; + } + + .largeText { + font-size: 18px; + } + + .xlargeText { + font-size: 20px; + } + + .xxlargeText { + font-size: 22px; + } + + :root { + color-scheme: light dark; + --accent-color: rgba(8, 106, 238, 1); + --block-quote-border-color: rgba(8, 106, 238, .50); + } + + @media(prefers-color-scheme: dark) { + :root { + --accent-color: rgba(94, 158, 244, 1); + --block-quote-border-color: rgba(94, 158, 244, .50); + --header-table-border-color: rgba(255, 255, 255, 0.1); + } + } + + body a, body a:visited, body a * { + color: var(--accent-color); + } + + pre { + border: 1px solid var(--accent-color); + padding: 10px; + } + + .nnw-overflow table { + border: 1px solid var(--accent-color); + } + } diff --git a/Shared/Article Rendering/template.html b/Shared/Article Rendering/template.html index 295b28fd4..a8bbed925 100644 --- a/Shared/Article Rendering/template.html +++ b/Shared/Article Rendering/template.html @@ -1,15 +1,47 @@ + +
- - [[avatars]] + +
[[feedlink]]
[[byline]]
[[feed_link_title]]
[[byline]]
diff --git a/Shared/ArticleStyles/ArticleStyle.swift b/Shared/ArticleStyles/ArticleStyle.swift deleted file mode 100644 index 97b9a3030..000000000 --- a/Shared/ArticleStyles/ArticleStyle.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// ArticleStyle.swift -// NetNewsWire -// -// Created by Brent Simmons on 9/26/15. -// Copyright © 2015 Ranchero Software, LLC. All rights reserved. -// - -import Foundation - -struct ArticleStyle: Equatable { - - static let defaultStyle = ArticleStyle() - let path: String? - let template: String? - let css: String? - let emptyCSS: String? - let info: NSDictionary? - - init() { - - //Default style - - self.path = nil; - self.emptyCSS = nil - - self.info = ["CreatorHomePage": "https://netnewswire.com/", "CreatorName": "Ranchero Software", "Version": "1.0"] - - let sharedCSSPath = Bundle.main.path(forResource: "shared", ofType: "css")! - let platformCSSPath = Bundle.main.path(forResource: "styleSheet", ofType: "css")! - - if let sharedCSS = stringAtPath(sharedCSSPath), let platformCSS = stringAtPath(platformCSSPath) { - css = sharedCSS + "\n" + platformCSS - } else { - css = nil - } - - let templatePath = Bundle.main.path(forResource: "template", ofType: "html")! - template = stringAtPath(templatePath) - } - - init(path: String) { - - self.path = path - - let isFolder = FileManager.default.isFolder(atPath: path) - - if isFolder { - - let infoPath = (path as NSString).appendingPathComponent("Info.plist") - self.info = NSDictionary(contentsOfFile: infoPath) - - let cssPath = (path as NSString).appendingPathComponent("stylesheet.css") - self.css = stringAtPath(cssPath) - - let emptyCSSPath = (path as NSString).appendingPathComponent("stylesheet_empty.css") - self.emptyCSS = stringAtPath(emptyCSSPath) - - let templatePath = (path as NSString).appendingPathComponent("template.html") - self.template = stringAtPath(templatePath) - } - - else { - - self.css = stringAtPath(path) - self.template = nil - self.emptyCSS = nil - self.info = nil - } - } -} - -private func stringAtPath(_ f: String) -> String? { - - if !FileManager.default.fileExists(atPath: f) { - return nil - } - - if let s = try? NSString(contentsOfFile: f, usedEncoding: nil) as String { - return s - } - return nil -} diff --git a/Shared/ArticleStyles/ArticleStylesManager.swift b/Shared/ArticleStyles/ArticleStylesManager.swift deleted file mode 100644 index ddc23b3f8..000000000 --- a/Shared/ArticleStyles/ArticleStylesManager.swift +++ /dev/null @@ -1,189 +0,0 @@ -// -// ArticleStylesManager.sqift -// NetNewsWire -// -// Created by Brent Simmons on 9/26/15. -// Copyright © 2015 Ranchero Software, LLC. All rights reserved. -// - -#if os(macOS) -import AppKit -#else -import UIKit -#endif - -import RSCore - -let ArticleStyleNamesDidChangeNotification = "ArticleStyleNamesDidChangeNotification" -let CurrentArticleStyleDidChangeNotification = "CurrentArticleStyleDidChangeNotification" - -private let styleKey = "style" -private let defaultStyleName = "Default" -private let stylesFolderName = "Styles" -private let stylesInResourcesFolderName = "Styles" -private let styleSuffix = ".netnewswirestyle" -private let nnwStyleSuffix = ".nnwstyle" -private let cssStyleSuffix = ".css" -private let styleSuffixes = [styleSuffix, nnwStyleSuffix, cssStyleSuffix]; - -final class ArticleStylesManager { - - static let shared = ArticleStylesManager() - private let folderPath = Platform.dataSubfolder(forApplication: nil, folderName: stylesFolderName)! - - var currentStyleName: String { - get { - return UserDefaults.standard.string(forKey: styleKey)! - } - set { - if newValue != currentStyleName { - UserDefaults.standard.set(newValue, forKey: styleKey) - } - } - } - - var currentStyle: ArticleStyle { - didSet { - NotificationCenter.default.post(name: Notification.Name(rawValue: CurrentArticleStyleDidChangeNotification), object: self) - } - } - - var styleNames = [defaultStyleName] { - didSet { - NotificationCenter.default.post(name: Notification.Name(rawValue: ArticleStyleNamesDidChangeNotification), object: self) - } - } - - init() { - - UserDefaults.standard.register(defaults: [styleKey: defaultStyleName]) -// -// let defaultStylesFolder = (Bundle.main.resourcePath! as NSString).appendingPathComponent(stylesInResourcesFolderName) -// do { -// try FileManager.default.rs_copyFiles(inFolder: defaultStylesFolder, destination: folderPath) -// } -// catch { -// print(error) -// } - - currentStyle = ArticleStyle.defaultStyle - - updateStyleNames() - updateCurrentStyle() - - #if os(macOS) - NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: NSApplication.didBecomeActiveNotification, object: nil) - #else - NotificationCenter.default.addObserver(self, selector: #selector(applicationDidBecomeActive(_:)), name: UIApplication.didBecomeActiveNotification, object: nil) - #endif - } - - // MARK: Notifications - - @objc dynamic func applicationDidBecomeActive(_ note: Notification) { - - updateStyleNames() - updateCurrentStyle() - } - - // MARK : Internal - - private func updateStyleNames() { - - let updatedStyleNames = allStylePaths(folderPath).map { styleNameForPath($0) } - - if updatedStyleNames != styleNames { - styleNames = updatedStyleNames - } - } - - private func articleStyleWithStyleName(_ styleName: String) -> ArticleStyle? { - - if styleName == defaultStyleName { - return ArticleStyle.defaultStyle - } - - guard let path = pathForStyleName(styleName, folder: folderPath) else { - return nil - } - - return ArticleStyle(path: path) - } - - private func defaultArticleStyle() -> ArticleStyle { - - return articleStyleWithStyleName(defaultStyleName)! - } - - private func updateCurrentStyle() { - - var styleName = currentStyleName - if !styleNames.contains(styleName) { - styleName = defaultStyleName - currentStyleName = defaultStyleName - } - - var articleStyle = articleStyleWithStyleName(styleName) - if articleStyle == nil { - articleStyle = defaultArticleStyle() - currentStyleName = defaultStyleName - } - - if let articleStyle = articleStyle, articleStyle != currentStyle { - currentStyle = articleStyle - } - } -} - - -private func allStylePaths(_ folder: String) -> [String] { - - let filepaths = FileManager.default.filePaths(inFolder: folder) - return filepaths?.filter { fileAtPathIsStyle($0) } ?? [] -} - -private func fileAtPathIsStyle(_ f: String) -> Bool { - - if !f.hasSuffix(styleSuffix) && !f.hasSuffix(nnwStyleSuffix) && !f.hasSuffix(cssStyleSuffix) { - return false - } - - if (f as NSString).lastPathComponent.hasPrefix(".") { - return false - } - - return true -} - -private func filenameWithStyleSuffixRemoved(_ filename: String) -> String { - - for oneSuffix in styleSuffixes { - if filename.hasSuffix(oneSuffix) { - return filename.stripping(suffix: oneSuffix) - } - } - - return filename -} - -private func styleNameForPath(_ f: String) -> String { - - let filename = (f as NSString).lastPathComponent - return filenameWithStyleSuffixRemoved(filename) -} - -private func pathIsPathForStyleName(_ styleName: String, path: String) -> Bool { - - let filename = (path as NSString).lastPathComponent - return filenameWithStyleSuffixRemoved(filename) == styleName -} - -private func pathForStyleName(_ styleName: String, folder: String) -> String? { - for onePath in allStylePaths(folder) { - if pathIsPathForStyleName(styleName, path: onePath) { - return onePath - } - } - - return nil -} diff --git a/Shared/ArticleStyles/ArticleTheme+Notifications.swift b/Shared/ArticleStyles/ArticleTheme+Notifications.swift new file mode 100644 index 000000000..4c2391ff4 --- /dev/null +++ b/Shared/ArticleStyles/ArticleTheme+Notifications.swift @@ -0,0 +1,15 @@ +// +// ArticleTheme+Notifications.swift +// ArticleTheme+Notifications +// +// Created by Stuart Breckenridge on 20/09/2021. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import Foundation + +extension Notification.Name { + static let didBeginDownloadingTheme = Notification.Name("didBeginDownloadingTheme") + static let didEndDownloadingTheme = Notification.Name("didEndDownloadingTheme") + static let didFailToImportThemeWithError = Notification.Name("didFailToImportThemeWithError") +} diff --git a/Shared/ArticleStyles/ArticleTheme.swift b/Shared/ArticleStyles/ArticleTheme.swift new file mode 100644 index 000000000..e40bde46f --- /dev/null +++ b/Shared/ArticleStyles/ArticleTheme.swift @@ -0,0 +1,98 @@ +// +// ArticleTheme.swift +// NetNewsWire +// +// Created by Brent Simmons on 9/26/15. +// Copyright © 2015 Ranchero Software, LLC. All rights reserved. +// + +import Foundation + +struct ArticleTheme: Equatable { + + static let defaultTheme = ArticleTheme() + static let nnwThemeSuffix = ".nnwtheme" + + private static let defaultThemeName = NSLocalizedString("Default", comment: "Default") + private static let unknownValue = NSLocalizedString("Unknown", comment: "Unknown Value") + + let path: String? + let template: String? + let css: String? + + var name: String { + guard let path = path else { return Self.defaultThemeName } + return Self.themeNameForPath(path) + } + + var creatorHomePage: String { + return info?.creatorHomePage ?? Self.unknownValue + } + + var creatorName: String { + return info?.creatorName ?? Self.unknownValue + } + + var version: String { + return String(describing: info?.version ?? 0) + } + + private let info: ArticleThemePlist? + + init() { + self.path = nil; + self.info = ArticleThemePlist(name: "Article Theme", themeIdentifier: "com.ranchero.netnewswire.theme.article", creatorHomePage: "https://netnewswire.com/", creatorName: "Ranchero Software", version: 1) + + let corePath = Bundle.main.path(forResource: "core", ofType: "css")! + let stylesheetPath = Bundle.main.path(forResource: "stylesheet", ofType: "css")! + css = Self.stringAtPath(corePath)! + "\n" + Self.stringAtPath(stylesheetPath)! + + let templatePath = Bundle.main.path(forResource: "template", ofType: "html")! + template = Self.stringAtPath(templatePath)! + } + + init(path: String) throws { + self.path = path + + let infoPath = (path as NSString).appendingPathComponent("Info.plist") + let data = try Data(contentsOf: URL(fileURLWithPath: infoPath)) + self.info = try PropertyListDecoder().decode(ArticleThemePlist.self, from: data) + + let corePath = Bundle.main.path(forResource: "core", ofType: "css")! + let stylesheetPath = (path as NSString).appendingPathComponent("stylesheet.css") + if let stylesheetCSS = Self.stringAtPath(stylesheetPath) { + self.css = Self.stringAtPath(corePath)! + "\n" + stylesheetCSS + } else { + self.css = nil + } + + let templatePath = (path as NSString).appendingPathComponent("template.html") + self.template = Self.stringAtPath(templatePath) + } + + static func stringAtPath(_ f: String) -> String? { + if !FileManager.default.fileExists(atPath: f) { + return nil + } + + if let s = try? NSString(contentsOfFile: f, usedEncoding: nil) as String { + return s + } + return nil + } + + static func filenameWithThemeSuffixRemoved(_ filename: String) -> String { + return filename.stripping(suffix: Self.nnwThemeSuffix) + } + + static func themeNameForPath(_ f: String) -> String { + let filename = (f as NSString).lastPathComponent + return filenameWithThemeSuffixRemoved(filename) + } + + static func pathIsPathForThemeName(_ themeName: String, path: String) -> Bool { + let filename = (path as NSString).lastPathComponent + return filenameWithThemeSuffixRemoved(filename) == themeName + } + +} diff --git a/Shared/ArticleStyles/ArticleThemeDownloader.swift b/Shared/ArticleStyles/ArticleThemeDownloader.swift new file mode 100644 index 000000000..7c3e5534c --- /dev/null +++ b/Shared/ArticleStyles/ArticleThemeDownloader.swift @@ -0,0 +1,108 @@ +// +// ArticleThemeDownloader.swift +// ArticleThemeDownloader +// +// Created by Stuart Breckenridge on 20/09/2021. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import Foundation +import Zip + +public class ArticleThemeDownloader { + + public enum ArticleThemeDownloaderError: LocalizedError { + case noThemeFile + + public var errorDescription: String? { + switch self { + case .noThemeFile: + return "There is no NetNewsWire theme available." + } + } + } + + public static let shared = ArticleThemeDownloader() + private init() {} + + public func handleFile(at location: URL) throws { + createDownloadDirectoryIfRequired() + let movedFileLocation = try moveTheme(from: location) + let unzippedFileLocation = try unzipFile(at: movedFileLocation) + NotificationCenter.default.post(name: .didEndDownloadingTheme, object: nil, userInfo: ["url" : unzippedFileLocation]) + } + + + /// Creates `Application Support/NetNewsWire/Downloads` if needed. + private func createDownloadDirectoryIfRequired() { + try? FileManager.default.createDirectory(at: downloadDirectory(), withIntermediateDirectories: true, attributes: nil) + } + + /// Moves the downloaded `.tmp` file to the `downloadDirectory` and renames it a `.zip` + /// - Parameter location: The temporary file location. + /// - Returns: Destination `URL`. + private func moveTheme(from location: URL) throws -> URL { + var tmpFileName = location.lastPathComponent + tmpFileName = tmpFileName.replacingOccurrences(of: ".tmp", with: ".zip") + let fileUrl = downloadDirectory().appendingPathComponent("\(tmpFileName)") + try FileManager.default.moveItem(at: location, to: fileUrl) + return fileUrl + } + + /// Unzips the zip file + /// - Parameter location: Location of the zip archive. + /// - Returns: Enclosed `.nnwtheme` file. + private func unzipFile(at location: URL) throws -> URL { + do { + let unzipDirectory = URL(fileURLWithPath: location.path.replacingOccurrences(of: ".zip", with: "")) + try Zip.unzipFile(location, destination: unzipDirectory, overwrite: true, password: nil, progress: nil, fileOutputHandler: nil) // Unzips to folder in Application Support/NetNewsWire/Downloads + try FileManager.default.removeItem(at: location) // Delete zip in Cache + let themeFilePath = findThemeFile(in: unzipDirectory.path) + if themeFilePath == nil { + throw ArticleThemeDownloaderError.noThemeFile + } + return URL(fileURLWithPath: unzipDirectory.appendingPathComponent(themeFilePath!).path) + } catch { + try? FileManager.default.removeItem(at: location) + throw error + } + } + + + /// Performs a deep search of the unzipped direcotry to find the theme file. + /// - Parameter searchPath: directory to search + /// - Returns: optional `String` + private func findThemeFile(in searchPath: String) -> String? { + if let directoryContents = FileManager.default.enumerator(atPath: searchPath) { + while let file = directoryContents.nextObject() as? String { + if file.hasSuffix(".nnwtheme") { + return file + } + } + } + + return nil + } + + /// The download directory used by the theme downloader: `Application Suppport/NetNewsWire/Downloads` + /// - Returns: `URL` + private func downloadDirectory() -> URL { + FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("NetNewsWire/Downloads", isDirectory: true) + } + + /// Removes downloaded themes, where themes == folders, from `Application Suppport/NetNewsWire/Downloads`. + public func cleanUp() { + guard let filenames = try? FileManager.default.contentsOfDirectory(atPath: downloadDirectory().path) else { + return + } + for path in filenames { + do { + if FileManager.default.isFolder(atPath: downloadDirectory().appendingPathComponent(path).path) { + try FileManager.default.removeItem(atPath: downloadDirectory().appendingPathComponent(path).path) + } + } catch { + print(error) + } + } + } +} diff --git a/Shared/ArticleStyles/ArticleThemePlist.swift b/Shared/ArticleStyles/ArticleThemePlist.swift new file mode 100644 index 000000000..f324cdbde --- /dev/null +++ b/Shared/ArticleStyles/ArticleThemePlist.swift @@ -0,0 +1,25 @@ +// +// ArticleThemePlist.swift +// NetNewsWire +// +// Created by Stuart Breckenridge on 19/09/2021. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import Foundation + +public struct ArticleThemePlist: Codable, Equatable { + public var name: String + public var themeIdentifier: String + public var creatorHomePage: String + public var creatorName: String + public var version: Int + + enum CodingKeys: String, CodingKey { + case name = "Name" + case themeIdentifier = "ThemeIdentifier" + case creatorHomePage = "CreatorHomePage" + case creatorName = "CreatorName" + case version = "Version" + } +} diff --git a/Shared/ArticleStyles/ArticleThemesManager.swift b/Shared/ArticleStyles/ArticleThemesManager.swift new file mode 100644 index 000000000..e2205240a --- /dev/null +++ b/Shared/ArticleStyles/ArticleThemesManager.swift @@ -0,0 +1,191 @@ +// +// ArticleThemesManager.sqift +// NetNewsWire +// +// Created by Brent Simmons on 9/26/15. +// Copyright © 2015 Ranchero Software, LLC. All rights reserved. +// + +import Foundation +import RSCore + +public extension Notification.Name { + static let ArticleThemeNamesDidChangeNotification = Notification.Name("ArticleThemeNamesDidChangeNotification") + static let CurrentArticleThemeDidChangeNotification = Notification.Name("CurrentArticleThemeDidChangeNotification") +} + +final class ArticleThemesManager: NSObject, NSFilePresenter { + + static var shared: ArticleThemesManager! + public let folderPath: String + + lazy var presentedItemOperationQueue = OperationQueue.main + var presentedItemURL: URL? { + return URL(fileURLWithPath: folderPath) + } + + var currentThemeName: String { + get { + return AppDefaults.shared.currentThemeName ?? AppDefaults.defaultThemeName + } + set { + if newValue != currentThemeName { + AppDefaults.shared.currentThemeName = newValue + updateCurrentTheme() + } + } + } + + var currentTheme: ArticleTheme { + didSet { + NotificationCenter.default.post(name: .CurrentArticleThemeDidChangeNotification, object: self) + } + } + + var themeNames = [AppDefaults.defaultThemeName] { + didSet { + NotificationCenter.default.post(name: .ArticleThemeNamesDidChangeNotification, object: self) + } + } + + init(folderPath: String) { + self.folderPath = folderPath + self.currentTheme = ArticleTheme.defaultTheme + + super.init() + + do { + try FileManager.default.createDirectory(atPath: folderPath, withIntermediateDirectories: true, attributes: nil) + } catch { + assertionFailure("Could not create folder for Themes.") + abort() + } + + let themeFilenames = Bundle.main.paths(forResourcesOfType: ArticleTheme.nnwThemeSuffix, inDirectory: nil) + let installedThemes = readInstalledThemes() ?? [String: Date]() + for themeFilename in themeFilenames { + let themeName = ArticleTheme.themeNameForPath(themeFilename) + if !installedThemes.keys.contains(themeName) { + try? importTheme(filename: themeFilename) + } + } + + updateThemeNames() + updateCurrentTheme() + + NSFileCoordinator.addFilePresenter(self) + } + + func presentedSubitemDidChange(at url: URL) { + updateThemeNames() + updateCurrentTheme() + } + + // MARK: API + + func themeExists(filename: String) -> Bool { + let filenameLastPathComponent = (filename as NSString).lastPathComponent + let toFilename = (folderPath as NSString).appendingPathComponent(filenameLastPathComponent) + return FileManager.default.fileExists(atPath: toFilename) + } + + func importTheme(filename: String) throws { + let filenameLastPathComponent = (filename as NSString).lastPathComponent + let toFilename = (folderPath as NSString).appendingPathComponent(filenameLastPathComponent) + + if FileManager.default.fileExists(atPath: toFilename) { + try FileManager.default.removeItem(atPath: toFilename) + } + + try FileManager.default.copyItem(atPath: filename, toPath: toFilename) + + let themeName = ArticleTheme.themeNameForPath(filename) + var installedThemes = readInstalledThemes() ?? [String: Date]() + installedThemes[themeName] = Date() + writeInstalledThemes(installedThemes) + } + + func deleteTheme(themeName: String) { + if let filename = pathForThemeName(themeName, folder: folderPath) { + try? FileManager.default.removeItem(atPath: filename) + } + } + +} + +// MARK : Private + +private extension ArticleThemesManager { + + func updateThemeNames() { + let updatedThemeNames = allThemePaths(folderPath).map { ArticleTheme.themeNameForPath($0) } + let sortedThemeNames = updatedThemeNames.sorted(by: { $0.compare($1, options: .caseInsensitive) == .orderedAscending }) + if sortedThemeNames != themeNames { + themeNames = sortedThemeNames + } + } + + func articleThemeWithThemeName(_ themeName: String) -> ArticleTheme? { + if themeName == AppDefaults.defaultThemeName { + return ArticleTheme.defaultTheme + } + + guard let path = pathForThemeName(themeName, folder: folderPath) else { + return nil + } + do { + return try ArticleTheme(path: path) + } catch { + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) + return nil + } + + } + + func defaultArticleTheme() -> ArticleTheme { + return articleThemeWithThemeName(AppDefaults.defaultThemeName)! + } + + func updateCurrentTheme() { + var themeName = currentThemeName + if !themeNames.contains(themeName) { + themeName = AppDefaults.defaultThemeName + currentThemeName = AppDefaults.defaultThemeName + } + + var articleTheme = articleThemeWithThemeName(themeName) + if articleTheme == nil { + articleTheme = defaultArticleTheme() + currentThemeName = AppDefaults.defaultThemeName + } + + if let articleTheme = articleTheme, articleTheme != currentTheme { + currentTheme = articleTheme + } + } + + func allThemePaths(_ folder: String) -> [String] { + let filepaths = FileManager.default.filePaths(inFolder: folder) + return filepaths?.filter { $0.hasSuffix(ArticleTheme.nnwThemeSuffix) } ?? [] + } + + func pathForThemeName(_ themeName: String, folder: String) -> String? { + for onePath in allThemePaths(folder) { + if ArticleTheme.pathIsPathForThemeName(themeName, path: onePath) { + return onePath + } + } + return nil + } + + func readInstalledThemes() -> [String: Date]? { + let filePath = (folderPath as NSString).appendingPathComponent("InstalledThemes.plist") + return NSDictionary(contentsOfFile: filePath) as? [String: Date] + } + + func writeInstalledThemes(_ dict: [String: Date]) { + let filePath = (folderPath as NSString).appendingPathComponent("InstalledThemes.plist") + (dict as NSDictionary).write(toFile: filePath, atomically: true) + } + +} diff --git a/Shared/ExtensionPoints/SendToMarsEditCommand.swift b/Shared/ExtensionPoints/SendToMarsEditCommand.swift index 12f1eda2c..c921d71c3 100644 --- a/Shared/ExtensionPoints/SendToMarsEditCommand.swift +++ b/Shared/ExtensionPoints/SendToMarsEditCommand.swift @@ -75,7 +75,7 @@ private extension SendToMarsEditCommand { let body = article.contentHTML ?? article.contentText ?? article.summary let authorName = article.authors?.first?.name - let sender = SendToBlogEditorApp(targetDescriptor: targetDescriptor, title: article.title, body: body, summary: article.summary, link: article.externalURL, permalink: article.url, subject: nil, creator: authorName, commentsURL: nil, guid: article.uniqueID, sourceName: article.webFeed?.nameForDisplay, sourceHomeURL: article.webFeed?.homePageURL, sourceFeedURL: article.webFeed?.url) + let sender = SendToBlogEditorApp(targetDescriptor: targetDescriptor, title: article.title, body: body, summary: article.summary, link: article.externalLink, permalink: article.link, subject: nil, creator: authorName, commentsURL: nil, guid: article.uniqueID, sourceName: article.webFeed?.nameForDisplay, sourceHomeURL: article.webFeed?.homePageURL, sourceFeedURL: article.webFeed?.url) let _ = sender.send() } diff --git a/Shared/Extensions/ArticleUtilities.swift b/Shared/Extensions/ArticleUtilities.swift index ace388e04..0b52c15d8 100644 --- a/Shared/Extensions/ArticleUtilities.swift +++ b/Shared/Extensions/ArticleUtilities.swift @@ -46,26 +46,48 @@ extension Article { return account?.existingWebFeed(withWebFeedID: webFeedID) } + var url: URL? { + return URL.reparingIfRequired(rawLink) + } + + var externalURL: URL? { + return URL.reparingIfRequired(rawExternalLink) + } + + var imageURL: URL? { + return URL.reparingIfRequired(rawImageLink) + } + + var link: String? { + // Prefer link from URL, if one can be created, as these are repaired if required. + // Provide the raw link if URL creation fails. + return url?.absoluteString ?? rawLink + } + + var externalLink: String? { + // Prefer link from externalURL, if one can be created, as these are repaired if required. + // Provide the raw link if URL creation fails. + return externalURL?.absoluteString ?? rawExternalLink + } + + var imageLink: String? { + // Prefer link from imageURL, if one can be created, as these are repaired if required. + // Provide the raw link if URL creation fails. + return imageURL?.absoluteString ?? rawImageLink + } + var preferredLink: String? { - if let url = url, !url.isEmpty { - return url + if let link = link, !link.isEmpty { + return link } - if let externalURL = externalURL, !externalURL.isEmpty { - return externalURL + if let externalLink = externalLink, !externalLink.isEmpty { + return externalLink } return nil } var preferredURL: URL? { - guard let link = preferredLink else { return nil } - // If required, we replace any space characters to handle malformed links that are otherwise percent - // encoded but contain spaces. For performance reasons, only try this if initial URL init fails. - if let url = URL(string: link) { - return url - } else if let url = URL(string: link.replacingOccurrences(of: " ", with: "%20")) { - return url - } - return nil + return url ?? externalURL } var body: String? { diff --git a/Shared/Extensions/URL-Extensions.swift b/Shared/Extensions/URL-Extensions.swift index 707877cca..f28ade9c1 100644 --- a/Shared/Extensions/URL-Extensions.swift +++ b/Shared/Extensions/URL-Extensions.swift @@ -42,4 +42,16 @@ extension URL { return value } + + static func reparingIfRequired(_ link: String?) -> URL? { + // If required, we replace any space characters to handle malformed links that are otherwise percent + // encoded but contain spaces. For performance reasons, only try this if initial URL init fails. + guard let link = link, !link.isEmpty else { return nil } + if let url = URL(string: link) { + return url + } else { + return URL(string: link.replacingOccurrences(of: " ", with: "%20")) + } + } + } diff --git a/Shared/Images/FeaturedImageDownloader.swift b/Shared/Images/FeaturedImageDownloader.swift index 618cf07de..2fdf13e7f 100644 --- a/Shared/Images/FeaturedImageDownloader.swift +++ b/Shared/Images/FeaturedImageDownloader.swift @@ -25,11 +25,11 @@ final class FeaturedImageDownloader { func image(for article: Article) -> RSImage? { - if let url = article.imageURL { - return image(forFeaturedImageURL: url) + if let imageLink = article.imageLink { + return image(forFeaturedImageURL: imageLink) } - if let articleURL = article.url { - return image(forArticleURL: articleURL) + if let link = article.link { + return image(forArticleURL: link) } return nil } diff --git a/Shared/Importers/DefaultFeeds.opml b/Shared/Importers/DefaultFeeds.opml index 90bfa495c..0011c80b4 100644 --- a/Shared/Importers/DefaultFeeds.opml +++ b/Shared/Importers/DefaultFeeds.opml @@ -4,10 +4,10 @@ Default Feeds - + - + diff --git a/Shared/Resources/Appanoose.nnwtheme/Info.plist b/Shared/Resources/Appanoose.nnwtheme/Info.plist new file mode 100644 index 000000000..baf355209 --- /dev/null +++ b/Shared/Resources/Appanoose.nnwtheme/Info.plist @@ -0,0 +1,16 @@ + + + + + Name + Starter + ThemeIdentifier + com.netnewswire.themes.starter + CreatorHomePage + http://netnewswire.com/ + CreatorName + Ranchero Software + Version + 1 + + diff --git a/Shared/Resources/Appanoose.nnwtheme/stylesheet.css b/Shared/Resources/Appanoose.nnwtheme/stylesheet.css new file mode 100644 index 000000000..9342479db --- /dev/null +++ b/Shared/Resources/Appanoose.nnwtheme/stylesheet.css @@ -0,0 +1,540 @@ +/* Shared iOS and macOS CSS rules. Platform specific rules are at the bottom of this file. */ + +body { + word-wrap: break-word; + max-width: 44em; +} + +article { + margin-top: 12px; +} + +a { + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.footerTable { + width: 100%; +} + +.systemMessage { + position: absolute; + top: 45%; + left: 50%; + transform: translateX(-55%) translateY(-50%); + -webkit-user-select: none; + cursor: default; +} + +:root { + --footer-table-border-color: rgba(0, 0, 0, 0.1); + --footer-color: rgba(0, 0, 0, 0.3); + --body-code-color: #666; + --system-message-color: #cbcbcb; + --attribution-color: dimgray; + --article-title-color: #333; + --article-date-color: rgba(0, 0, 0, 0.3); + --table-cell-border-color: lightgray; +} + +@media(prefers-color-scheme: dark) { + :root { + --footer-color: rgba(94, 158, 244, 1); + --body-code-color: #b2b2b2; + --system-message-color: #5f5f5f; + --attribution-color: #b2b2b2; + --article-title-color: #e0e0e0; + --article-date-color: rgba(255, 255, 255, 0.5); + --table-cell-border-color: dimgray; + } +} + +.nnwDate { + white-space: nowrap; + color: var(--body-code-color); +} + +.nnwTime { + white-space: nowrap; + font-size: 66%; + color: var(--body-code-color); +} + +.nnwDateTime { + line-height: 1.2; + color: var(--attribution-color); +} + +body .header { + padding: 6px; + border: 1px solid var(--header-footer-border-color); + border-radius: 4px; + display: flex; +} + +body .footer { + margin-top: 22px; + padding: 6px; + border: 1px solid var(--header-footer-border-color); + border-radius: 4px; + color: var(--body-code-color); +} + +body .footer a:link, .footer a:visited { + color: var(--body-code-color); +} + +body code, body pre { + color: var(--body-code-color); +} + +body > .systemMessage { + color: var(--system-message-color); +} + +.feedIcon { + border-radius: 4px; +} + +.attribution { + line-height: 1.2; + margin-left: 4px; + margin-right: 4px; + display: inline-block; + vertical-align: middle; + text-align: left; + flex: 20 1 auto; + color: var(--attribution-color); +} + +.feedLink { + color: var(--attribution-color); +} + +.byline { + color: var(--attribution-color); + font-size: 66%; +} + +body .byline a:link, .byline a:visited { + color: var(--attribution-color); +} + +.rightAlign { + text-align: end; +} + +.leftAlign { + text-align: start; +} + +.articleTitle { + margin-top: 16px; + font-size: 1.2em; + font-weight: bold; + color: var(--article-title-color); + text-align: center; +} + +.articleTitle a:link, .articleTitle a:visited { + color: var(--article-title-color); +} + +.articleDateline { + color: var(--article-date-color); +} + +.articleDateline a:link, .articleDateline a:visited { + color: var(--article-date-color); +} + +.articleDatelineTitle a:link, .articleDatelineTitle a:visited { + color: var(--article-title-color); +} + +.externalLink { + font-style: italic; + width: 100%; +} + +.singleLine { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +body .externalLink a:link, .externalLink a:visited { + color: var(--article-title-color); +} + +.articleBody { + margin-top: 10px; + line-height: 1.6em; +} + +h1 { + line-height: 1.15em; + font-weight: bold; + padding-bottom: 0; + margin-bottom: 5px; +} + +h3 { + font-weight: bold; + margin-bottom: 3px; +} + +pre { + max-width: 100%; + margin: 0; + overflow: auto; + overflow-y: hidden; + word-wrap: normal; + word-break: normal; +} + +pre { + line-height: 1.4286em; +} + +code, pre { + font-family: "SF Mono", Menlo, "Courier New", Courier, monospace; + font-size: 1em; + -webkit-hyphens: none; +} + +pre code { + letter-spacing: -.027em; + font-size: 0.9375em; +} + +.nnw-overflow { + overflow-x: auto; +} + +.avatar { + vertical-align: middle; + border-radius: 4px; + height: 2em; + width: 2em; + min-width: 2em; +} + +/* + Instead of the last-child bits, border-collapse: collapse + could have been used. However, then the inter-cell borders + overlap the table border, which looks bad. + */ +.nnw-overflow table { + margin-bottom: 1px; + border-spacing: 0; + border: 1px solid var(--secondary-accent-color); + font-size: inherit; +} + +.nnw-overflow table table { + margin-bottom: 0; + border: none; +} + +.nnw-overflow td, .nnw-overflow th { + -webkit-hyphens: none; + word-break: normal; + border: 1px solid var(--table-cell-border-color); + border-top: none; + border-left: none; + padding: 5px; +} + +.nnw-overflow tr :matches(td, th):last-child { + border-right: none; +} + +.nnw-overflow :matches(thead, tbody, tfoot):last-child > tr:last-child :matches(td, th) { + border-bottom: none; +} + +.nnw-overflow td pre { + border: none; + padding: 0; +} + +.nnw-overflow table[border="0"] { + border-width: 0; +} + +img, figure, video, div, object { + max-width: 100%; + height: auto !important; + margin: 0 auto; +} + +iframe { + max-width: 100%; + margin: 0 auto; +} + +iframe.nnw-constrained { + max-height: 50vw; +} + +figure { + margin-bottom: 1em; + margin-top: 1em; +} + +figcaption { + font-size: 14px; + line-height: 1.3em; +} + +sup { + vertical-align: top; + position: relative; + bottom: 0.2rem; +} + +sub { + vertical-align: bottom; + position: relative; + top: 0.2rem; +} + +hr { + border: 1.5px solid var(--table-cell-border-color); +} + +.iframeWrap { + position: relative; + display: block; + padding-top: 56.25%; +} + +.iframeWrap iframe { + position: absolute; + top: 0; + left: 0; + height: 100% !important; + width: 100% !important; +} + +blockquote { + margin-inline-start: 0; + margin-inline-end: 0; + padding-inline-start: 15px; + border-inline-start: 3px solid var(--block-quote-border-color); +} + +/* Feed Specific */ + +.feedbin--article-wrap { + border-top: 1px solid var(--footer-table-border-color); +} + +/* Twitter */ + +.twitterAvatar { + vertical-align: middle; + border-radius: 4px; + height: 1.7em; + width: 1.7em; +} + +.twitterUsername { + line-height: 1.2; + margin-left: 4px; + display: inline-block; + vertical-align: middle; +} + +.twitterScreenName { + font-size: 66%; +} + +.twitterTimestamp { + font-size: 66%; +} + +/* Newsfoot theme for light mode (default) */ +.newsfoot-footnote-popover { + background: #ccc; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5), 0 3px 6px rgba(0, 0, 0, 0.25); + color: black; + padding: 1px; +} + +.newsfoot-footnote-popover-arrow { + background: #fafafa; + border: 1px solid #ccc; +} + +.newsfoot-footnote-popover-inner { + background: #fafafa; +} + +body a.footnote, +body a.footnote:visited, +.newsfoot-footnote-popover + a.footnote:hover { + background: #aaa; + color: white; + transition: background-color 200ms ease-out; +} + +a.footnote:hover, +.newsfoot-footnote-popover + a.footnote { + background: #666; + transition: background-color 200ms ease-out; +} + +/* Newsfoot theme for dark mode */ +@media screen and (prefers-color-scheme: dark) { + .newsfoot-footnote-popover { + background: #444; + color: rgb(224, 224, 224); + } + + .newsfoot-footnote-popover-arrow { + background: #242424; + border: 1px solid #444; + } + + .newsfoot-footnote-popover-inner { + background: #242424; + } + + body a.footnote, + body a.footnote:visited, + .newsfoot-footnote-popover + a.footnote:hover { + background: #aaa; + color: white; + transition: background-color 200ms ease-out; + } + + a.footnote:hover, + .newsfoot-footnote-popover + a.footnote { + background: #666; + transition: background-color 200ms ease-out; + } + +} + +/* iOS Specific */ +@supports (-webkit-touch-callout: none) { + + body { + margin-top: 3px; + margin-bottom: 20px; + margin-left: 20px; + margin-right: 20px; + + word-break: break-word; + -webkit-hyphens: auto; + -webkit-text-size-adjust: none; + } + + :root { + color-scheme: light dark; + font: -apple-system-body; + /* The font-size is replaced at runtime by the dynamic type size */ + font-size: [[font-size]]px; + --primary-accent-color: #086AEE; + --secondary-accent-color: #086AEE; + --block-quote-border-color: rgba(8, 106, 238, 0.50); + --header-footer-border-color: rgba(8, 106, 238, 0.75); + --secondary-accent-color: #086AEE; + } + + @media(prefers-color-scheme: dark) { + :root { + --primary-accent-color: #2D80F1; + --secondary-accent-color: #5E9EF4; + --block-quote-border-color: rgba(94, 158, 244, 0.50); + --header-footer-border-color: rgba(94, 158, 244, 0.75); + --footer-table-border-color: rgba(255, 255, 255, 0.2); + } + } + + body a, body a:visited, body a * { + color: var(--secondary-accent-color); + } + + .fontSize { + font: -apple-system-body; + font-size: [[font-size]]px; + } + + pre { + border: 1px solid var(--secondary-accent-color); + padding: 5px; + } + + .nnw-overflow table { + border: 1px solid var(--secondary-accent-color); + } + +} + +/* macOS Specific */ +@supports not (-webkit-touch-callout: none) { + + body { + margin-top: 20px; + margin-bottom: 20px; + margin-left: 48px; + margin-right: 48px; + font-family: -apple-system; + } + + .smallText { + font-size: 14px; + } + + .mediumText { + font-size: 16px; + } + + .largeText { + font-size: 18px; + } + + .xlargeText { + font-size: 20px; + } + + .xxlargeText { + font-size: 22px; + } + + :root { + color-scheme: light dark; + --accent-color: rgba(8, 106, 238, 1); + --block-quote-border-color: rgba(8, 106, 238, .50); + --header-footer-border-color: rgba(8, 106, 238, 0.75); + } + + @media(prefers-color-scheme: dark) { + :root { + --accent-color: rgba(94, 158, 244, 1); + --block-quote-border-color: rgba(94, 158, 244, .50); + --header-footer-border-color: rgba(94, 158, 244, 0.75); + } + } + + body a, body a:visited, body a * { + color: var(--accent-color); + } + + pre { + border: 1px solid var(--accent-color); + padding: 10px; + } + + .nnw-overflow table { + border: 1px solid var(--accent-color); + } + +} diff --git a/Shared/Resources/Appanoose.nnwtheme/template.html b/Shared/Resources/Appanoose.nnwtheme/template.html new file mode 100644 index 000000000..acc460be6 --- /dev/null +++ b/Shared/Resources/Appanoose.nnwtheme/template.html @@ -0,0 +1,56 @@ + + +
+
+
+ +
+
+ + +
+ +
+ + + +
+
[[body]]
+
+ + \ No newline at end of file diff --git a/Shared/Resources/Promenade.nnwtheme/Info.plist b/Shared/Resources/Promenade.nnwtheme/Info.plist new file mode 100644 index 000000000..24ec5ca47 --- /dev/null +++ b/Shared/Resources/Promenade.nnwtheme/Info.plist @@ -0,0 +1,16 @@ + + + + + Name + Promenade + ThemeIdentifier + com.mynameisstuart.themes.promenade + CreatorHomePage + https://mynameisstuart.com/ + CreatorName + Stuart Breckenridge + Version + 14 + + diff --git a/Shared/Resources/Promenade.nnwtheme/stylesheet.css b/Shared/Resources/Promenade.nnwtheme/stylesheet.css new file mode 100644 index 000000000..8b78fa56d --- /dev/null +++ b/Shared/Resources/Promenade.nnwtheme/stylesheet.css @@ -0,0 +1,679 @@ +body { + margin-left: auto; + margin-right: auto; + + word-wrap: break-word; + max-width: 44em; +} + +.feedHeader { + margin: auto; + text-align: center; +} + +.feedHeader img { + margin: auto; + display: block; + padding-top: 1em; + padding-bottom: 1em; +} + +.svg-inline--fa { + height: 1em; + width: 1em; + vertical-align: middle; +} + +a { + text-decoration: none; + font-weight: bold; +} +a:hover { + text-decoration: underline; +} +.feedlink { + font-weight: bold; +} +.headerTable { + width: 100%; + height: 68px; +} + +img { + border-radius: 4px; +} + +pre { + padding: 5px; + border-radius: 4px; +} + +.systemMessage { + position: absolute; + top: 45%; + left: 50%; + transform: translateX(-55%) translateY(-50%); + -webkit-user-select: none; + cursor: default; +} + +:root { + --header-table-border-color: rgba(0, 0, 0, 0.1); + --header-color: rgba(0, 0, 0, 0.3); + --body-code-color: #666; + --system-message-color: #cbcbcb; + --feedlink-color: rgba(255, 0, 0, 0.6); + --article-title-color: #333; + --article-date-color: rgba(0, 0, 0, 0.3); + --table-cell-border-color: lightgray; +} + +@media(prefers-color-scheme: dark) { + :root { + --header-color: rgba(94, 158, 244, 1); + --body-code-color: #b2b2b2; + --system-message-color: #5f5f5f; + --feedlink-color: rgba(94, 158, 244, 1); + --article-title-color: #e0e0e0; + --article-date-color: rgba(255, 255, 255, 0.5); + --table-cell-border-color: dimgray; + } +} + +body .headerTable { + border-bottom: 1px solid var(--header-table-border-color); + color: var(--header-color); +} +body .header { + color: var(--header-color); +} +body .header a:link, .header a:visited { + color: var(--header-color); +} + +body code, body pre { + color: var(--body-code-color); +} + +body > .systemMessage { + color: var(--system-message-color); +} + +.feedlink a:link, .feedlink a:visited { + color: var(--feedlink-color); +} + +.avatar img { + border-radius: 4px; +} + +.feedIcon { + border-radius: 4px; +} +.rightAlign { + text-align: end; +} +.leftAlign { + text-align: start; +} + +.articleTitle a:link, .articleTitle a:visited, .articleTitle h1 { + color: var(--article-title-color); + margin-top: 26px; + padding-bottom: 12px; + text-align: center; + margin: auto; + font-family: Charter, ui-serif; +} + +.articleDateline { + color: rgba(124, 124, 124, 1); + margin-bottom: 5px; + text-align: center; + padding-top: 1em; +} + +.articleDateline a:link, .articleDateline a:visited { + color: rgba(166, 166, 166, 1); + font-weight: normal; + padding-top: 1em; + text-align: center; +} + +.articleDatelineTitle { + color: rgba(124, 124, 124, 1); + margin-bottom: 5px; + font-weight: normal; +} + +.articleDatelineTitle a:link, .articleDatelineTitle a:visited { + color: var(--article-title-color); +} + +.externalLink { + margin-bottom: 5px; + font-style: italic; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: center; +} + + + +.articleBody { + margin-top: 20px; + line-height: 1.6em; +} + +h1 { + line-height: 1.15em; + font-weight: bold; + padding-bottom: 0; + margin-bottom: 5px; +} + +pre { + max-width: 100%; + margin: 0; + overflow: auto; + overflow-y: hidden; + word-wrap: normal; + word-break: normal; +} + +pre { + line-height: 1.4286em; +} + +code, pre { + font-family: "SF Mono", Menlo, "Courier New", Courier, monospace; + font-size: 0.95em; + -webkit-hyphens: none; +} + +pre code { + letter-spacing: -.027em; + font-size: 0.9375em; +} + +.nnw-overflow { + overflow-x: auto; +} +/* + Instead of the last-child bits, border-collapse: collapse + could have been used. However, then the inter-cell borders + overlap the table border, which looks bad. + */ +.nnw-overflow table { + margin-bottom: 1px; + border-spacing: 0; + border: 1px solid var(--secondary-accent-color); + font-size: inherit; +} +.nnw-overflow table table { + margin-bottom: 0; + border: none; +} +.nnw-overflow td, .nnw-overflow th { + -webkit-hyphens: none; + word-break: normal; + border: 1px solid var(--table-cell-border-color); + border-top: none; + border-left: none; + padding: 5px; +} + +.nnw-overflow tr :matches(td, th):last-child { + border-right: none; +} + +.nnw-overflow :matches(thead, tbody, tfoot):last-child > tr:last-child :matches(td, th) { + border-bottom: none; +} +.nnw-overflow td pre { + border: none; + padding: 0; +} +.nnw-overflow table[border="0"] { + border-width: 0; +} + +img, figure, video, div, object { + max-width: 100%; + height: auto !important; + margin: 0 auto; +} + +iframe { + max-width: 100%; + margin: 0 auto; +} + +iframe.nnw-constrained { + max-height: 50vw; +} + +figure { + margin-bottom: 1em; + margin-top: 1em; +} + +figcaption { + font-size: 14px; + line-height: 1.3em; +} + +sup { + vertical-align: top; + position: relative; + bottom: 0.2rem; +} + +sub { + vertical-align: bottom; + position: relative; + top: 0.2rem; +} + +hr { + border: none; + background-color: var(--table-cell-border-color); + height: 1px; + margin-top: 1em; +} + +.iframeWrap { + position: relative; + display: block; + padding-top: 56.25%; +} + +.iframeWrap iframe { + position: absolute; + top: 0; + left: 0; + height: 100% !important; + width: 100% !important; +} + +@media (prefers-color-scheme: light) { + blockquote { + background: color(srgb 0.947 0.947 0.947); + border-radius: 4px; + margin: 1.5em 0; + padding: 1rem; + color: color(srgb 0.383 0.383 0.383); + font-size: 0.95rem; + } + + pre { + background: color(srgb 0.947 0.947 0.947); + } + + p code { + background: color(srgb 0.947 0.947 0.947); + padding: 0.2em; + border-radius: 4px; + } +} + +@media (prefers-color-scheme: dark) { + blockquote { + background: rgba(49, 49, 49, 1); + border-radius: 4px; + margin: 1.5em 0; + padding: 1em; + color: rgba(195, 195, 195, 1); + font-size: 0.95rem; + } + + pre { + background: rgba(49, 49, 49, 1); + } + + p code { + background: rgba(49, 49, 49, 1); + padding: 0.2em; + border-radius: 4px; + } +} + +blockquote > blockquote { + padding-left: 1em; + padding-top: 0em; + padding-bottom: 0em; + font-style: italic; +} + + +blockquote > *:first-child { + margin-block-start: 0px; +} + +blockquote > *:last-child { + margin-block-end: 0px; +} + + +/* Feed Specific */ + +.feedbin--article-wrap { + border-top: 1px solid var(--header-table-border-color); +} + +/* Hide the external link at the bottom of Daring Fireball posts */ + +.x-netnewswire-hide { + display: none; +} + +/* see removeWpSmiley; this rule is kept in case a wp-smiley is encountered without alt text */ + +.wp-smiley { + height: 1em; + max-height: 1em; +} + +/* Twitter */ + +.twitterAvatar { + vertical-align: middle; + border-radius: 4px; + height: 1.7em; + width: 1.7em; +} + +.twitterUsername { + line-height: 1.2; + margin-left: 4px; + display: inline-block; + vertical-align: middle; +} + +.twitterScreenName { + font-size: 66%; +} + +.twitterTimestamp { + font-size: 66%; +} + +/*Block ads and junk*/ + +iframe[src*="feedads"], +iframe[src*="doubleclick"], +iframe[src*="plusone.google"] { + display: none !important; +} + +a[href*=".ads."], +a[href*="feedads"], +a[href*="doubleclick"], +a[href*="//ads."], +a[href*="api.tweetmeme"], +a[href*="delicious.com/post?"], +a[href*="digg.com/submit?"], +a[href*="google.com/bookmarks/mark?"], +a[href*="posterous.com/share?"], +a[href*="tumblr.com/share?"], +a[href*="linkedin.com/shareArticle?"], +a[href*="facebook.com/share.php?"], +a[href*="http://twitter.com/home?"], +a[href*="addtoany.com/share_save"] { + display: none !important; +} + +img[src*=".ads."], +img[src*="//ads."], +img[src*="doubleclick"], +img[src*="feedads"], +img[src*="feedburner"], +img[src*="feedblitz"], +img[src*="share-buttons"] { + display: none !important; +} + +/* Newsfoot specific styles. Structural styles come first, theme styles second */ +.newsfoot-footnote-container { + position: relative; + display: inline-block; + z-index: 9999; +} + +.newsfoot-footnote-popover { + position: absolute; + display: block; + padding: 0em 1em; + margin: 1em; + top: 0.75em; + max-width: none; + border-radius: 0.3em; + box-sizing: border-box; +} + +.newsfoot-footnote-popover { + left: calc(-1 * (50vw - 1em)); + right: calc(-1 * (50vw - 1em)); +} +.newsfoot-footnote-popover-arrow { + content: ''; + display: block; + width: 1em; + position: absolute; + top: -0.5em; + left: calc(50% - 0.5em); + height: 1em !important; + transform: rotate(45deg); + z-index:0; +} +.newsfoot-footnote-popover-inner { + border-radius: calc(0.3em - 1px); + padding: 1em; + position: relative; + z-index: 1; +} + +.newsfoot-footnote-popover-inner :first-child { + margin-top: 0; +} +.newsfoot-footnote-popover-inner :last-child { + margin-bottom: 0; +} + +.newsfoot-footnote-popover .reversefootnote, +.newsfoot-footnote-popover .footnoteBackLink, +.newsfoot-footnote-popover .footnote-return, +.newsfoot-footnote-popover a[href*='#fn'] { + display: none; +} + +sup[id^='fn'] { + vertical-align: baseline; +} + +a.footnote { + display: inline-block; + text-decoration: none; + padding: 0.05em 0.75em; + border-radius: 1em; + min-width: 1em; + text-align: center; + font-size: 0.8em; + line-height: 1em; + position:relative; + top: -0.1em; +} + +/* light / default */ +.newsfoot-footnote-popover { + background: #ccc; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5), 0 3px 6px rgba(0, 0, 0, 0.25); + color: black; + padding: 1px; +} +.newsfoot-footnote-popover-arrow { + background: #fafafa; + border: 1px solid #ccc; +} +.newsfoot-footnote-popover-inner { + background: #fafafa; +} +body a.footnote, +body a.footnote:visited, +.newsfoot-footnote-popover + a.footnote:hover { + background: #aaa; + color: white; + transition: background-color 200ms ease-out; +} +a.footnote:hover, +.newsfoot-footnote-popover + a.footnote { + background: #666; + transition: background-color 200ms ease-out; +} + +/* dark */ +@media screen and (prefers-color-scheme: dark) { + .newsfoot-footnote-popover { + background: #444; + color: rgb(224, 224, 224); + } + .newsfoot-footnote-popover-arrow { + background: #242424; + border: 1px solid #444; + } + .newsfoot-footnote-popover-inner { + background: #242424; + } + body a.footnote, + body a.footnote:visited, + .newsfoot-footnote-popover + a.footnote:hover { + background: #aaa; + color: white; + transition: background-color 200ms ease-out; + } + a.footnote:hover, + .newsfoot-footnote-popover + a.footnote { + background: #666; + transition: background-color 200ms ease-out; + } +} + +/* iOS Specific */ +@supports (-webkit-touch-callout: none) { + + body { + margin-top: 3px; + margin-bottom: 20px; + padding-left: 20px; + padding-right: 20px; + } + + :root { + color-scheme: light dark; + font-family: 'Avenir-Book', -apple-system-body; + font-size: [[font-size]]px; + --primary-accent-color: #086AEE; + --secondary-accent-color: #086AEE; + --block-quote-border-color: rgba(8, 106, 238, 0.75); + } + + @media(prefers-color-scheme: dark) { + :root { + --primary-accent-color: #2D80F1; + --secondary-accent-color: #5E9EF4; + --block-quote-border-color: rgba(94, 158, 244, 0.75); + --header-table-border-color: rgba(255, 255, 255, 0.2); + } + } + + body a, body a:visited, body a * { + color: var(--secondary-accent-color); + } + body .header { + font: -apple-system-body; + font-size: [[font-size]]px; + } + body .header a:link, body .header a:visited { + color: var(--primary-accent-color); + } + + .avatar img { + border-radius: 4px; + } + + .nnw-overflow table { + border: none; + } + + .activityIndicatorWrap { + position: relative; + } + + .activityIndicator { + z-index: 1; + width: 64px; + height: 64px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } + +} + +/* macOS Specific */ +@supports not (-webkit-touch-callout: none) { + + body { + margin-top: 20px; + margin-bottom: 64px; + padding-left: 48px; + padding-right: 48px; + font-family: 'Avenir', -apple-system; + } + + .smallText { + font-size: 14px; + } + + .mediumText { + font-size: 16px; + } + + .largeText { + font-size: 18px; + } + + .xlargeText { + font-size: 20px; + } + + .xxlargeText { + font-size: 22px; + } + + :root { + color-scheme: light dark; + --accent-color: rgba(8, 106, 238, 1); + --block-quote-border-color: rgba(8, 106, 238, .50); + } + + @media(prefers-color-scheme: dark) { + :root { + --accent-color: rgba(94, 158, 244, 1); + --block-quote-border-color: rgba(94, 158, 244, .50); + --header-table-border-color: rgba(255, 255, 255, 0.1); + } + } + + body a, body a:visited, body a * { + color: var(--accent-color); + } + + .nnw-overflow table { + //border: 1px solid var(--accent-color); + } + +} diff --git a/Shared/Resources/Promenade.nnwtheme/template.html b/Shared/Resources/Promenade.nnwtheme/template.html new file mode 100644 index 000000000..b7c93560a --- /dev/null +++ b/Shared/Resources/Promenade.nnwtheme/template.html @@ -0,0 +1,60 @@ + + + +
+ +
+

[[title]]

+
+ +
[[byline]]
+
+
+ + + + diff --git a/Shared/Resources/Sepia.nnwtheme/Info.plist b/Shared/Resources/Sepia.nnwtheme/Info.plist new file mode 100644 index 000000000..6ad2fab65 --- /dev/null +++ b/Shared/Resources/Sepia.nnwtheme/Info.plist @@ -0,0 +1,16 @@ + + + + + Name + Sepia + ThemeIdentifier + com.netnewswire.themes.sepia + CreatorHomePage + http://netnewswire.com/ + CreatorName + Ranchero Software + Version + 1 + + diff --git a/Shared/Resources/Sepia.nnwtheme/stylesheet.css b/Shared/Resources/Sepia.nnwtheme/stylesheet.css new file mode 100644 index 000000000..b7d9a02dc --- /dev/null +++ b/Shared/Resources/Sepia.nnwtheme/stylesheet.css @@ -0,0 +1,428 @@ +/* Shared iOS and macOS CSS rules. Platform specific rules are at the bottom of this file. */ + +body { + margin-left: auto; + margin-right: auto; + + word-wrap: break-word; + max-width: 44em; + background-color: #FBF0D9; + color: #704214; +} + +a:hover { + text-decoration: underline; +} + +.feedlink { + font-weight: bold; +} + +.headerTable { + width: 100%; + height: 68px; +} + +.systemMessage { + position: absolute; + top: 45%; + left: 50%; + transform: translateX(-55%) translateY(-50%); + -webkit-user-select: none; + cursor: default; +} + +:root { + --header-table-border-color: rgba(0, 0, 0, 0.3); + --header-color: rgba(0, 0, 0, 0.5); + --body-code-color: #704214; + --system-message-color: #704214; + --feedlink-color: rgba(255, 0, 0, 0.6); + --article-title-color: #704214; + --article-date-color: rgba(0, 0, 0, 0.5); + --table-cell-border-color: lightgray; + --primary-accent-color: #43350E; + --secondary-accent-color: #43350E; + --block-quote-border-color: rgba(0, 0, 0, 0.3); +} + +body a, body a:visited, body a * { + color: var(--secondary-accent-color); +} + + +body .headerTable { + border-bottom: 1px solid var(--header-table-border-color); + color: var(--header-color); +} + +body .header { + color: var(--header-color); +} + +body .header a:link, .header a:visited { + color: var(--header-color); +} + +body code, body pre { + color: var(--body-code-color); +} + +body > .systemMessage { + color: var(--system-message-color); +} + +.headerContainer a:link, .headerContainer a:visited { + text-decoration: none; + color: var(--feedlink-color); +} + +.headerContainer a:hover { + text-decoration: underline; +} + +.avatar img { + border-radius: 4px; +} + +.feedIcon { + border-radius: 4px; +} + +.rightAlign { + text-align: end; +} + +.leftAlign { + text-align: start; +} + +.articleTitle a:link, .articleTitle a:visited { + text-decoration: none; + color: var(--article-title-color); + margin-top: 26px; +} + +.articleTitle a:hover { + text-decoration: underline; +} + +.articleDateline { + margin-bottom: 5px; + font-weight: bold; +} + +.articleDateline a:link, .articleDateline a:visited { + text-decoration: none; + color: var(--article-date-color); +} + +.articleDateline a:hover { + text-decoration: underline; +} + +.articleDatelineTitle { + margin-bottom: 5px; + font-weight: bold; +} + +.articleDatelineTitle a:link, .articleDatelineTitle a:visited { + text-decoration: none; + color: var(--article-title-color); +} + +.articleDatelineTitle a:hover { + text-decoration: underline; +} + +.externalLink { + margin-bottom: 5px; + font-style: italic; + width: 100%; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.externalLink a:link, .externalLink a:visited { + text-decoration: none; +} + +.externalLink a:hover { + text-decoration: underline; +} + +.articleBody { + margin-top: 20px; + line-height: 1.6em; +} + +h1 { + line-height: 1.15em; + font-weight: bold; + padding-bottom: 0; + margin-bottom: 5px; +} + +pre { + max-width: 100%; + margin: 0; + overflow: auto; + overflow-y: hidden; + word-wrap: normal; + word-break: normal; +} + +pre { + line-height: 1.4286em; +} + +code, pre { + font-family: "SF Mono", Menlo, "Courier New", Courier, monospace; + font-size: 1em; + -webkit-hyphens: none; +} + +pre code { + letter-spacing: -.027em; + font-size: 0.9375em; +} + +.nnw-overflow { + overflow-x: auto; +} + +/* + Instead of the last-child bits, border-collapse: collapse + could have been used. However, then the inter-cell borders + overlap the table border, which looks bad. + */ +.nnw-overflow table { + margin-bottom: 1px; + border-spacing: 0; + border: 1px solid var(--secondary-accent-color); + font-size: inherit; +} + +.nnw-overflow table table { + margin-bottom: 0; + border: none; +} + +.nnw-overflow td, .nnw-overflow th { + -webkit-hyphens: none; + word-break: normal; + border: 1px solid var(--table-cell-border-color); + border-top: none; + border-left: none; + padding: 5px; +} + +.nnw-overflow tr :matches(td, th):last-child { + border-right: none; +} + +.nnw-overflow :matches(thead, tbody, tfoot):last-child > tr:last-child :matches(td, th) { + border-bottom: none; +} + +.nnw-overflow td pre { + border: none; + padding: 0; +} + +.nnw-overflow table[border="0"] { + border-width: 0; +} + +img, figure, video, div, object { + max-width: 100%; + height: auto !important; + margin: 0 auto; +} + +iframe { + max-width: 100%; + margin: 0 auto; +} + +iframe.nnw-constrained { + max-height: 50vw; +} + +figure { + margin-bottom: 1em; + margin-top: 1em; +} + +figcaption { + font-size: 14px; + line-height: 1.3em; +} + +sup { + vertical-align: top; + position: relative; + bottom: 0.2rem; +} + +sub { + vertical-align: bottom; + position: relative; + top: 0.2rem; +} + +hr { + border: 1.5px solid var(--table-cell-border-color); +} + +.iframeWrap { + position: relative; + display: block; + padding-top: 56.25%; +} + +.iframeWrap iframe { + position: absolute; + top: 0; + left: 0; + height: 100% !important; + width: 100% !important; +} + +blockquote { + margin-inline-start: 0; + margin-inline-end: 0; + padding-inline-start: 15px; + border-inline-start: 3px solid var(--block-quote-border-color); +} + +/* Feed Specific */ + +.feedbin--article-wrap { + border-top: 1px solid var(--header-table-border-color); +} + +/* Twitter */ + +.twitterAvatar { + vertical-align: middle; + border-radius: 4px; + height: 1.7em; + width: 1.7em; +} + +.twitterUsername { + line-height: 1.2; + margin-left: 4px; + display: inline-block; + vertical-align: middle; +} + +.twitterScreenName { + font-size: 66%; +} + +.twitterTimestamp { + font-size: 66%; +} + +/* Newsfoot theme for light mode (default) */ +.newsfoot-footnote-popover { + background: #ccc; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5), 0 3px 6px rgba(0, 0, 0, 0.25); + color: #704214; + padding: 1px; +} + +.newsfoot-footnote-popover-arrow { + background: #FBF0D9; + border: 1px solid #ccc; +} + +.newsfoot-footnote-popover-inner { + background: #FBF0D9; +} + +body a.footnote, +body a.footnote:visited, +.newsfoot-footnote-popover + a.footnote:hover { + background: #aaa; + color: white; + transition: background-color 200ms ease-out; +} + +a.footnote:hover, +.newsfoot-footnote-popover + a.footnote { + background: #666; + transition: background-color 200ms ease-out; +} + +/* iOS Specific */ +@supports (-webkit-touch-callout: none) { + + body { + margin-top: 3px; + margin-bottom: 20px; + padding-left: 20px; + padding-right: 20px; + + word-break: break-word; + -webkit-hyphens: auto; + -webkit-text-size-adjust: none; + font: Georgia; + font-size: [[font-size]]px; + } + + pre { + border: 1px solid var(--secondary-accent-color); + padding: 5px; + } + + .nnw-overflow table { + border: 1px solid var(--secondary-accent-color); + } + +} + +/* macOS Specific */ +@supports not (-webkit-touch-callout: none) { + + body { + margin-top: 20px; + margin-bottom: 64px; + padding-left: 48px; + padding-right: 48px; + font-family: Georgia; + } + + .smallText { + font-size: 14px; + } + + .mediumText { + font-size: 16px; + } + + .largeText { + font-size: 18px; + } + + .xlargeText { + font-size: 20px; + } + + .xxlargeText { + font-size: 22px; + } + + pre { + border: 1px solid var(--primary-accent-color); + padding: 10px; + } + + .nnw-overflow table { + border: 1px solid var(--primary-accent-color); + } + +} \ No newline at end of file diff --git a/Shared/Resources/Sepia.nnwtheme/template.html b/Shared/Resources/Sepia.nnwtheme/template.html new file mode 100644 index 000000000..2b6461013 --- /dev/null +++ b/Shared/Resources/Sepia.nnwtheme/template.html @@ -0,0 +1,15 @@ +
+ + + + + +
[[feed_link_title]]
[[byline]]
+
+ + diff --git a/Shared/SmartFeeds/SmartFeed.swift b/Shared/SmartFeeds/SmartFeed.swift index e8aa2f1bc..e8245a0b9 100644 --- a/Shared/SmartFeeds/SmartFeed.swift +++ b/Shared/SmartFeeds/SmartFeed.swift @@ -14,6 +14,8 @@ import Account final class SmartFeed: PseudoFeed { + var account: Account? = nil + public var defaultReadFilterType: ReadFilterType { return .none } diff --git a/Shared/SmartFeeds/UnreadFeed.swift b/Shared/SmartFeeds/UnreadFeed.swift index f8ce3660c..eb9f4fb9c 100644 --- a/Shared/SmartFeeds/UnreadFeed.swift +++ b/Shared/SmartFeeds/UnreadFeed.swift @@ -19,6 +19,8 @@ import ArticlesDatabase // This just shows the global unread count, which appDelegate already has. Easy. final class UnreadFeed: PseudoFeed { + + var account: Account? = nil public var defaultReadFilterType: ReadFilterType { return .alwaysRead diff --git a/Shared/UserInfoKey.swift b/Shared/UserInfoKey.swift index dbd9900bb..f46f5ca8b 100644 --- a/Shared/UserInfoKey.swift +++ b/Shared/UserInfoKey.swift @@ -23,5 +23,7 @@ struct UserInfoKey { static let readArticlesFilterStateKeys = "readArticlesFilterStateKey" static let readArticlesFilterStateValues = "readArticlesFilterStateValue" static let selectedFeedsState = "selectedFeedsState" + static let isShowingExtractedArticle = "isShowingExtractedArticle" + static let articleWindowScrollY = "articleWindowScrollY" } diff --git a/Shared/Widget/WidgetDataEncoder.swift b/Shared/Widget/WidgetDataEncoder.swift index 86c46be3b..575848eb4 100644 --- a/Shared/Widget/WidgetDataEncoder.swift +++ b/Shared/Widget/WidgetDataEncoder.swift @@ -30,65 +30,59 @@ public final class WidgetDataEncoder { @available(iOS 14, *) func encodeWidgetData() throws { + os_log(.debug, log: log, "Starting encoding widget data.") - do { - let unreadArticles = Array(try AccountManager.shared.fetchArticles(.unread(fetchLimit))).sortedByDate(.orderedDescending) - let starredArticles = Array(try AccountManager.shared.fetchArticles(.starred(fetchLimit))).sortedByDate(.orderedDescending) - let todayArticles = Array(try AccountManager.shared.fetchArticles(.today(fetchLimit))).sortedByDate(.orderedDescending) - - var unread = [LatestArticle]() - var today = [LatestArticle]() - var starred = [LatestArticle]() - - for article in unreadArticles { - let latestArticle = LatestArticle(id: article.sortableArticleID, - feedTitle: article.sortableName, - articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), - articleSummary: article.summary, - feedIcon: article.iconImage()?.image.dataRepresentation(), - pubDate: article.datePublished?.description ?? "") - unread.append(latestArticle) - } - - for article in starredArticles { - let latestArticle = LatestArticle(id: article.sortableArticleID, - feedTitle: article.sortableName, - articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), - articleSummary: article.summary, - feedIcon: article.iconImage()?.image.dataRepresentation(), - pubDate: article.datePublished?.description ?? "") - starred.append(latestArticle) - } - - for article in todayArticles { - let latestArticle = LatestArticle(id: article.sortableArticleID, - feedTitle: article.sortableName, - articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), - articleSummary: article.summary, - feedIcon: article.iconImage()?.image.dataRepresentation(), - pubDate: article.datePublished?.description ?? "") - today.append(latestArticle) - } - - let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount, - currentTodayCount: SmartFeedsController.shared.todayFeed.unreadCount, - currentStarredCount: try! SmartFeedsController.shared.starredFeed.fetchArticles().count, - unreadArticles: unread, - starredArticles: starred, - todayArticles:today, - lastUpdateTime: Date()) - - - DispatchQueue.global().async { [weak self] in - guard let self = self else { return } + DispatchQueue.main.async { + do { + let unreadArticles = Array(try AccountManager.shared.fetchArticles(.unread(self.fetchLimit))).sortedByDate(.orderedDescending) + let starredArticles = Array(try AccountManager.shared.fetchArticles(.starred(self.fetchLimit))).sortedByDate(.orderedDescending) + let todayArticles = Array(try AccountManager.shared.fetchArticles(.today(self.fetchLimit))).sortedByDate(.orderedDescending) - self.backgroundTaskID = UIApplication.shared.beginBackgroundTask (withName: "com.ranchero.NetNewsWire.Encode") { - UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) - self.backgroundTaskID = .invalid + var unread = [LatestArticle]() + var today = [LatestArticle]() + var starred = [LatestArticle]() + + for article in unreadArticles { + let latestArticle = LatestArticle(id: article.sortableArticleID, + feedTitle: article.sortableName, + articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), + articleSummary: article.summary, + feedIcon: article.iconImage()?.image.dataRepresentation(), + pubDate: article.datePublished?.description ?? "") + unread.append(latestArticle) } - let encodedData = try? JSONEncoder().encode(latestData) + for article in starredArticles { + let latestArticle = LatestArticle(id: article.sortableArticleID, + feedTitle: article.sortableName, + articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), + articleSummary: article.summary, + feedIcon: article.iconImage()?.image.dataRepresentation(), + pubDate: article.datePublished?.description ?? "") + starred.append(latestArticle) + } + + for article in todayArticles { + let latestArticle = LatestArticle(id: article.sortableArticleID, + feedTitle: article.sortableName, + articleTitle: ArticleStringFormatter.truncatedTitle(article).isEmpty ? ArticleStringFormatter.truncatedSummary(article) : ArticleStringFormatter.truncatedTitle(article), + articleSummary: article.summary, + feedIcon: article.iconImage()?.image.dataRepresentation(), + pubDate: article.datePublished?.description ?? "") + today.append(latestArticle) + } + + let latestData = WidgetData(currentUnreadCount: SmartFeedsController.shared.unreadFeed.unreadCount, + currentTodayCount: SmartFeedsController.shared.todayFeed.unreadCount, + currentStarredCount: try! SmartFeedsController.shared.starredFeed.fetchArticles().count, + unreadArticles: unread, + starredArticles: starred, + todayArticles:today, + lastUpdateTime: Date()) + + + let encodedData = try JSONEncoder().encode(latestData) os_log(.debug, log: self.log, "Finished encoding widget data.") if self.fileExists() { @@ -98,13 +92,9 @@ public final class WidgetDataEncoder { if FileManager.default.createFile(atPath: self.dataURL!.path, contents: encodedData, attributes: nil) { os_log(.debug, log: self.log, "Wrote widget data to container.") WidgetCenter.shared.reloadAllTimelines() - UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) - self.backgroundTaskID = .invalid - } else { - UIApplication.shared.endBackgroundTask(self.backgroundTaskID!) - self.backgroundTaskID = .invalid } - + } catch { + print(error.localizedDescription) } } } diff --git a/Technotes/ReleaseNotes-iOS.markdown b/Technotes/ReleaseNotes-iOS.markdown index 8102b5fd3..4150dfb6c 100644 --- a/Technotes/ReleaseNotes-iOS.markdown +++ b/Technotes/ReleaseNotes-iOS.markdown @@ -1,5 +1,26 @@ # iOS Release Notes +### 6.0.1 TestFlight build 608 - 28 Aug 2021 + +Fixed our top crashing bug — it could happen when updating a table view + +### 6.0.1 TestFlight build 607 - 21 Aug 2021 + +Fixed bug where BazQux-synced feeds might stop updating +Fixed bug where words prepended with $ wouldn’t appear in Twitter feeds +Fixed bug where newlines would be just a space in Twitter feeds +Fixed a crashing bug in Twitter rendering +Fixed bug where hitting b key to open in browser wouldn’t always work +Fixed a crashing bug due to running code off the main thread that needed to be on the main thread +Fixed bug where article unread indicator could have wrong alpha in specific circumstances +Fixed bug using right arrow key to move focus to Article view +Fixed bug where long press could trigger a crash +Fixed bug where external URLs in Feedbin feeds might be lost +Fixed bug where favicons wouldn’t be found when a home page URL has non-ASCII characters +Fixed bug where iCloud syncing could stop prematurely when the sync database has records not in the local database +Fixed bug where creating a new folder in iCloud and moving feeds to it wouldn’t sync correctly + + ### 6.0 TestFlight build 604 - 31 May 2021 This is a final candidate diff --git a/Technotes/Themes.md b/Technotes/Themes.md new file mode 100644 index 000000000..387c3cb65 --- /dev/null +++ b/Technotes/Themes.md @@ -0,0 +1,35 @@ +# Themes + +## `.nnwtheme` Structure + +An `.nnwtheme` comprises of three files: +- `Info.plist` +- `template.html` +- `stylesheet.css` + +### Info.plist +The `Info.plist` requires the following keys/types: + +|Key|Type|Notes| +|---|---|---| +|`ThemeIdentifier`|`String`|Unique identifier for the theme, e.g. using reverse domain name.| +|`Name`|`String`|Theme name| +|`CreatorHomePage`|`String`|| +|`CreatorName`|`String`|| +|`Version`|`Integer`|| + +### template.html +This provides a starting point for editing the structure of the page. Theme variables are documented in the header. + +### stylesheet.css +This provides a starting point for editing the style of the page. + +## Add Themes Directly to NetNewsWire with URL Scheme +On iOS and macOS, themes can be opened directly in NetNewsWire using the below URL scheme: + +`netnewswire://theme/add?url={url}` + +When using this URL scheme the theme being shared must be zipped. + +Parameters: +- `url`: (mandatory, URL-encoded): The theme's location. diff --git a/iOS/AppDefaults.swift b/iOS/AppDefaults.swift index d1e7ec909..c7a01f33a 100644 --- a/iOS/AppDefaults.swift +++ b/iOS/AppDefaults.swift @@ -28,6 +28,8 @@ enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable { final class AppDefaults { + static let defaultThemeName = "Defaults" + static let shared = AppDefaults() private init() {} @@ -54,6 +56,8 @@ final class AppDefaults { static let addWebFeedAccountID = "addWebFeedAccountID" static let addWebFeedFolderName = "addWebFeedFolderName" static let addFolderAccountID = "addFolderAccountID" + static let useSystemBrowser = "useSystemBrowser" + static let currentThemeName = "currentThemeName" } let isDeveloperBuild: Bool = { @@ -119,6 +123,15 @@ final class AppDefaults { } } + var useSystemBrowser: Bool { + get { + return UserDefaults.standard.bool(forKey: Key.useSystemBrowser) + } + set { + UserDefaults.standard.setValue(newValue, forKey: Key.useSystemBrowser) + } + } + var lastImageCacheFlushDate: Date? { get { return AppDefaults.date(for: Key.lastImageCacheFlushDate) @@ -210,6 +223,15 @@ final class AppDefaults { } } + var currentThemeName: String? { + get { + return AppDefaults.string(for: Key.currentThemeName) + } + set { + AppDefaults.setString(for: Key.currentThemeName, newValue) + } + } + static func registerDefaults() { let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue, Key.timelineGroupByFeed: false, @@ -219,7 +241,8 @@ final class AppDefaults { Key.timelineSortDirection: ComparisonResult.orderedDescending.rawValue, Key.articleFullscreenAvailable: false, Key.articleFullscreenEnabled: false, - Key.confirmMarkAllAsRead: true] + Key.confirmMarkAllAsRead: true, + Key.currentThemeName: Self.defaultThemeName] AppDefaults.store.register(defaults: defaults) } diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index d4fa54768..807d1a340 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -63,10 +63,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD appDelegate = self SecretsManager.provider = Secrets() - let documentAccountURL = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! - let documentAccountsFolder = documentAccountURL.appendingPathComponent("Accounts").absoluteString + let documentFolder = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let documentAccountsFolder = documentFolder.appendingPathComponent("Accounts").absoluteString let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7))) AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath) + + let documentThemesFolder = documentFolder.appendingPathComponent("Themes").absoluteString + let documentThemesFolderPath = String(documentThemesFolder.suffix(from: documentAccountsFolder.index(documentThemesFolder.startIndex, offsetBy: 7))) + ArticleThemesManager.shared = ArticleThemesManager(folderPath: documentThemesFolderPath) + FeedProviderManager.shared.delegate = ExtensionPointManager.shared NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) @@ -333,6 +338,7 @@ private extension AppDelegate { AccountManager.shared.suspendNetworkAll() AccountManager.shared.suspendDatabaseAll() + ArticleThemeDownloader.shared.cleanUp() CoalescingQueue.standard.performCallsImmediately() for scene in UIApplication.shared.connectedScenes { diff --git a/iOS/Article/ArticleViewController.swift b/iOS/Article/ArticleViewController.swift index 6629ecc4d..81e537dc3 100644 --- a/iOS/Article/ArticleViewController.swift +++ b/iOS/Article/ArticleViewController.swift @@ -59,6 +59,14 @@ class ArticleViewController: UIViewController { } } + var restoreScrollPosition: (isShowingExtractedArticle: Bool, articleWindowScrollY: Int)? { + didSet { + if let rsp = restoreScrollPosition { + currentWebViewController?.setScrollPosition(isShowingExtractedArticle: rsp.isShowingExtractedArticle, articleWindowScrollY: rsp.articleWindowScrollY) + } + } + } + var currentState: State? { guard let controller = currentWebViewController else { return nil} return State(extractedArticle: controller.extractedArticle, @@ -125,6 +133,10 @@ class ArticleViewController: UIViewController { controller = createWebViewController(article, updateView: true) } + if let rsp = restoreScrollPosition { + controller.setScrollPosition(isShowingExtractedArticle: rsp.isShowingExtractedArticle, articleWindowScrollY: rsp.articleWindowScrollY) + } + articleExtractorButton.buttonState = controller.articleExtractorButtonState self.pageViewController.setViewControllers([controller], direction: .forward, animated: false, completion: nil) @@ -311,7 +323,11 @@ class ArticleViewController: UIViewController { func openInAppBrowser() { currentWebViewController?.openInAppBrowser() - } + } + + func setScrollPosition(isShowingExtractedArticle: Bool, articleWindowScrollY: Int) { + currentWebViewController?.setScrollPosition(isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY) + } } // MARK: Find in Article diff --git a/iOS/Article/WebViewController.swift b/iOS/Article/WebViewController.swift index fe6188903..4f18650ad 100644 --- a/iOS/Article/WebViewController.swift +++ b/iOS/Article/WebViewController.swift @@ -63,14 +63,16 @@ class WebViewController: UIViewController { let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 0.3) var windowScrollY = 0 - + private var restoreWindowScrollY: Int? + override func viewDidLoad() { super.viewDidLoad() NotificationCenter.default.addObserver(self, selector: #selector(webFeedIconDidBecomeAvailable(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(faviconDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) - + NotificationCenter.default.addObserver(self, selector: #selector(currentArticleThemeDidChangeNotification(_:)), name: .CurrentArticleThemeDidChangeNotification, object: nil) + // Configure the tap zones configureTopShowBarsView() configureBottomShowBarsView() @@ -100,6 +102,10 @@ class WebViewController: UIViewController { reloadArticleImage() } + @objc func currentArticleThemeDidChangeNotification(_ note: Notification) { + loadWebView() + } + // MARK: Actions @objc func showBars(_ sender: Any) { @@ -124,6 +130,27 @@ class WebViewController: UIViewController { } + func setScrollPosition(isShowingExtractedArticle: Bool, articleWindowScrollY: Int) { + if isShowingExtractedArticle { + switch articleExtractor?.state { + case .ready: + restoreWindowScrollY = articleWindowScrollY + startArticleExtractor() + case .complete: + windowScrollY = articleWindowScrollY + loadWebView() + case .processing: + restoreWindowScrollY = articleWindowScrollY + default: + restoreWindowScrollY = articleWindowScrollY + startArticleExtractor() + } + } else { + windowScrollY = articleWindowScrollY + loadWebView() + } + } + func focus() { webView?.becomeFirstResponder() } @@ -250,8 +277,12 @@ class WebViewController: UIViewController { func openInAppBrowser() { guard let url = article?.preferredURL else { return } - let vc = SFSafariViewController(url: url) - present(vc, animated: true) + if AppDefaults.shared.useSystemBrowser { + UIApplication.shared.open(url, options: [:]) + } else { + let vc = SFSafariViewController(url: url) + present(vc, animated: true) + } } } @@ -268,6 +299,9 @@ extension WebViewController: ArticleExtractorDelegate { func articleExtractionDidComplete(extractedArticle: ExtractedArticle) { if articleExtractor?.state != .cancelled { self.extractedArticle = extractedArticle + if let restoreWindowScrollY = restoreWindowScrollY { + windowScrollY = restoreWindowScrollY + } isShowingExtractedArticle = true loadWebView() articleExtractorButtonState = .on @@ -344,7 +378,18 @@ extension WebViewController: WKNavigationDelegate { let components = URLComponents(url: url, resolvingAgainstBaseURL: false) if components?.scheme == "http" || components?.scheme == "https" { decisionHandler(.cancel) - openURL(url) + if AppDefaults.shared.useSystemBrowser { + UIApplication.shared.open(url, options: [:]) + } else { + UIApplication.shared.open(url, options: [.universalLinksOnly: true]) { didOpen in + guard didOpen == false else { + return + } + let vc = SFSafariViewController(url: url) + self.present(vc, animated: true) + } + } + } else if components?.scheme == "mailto" { decisionHandler(.cancel) @@ -528,23 +573,23 @@ private extension WebViewController { func renderPage(_ webView: PreloadedWebView?) { guard let webView = webView else { return } - let style = ArticleStylesManager.shared.currentStyle + let theme = ArticleThemesManager.shared.currentTheme let rendering: ArticleRenderer.Rendering if let articleExtractor = articleExtractor, articleExtractor.state == .processing { - rendering = ArticleRenderer.loadingHTML(style: style) + rendering = ArticleRenderer.loadingHTML(theme: theme) } else if let articleExtractor = articleExtractor, articleExtractor.state == .failedToParse, let article = article { - rendering = ArticleRenderer.articleHTML(article: article, style: style) + rendering = ArticleRenderer.articleHTML(article: article, theme: theme) } else if let article = article, let extractedArticle = extractedArticle { if isShowingExtractedArticle { - rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, style: style) + rendering = ArticleRenderer.articleHTML(article: article, extractedArticle: extractedArticle, theme: theme) } else { - rendering = ArticleRenderer.articleHTML(article: article, style: style) + rendering = ArticleRenderer.articleHTML(article: article, theme: theme) } } else if let article = article { - rendering = ArticleRenderer.articleHTML(article: article, style: style) + rendering = ArticleRenderer.articleHTML(article: article, theme: theme) } else { - rendering = ArticleRenderer.noSelectionHTML(style: style) + rendering = ArticleRenderer.noSelectionHTML(theme: theme) } let substitutions = [ @@ -571,6 +616,7 @@ private extension WebViewController { } func startArticleExtractor() { + guard articleExtractor == nil else { return } if let link = article?.preferredLink, let extractor = ArticleExtractor(link) { extractor.delegate = self extractor.process() diff --git a/iOS/Base.lproj/LaunchScreenPad.storyboard b/iOS/Base.lproj/LaunchScreenPad.storyboard index 876bded5e..06c951431 100644 --- a/iOS/Base.lproj/LaunchScreenPad.storyboard +++ b/iOS/Base.lproj/LaunchScreenPad.storyboard @@ -70,7 +70,7 @@ - + @@ -114,6 +114,6 @@ - + diff --git a/iOS/Base.lproj/LaunchScreenPhone.storyboard b/iOS/Base.lproj/LaunchScreenPhone.storyboard index 08b810d7d..61a86ee99 100644 --- a/iOS/Base.lproj/LaunchScreenPhone.storyboard +++ b/iOS/Base.lproj/LaunchScreenPhone.storyboard @@ -70,7 +70,7 @@ - + @@ -114,6 +114,6 @@ - + diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index 82dc391e2..44386a8f9 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -199,7 +199,7 @@ - + @@ -402,7 +402,7 @@ - + diff --git a/iOS/MasterFeed/Cell/MasterFeedRowIdentifier.swift b/iOS/MasterFeed/Cell/MasterFeedRowIdentifier.swift new file mode 100644 index 000000000..8737decf3 --- /dev/null +++ b/iOS/MasterFeed/Cell/MasterFeedRowIdentifier.swift @@ -0,0 +1,23 @@ +// +// MasterFeedRowIdentifier.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 10/20/21. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import Foundation + +class MasterFeedRowIdentifier: NSObject, NSCopying { + + var indexPath: IndexPath + + init(indexPath: IndexPath) { + self.indexPath = indexPath + } + + func copy(with zone: NSZone? = nil) -> Any { + return self + } + +} diff --git a/iOS/MasterFeed/MasterFeedDataSource.swift b/iOS/MasterFeed/MasterFeedDataSource.swift deleted file mode 100644 index dff3d79c1..000000000 --- a/iOS/MasterFeed/MasterFeedDataSource.swift +++ /dev/null @@ -1,22 +0,0 @@ -// -// MasterFeedDataSource.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 8/28/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import UIKit -import RSTree -import Account - -class MasterFeedDataSource: UITableViewDiffableDataSource { - - override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { - guard let identifier = itemIdentifier(for: indexPath), identifier.isEditable else { - return false - } - return true - } - -} diff --git a/iOS/MasterFeed/MasterFeedDataSourceOperation.swift b/iOS/MasterFeed/MasterFeedDataSourceOperation.swift deleted file mode 100644 index ac04c1946..000000000 --- a/iOS/MasterFeed/MasterFeedDataSourceOperation.swift +++ /dev/null @@ -1,39 +0,0 @@ -// -// MasterFeedDataSourceOperation.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 2/23/20. -// Copyright © 2020 Ranchero Software. All rights reserved. -// - -import UIKit -import RSCore -import RSTree - -class MasterFeedDataSourceOperation: MainThreadOperation { - - // MainThreadOperation - public var isCanceled = false - public var id: Int? - public weak var operationDelegate: MainThreadOperationDelegate? - public var name: String? = "MasterFeedDataSourceOperation" - public var completionBlock: MainThreadOperation.MainThreadOperationCompletionBlock? - - private var dataSource: UITableViewDiffableDataSource - private var snapshot: NSDiffableDataSourceSnapshot - private var animating: Bool - - init(dataSource: UITableViewDiffableDataSource, snapshot: NSDiffableDataSourceSnapshot, animating: Bool) { - self.dataSource = dataSource - self.snapshot = snapshot - self.animating = animating - } - - func run() { - dataSource.apply(snapshot, animatingDifferences: animating) { [weak self] in - guard let self = self else { return } - self.operationDelegate?.operationDidComplete(self) - } - } - -} diff --git a/iOS/MasterFeed/MasterFeedViewController+Drag.swift b/iOS/MasterFeed/MasterFeedViewController+Drag.swift index 7c089b796..9bf7a5010 100644 --- a/iOS/MasterFeed/MasterFeedViewController+Drag.swift +++ b/iOS/MasterFeed/MasterFeedViewController+Drag.swift @@ -13,11 +13,11 @@ import Account extension MasterFeedViewController: UITableViewDragDelegate { func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { - guard let identifier = dataSource.itemIdentifier(for: indexPath), identifier.isWebFeed, let url = identifier.url else { + guard let node = coordinator.nodeFor(indexPath), let webFeed = node.representedObject as? WebFeed else { return [UIDragItem]() } - let data = url.data(using: .utf8) + let data = webFeed.url.data(using: .utf8) let itemProvider = NSItemProvider() itemProvider.registerDataRepresentation(forTypeIdentifier: kUTTypeURL as String, visibility: .ownProcess) { completion in @@ -26,7 +26,7 @@ extension MasterFeedViewController: UITableViewDragDelegate { } let dragItem = UIDragItem(itemProvider: itemProvider) - dragItem.localObject = identifier + dragItem.localObject = node return [dragItem] } diff --git a/iOS/MasterFeed/MasterFeedViewController+Drop.swift b/iOS/MasterFeed/MasterFeedViewController+Drop.swift index 1dbb67fd2..c7f925c07 100644 --- a/iOS/MasterFeed/MasterFeedViewController+Drop.swift +++ b/iOS/MasterFeed/MasterFeedViewController+Drop.swift @@ -22,24 +22,22 @@ extension MasterFeedViewController: UITableViewDropDelegate { return UITableViewDropProposal(operation: .forbidden) } - guard let destIdentifier = dataSource.itemIdentifier(for: destIndexPath) else { - return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) - } - - guard let destAccount = destIdentifier.account, let destCell = tableView.cellForRow(at: destIndexPath) else { - return UITableViewDropProposal(operation: .forbidden) - } + guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? Feed, + let destAccount = destFeed.account, + let destCell = tableView.cellForRow(at: destIndexPath) else { + return UITableViewDropProposal(operation: .forbidden) + } // Validate account specific behaviors... if destAccount.behaviors.contains(.disallowFeedInMultipleFolders), - let sourceFeedID = (session.localDragSession?.items.first?.localObject as? MasterFeedTableViewIdentifier)?.feedID, - let sourceWebFeed = AccountManager.shared.existingFeed(with: sourceFeedID) as? WebFeed, + let sourceNode = session.localDragSession?.items.first?.localObject as? Node, + let sourceWebFeed = sourceNode.representedObject as? WebFeed, sourceWebFeed.account?.accountID != destAccount.accountID && destAccount.hasWebFeed(withURL: sourceWebFeed.url) { return UITableViewDropProposal(operation: .forbidden) } // Determine the correct drop proposal - if destIdentifier.isFolder { + if destFeed is Folder { if session.location(in: destCell).y >= 0 { return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath) } else { @@ -53,30 +51,29 @@ extension MasterFeedViewController: UITableViewDropDelegate { func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) { guard let dragItem = dropCoordinator.items.first?.dragItem, - let sourceIdentifier = dragItem.localObject as? MasterFeedTableViewIdentifier, - let sourceParentContainerID = sourceIdentifier.parentContainerID, - let source = AccountManager.shared.existingContainer(with: sourceParentContainerID), - let destIndexPath = dropCoordinator.destinationIndexPath else { - return - } + let dragNode = dragItem.localObject as? Node, + let source = dragNode.parent?.representedObject as? Container, + let destIndexPath = dropCoordinator.destinationIndexPath else { + return + } let isFolderDrop: Bool = { - if let propDestIdentifier = dataSource.itemIdentifier(for: destIndexPath), let propCell = tableView.cellForRow(at: destIndexPath) { - return propDestIdentifier.isFolder && dropCoordinator.session.location(in: propCell).y >= 0 + if coordinator.nodeFor(destIndexPath)?.representedObject is Folder, let propCell = tableView.cellForRow(at: destIndexPath) { + return dropCoordinator.session.location(in: propCell).y >= 0 } return false }() // Based on the drop we have to determine a node to start looking for a parent container. - let destIdentifier: MasterFeedTableViewIdentifier? = { + let destNode: Node? = { if isFolderDrop { - return dataSource.itemIdentifier(for: destIndexPath) + return coordinator.nodeFor(destIndexPath) } else { if destIndexPath.row == 0 { - return dataSource.itemIdentifier(for: IndexPath(row: 0, section: destIndexPath.section)) + return coordinator.nodeFor(IndexPath(row: 0, section: destIndexPath.section)) } else if destIndexPath.row > 0 { - return dataSource.itemIdentifier(for: IndexPath(row: destIndexPath.row - 1, section: destIndexPath.section)) + return coordinator.nodeFor(IndexPath(row: destIndexPath.row - 1, section: destIndexPath.section)) } else { return nil } @@ -86,25 +83,21 @@ extension MasterFeedViewController: UITableViewDropDelegate { // Now we start looking for the parent container let destinationContainer: Container? = { - if let containerID = destIdentifier?.containerID ?? destIdentifier?.parentContainerID { - return AccountManager.shared.existingContainer(with: containerID) + if let container = (destNode?.representedObject as? Container) ?? (destNode?.parent?.representedObject as? Container) { + return container } else { // If we got here, we are trying to drop on an empty section header. Go and find the Account for this section return coordinator.rootNode.childAtIndex(destIndexPath.section)?.representedObject as? Account } }() - guard let destination = destinationContainer else { return } - guard case .webFeed(_, let webFeedID) = sourceIdentifier.feedID else { return } - guard let webFeed = source.existingWebFeed(withWebFeedID: webFeedID) else { return } + guard let destination = destinationContainer, let webFeed = dragNode.representedObject as? WebFeed else { return } if source.account == destination.account { moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination) } else { moveWebFeedBetweenAccounts(feed: webFeed, sourceContainer: source, destinationContainer: destination) } - - } func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) { diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index 262f2c1ba..3bd93dcd7 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -27,14 +27,17 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } } - private let operationQueue = MainThreadOperationQueue() - lazy var dataSource = makeDataSource() - var undoableCommands = [UndoableCommand]() weak var coordinator: SceneCoordinator! private let keyboardManager = KeyboardManager(type: .sidebar) override var keyCommands: [UIKeyCommand]? { + + // If the first responder is the WKWebView (PreloadedWebView) we don't want to supply any keyboard + // commands that the system is looking for by going up the responder chain. They will interfere with + // the WKWebViews built in hardware keyboard shortcuts, specifically the up and down arrow keys. + guard let current = UIResponder.currentFirstResponder, !(current is PreloadedWebView) else { return nil } + return keyboardManager.keyCommands } @@ -57,7 +60,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { tableView.tableHeaderView = UIView(frame: frame) tableView.register(MasterFeedTableViewSectionHeader.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") - tableView.dataSource = dataSource tableView.dragDelegate = self tableView.dropDelegate = self tableView.dragInteractionEnabled = true @@ -77,7 +79,6 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { configureToolbar() becomeFirstResponder() - } override func viewWillAppear(_ animated: Bool) { @@ -86,9 +87,11 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { - IconImageCache.shared.emptyCache() super.traitCollectionDidChange(previousTraitCollection) - reloadAllVisibleCells() + if traitCollection.preferredContentSizeCategory != previousTraitCollection?.preferredContentSizeCategory { + IconImageCache.shared.emptyCache() + reloadAllVisibleCells() + } } // MARK: Notifications @@ -117,11 +120,9 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { node = coordinator.rootNode.descendantNodeRepresentingObject(representedObject as AnyObject) } - guard let unreadCountNode = node else { return } - let identifier = makeIdentifier(unreadCountNode) - if dataSource.indexPath(for: identifier) != nil { - self.reload(identifier) - } + guard let unreadCountNode = node, let indexPath = coordinator.indexPathFor(unreadCountNode) else { return } + tableView.reloadRows(at: [indexPath], with: .none) + restoreSelectionIfNecessary(adjustScroll: false) } @objc func faviconDidBecomeAvailable(_ note: Notification) { @@ -146,7 +147,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { @objc func contentSizeCategoryDidChange(_ note: Notification) { resetEstimatedRowHeight() - applyChanges(animated: false) + tableView.reloadData() } @objc func willEnterForeground(_ note: Notification) { @@ -155,6 +156,20 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { // MARK: Table View + override func numberOfSections(in tableView: UITableView) -> Int { + coordinator.numberOfSections() + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + coordinator.numberOfRows(in: section) + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell + configure(cell, indexPath) + return cell + } + override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { guard let nameProvider = coordinator.rootNode.childAtIndex(section)?.representedObject as? DisplayNameProvider else { @@ -245,13 +260,13 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { renameAction.backgroundColor = UIColor.systemOrange actions.append(renameAction) - if let identifier = dataSource.itemIdentifier(for: indexPath), identifier.isWebFeed { + if let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed { let moreTitle = NSLocalizedString("More", comment: "More") let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in if let self = self { - let alert = UIAlertController(title: identifier.nameForDisplay, message: nil, preferredStyle: .actionSheet) + let alert = UIAlertController(title: webFeed.nameForDisplay, message: nil, preferredStyle: .actionSheet) if let popoverController = alert.popoverPresentationController { popoverController.sourceView = view popoverController.sourceRect = CGRect(x: view.frame.size.width/2, y: view.frame.size.height/2, width: 1, height: 1) @@ -297,26 +312,25 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { - guard let identifier = dataSource.itemIdentifier(for: indexPath) else { + guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return nil } - if identifier.isWebFeed { - return makeWebFeedContextMenu(identifier: identifier, indexPath: indexPath, includeDeleteRename: true) - } else if identifier.isFolder { - return makeFolderContextMenu(identifier: identifier, indexPath: indexPath) - } else if identifier.isPsuedoFeed { - return makePseudoFeedContextMenu(identifier: identifier, indexPath: indexPath) + if feed is WebFeed { + return makeWebFeedContextMenu(indexPath: indexPath, includeDeleteRename: true) + } else if feed is Folder { + return makeFolderContextMenu(indexPath: indexPath) + } else if feed is PseudoFeed { + return makePseudoFeedContextMenu(indexPath: indexPath) } else { return nil } } override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { - guard let identifier = configuration.identifier as? MasterFeedTableViewIdentifier, - let indexPath = dataSource.indexPath(for: identifier), - let cell = tableView.cellForRow(at: indexPath) else { - return nil - } + guard let identifier = configuration.identifier as? MasterFeedRowIdentifier, + let cell = tableView.cellForRow(at: identifier.indexPath) else { + return nil + } return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell)) } @@ -336,21 +350,16 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { return coordinator.cappedIndexPath(proposedDestinationIndexPath) }() - guard let draggedIdentifier = dataSource.itemIdentifier(for: sourceIndexPath), - let draggedFeedID = draggedIdentifier.feedID, - let draggedNode = coordinator.nodeFor(feedID: draggedFeedID) else { + guard let draggedNode = coordinator.nodeFor(sourceIndexPath) else { assertionFailure("This should never happen") return sourceIndexPath } // If there is no destination node, we are dragging onto an empty Account - guard let destIdentifier = dataSource.itemIdentifier(for: destIndexPath), - let destFeedID = destIdentifier.feedID, - let destNode = coordinator.nodeFor(feedID: destFeedID), - let destParentContainerID = destIdentifier.parentContainerID, - let destParentNode = coordinator.nodeFor(containerID: destParentContainerID) else { - return proposedDestinationIndexPath - } + guard let destNode = coordinator.nodeFor(destIndexPath), + let destParentNode = destNode.parent else { + return proposedDestinationIndexPath + } // If this is a folder, let the users drop on it if destNode.representedObject is Folder { @@ -375,8 +384,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { if destParentNode.representedObject is Account { return IndexPath(row: 0, section: destIndexPath.section) } else { - let identifier = makeIdentifier(sortedNodes[index]) - if let candidateIndexPath = dataSource.indexPath(for: identifier) { + if let candidateIndexPath = coordinator.indexPathFor(sortedNodes[index]) { let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0 return IndexPath(row: candidateIndexPath.row - movementAdjustment, section: candidateIndexPath.section) } else { @@ -387,8 +395,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } else { if index >= sortedNodes.count { - let identifier = makeIdentifier(sortedNodes[sortedNodes.count - 1]) - if let lastSortedIndexPath = dataSource.indexPath(for: identifier) { + if let lastSortedIndexPath = coordinator.indexPathFor(sortedNodes[sortedNodes.count - 1]) { let movementAdjustment = sourceIndexPath > destIndexPath ? 1 : 0 return IndexPath(row: lastSortedIndexPath.row + movementAdjustment, section: lastSortedIndexPath.section) } else { @@ -396,8 +403,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } } else { let movementAdjustment = sourceIndexPath < destIndexPath ? 1 : 0 - let identifer = makeIdentifier(sortedNodes[index - movementAdjustment]) - return dataSource.indexPath(for: identifer) ?? sourceIndexPath + return coordinator.indexPathFor(sortedNodes[index - movementAdjustment]) ?? sourceIndexPath } } @@ -509,35 +515,23 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } @objc func expandSelectedRows(_ sender: Any?) { - if let indexPath = coordinator.currentFeedIndexPath, let containerID = dataSource.itemIdentifier(for: indexPath)?.containerID { - coordinator.expand(containerID) - self.applyChanges(animated: true) { - self.reloadAllVisibleCells() - } + if let indexPath = coordinator.currentFeedIndexPath, let node = coordinator.nodeFor(indexPath) { + coordinator.expand(node) } } @objc func collapseSelectedRows(_ sender: Any?) { - if let indexPath = coordinator.currentFeedIndexPath, let containerID = dataSource.itemIdentifier(for: indexPath)?.containerID { - coordinator.collapse(containerID) - self.applyChanges(animated: true) { - self.reloadAllVisibleCells() - } + if let indexPath = coordinator.currentFeedIndexPath, let node = coordinator.nodeFor(indexPath) { + coordinator.collapse(node) } } @objc func expandAll(_ sender: Any?) { coordinator.expandAllSectionsAndFolders() - self.applyChanges(animated: true) { - self.reloadAllVisibleCells() - } } @objc func collapseAllExceptForGroupItems(_ sender: Any?) { coordinator.collapseAllFolders() - self.applyChanges(animated: true) { - self.reloadAllVisibleCells() - } } @objc func markAllAsRead(_ sender: Any) { @@ -568,23 +562,64 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner { } func updateFeedSelection(animations: Animations) { - operationQueue.add(UpdateSelectionOperation(coordinator: coordinator, dataSource: dataSource, tableView: tableView, animations: animations)) - } - - func reloadFeeds(initialLoad: Bool, completion: (() -> Void)? = nil) { - updateUI() - - // We have to reload all the visible cells because if we got here by doing a table cell move, - // then the table itself is in a weird state. This is because we do unusual things like allowing - // drops on a "folder" that should cause the dropped cell to disappear. - applyChanges(animated: !initialLoad) { [weak self] in - if !initialLoad { - self?.reloadAllVisibleCells(completion: completion) - } else { - completion?() + if let indexPath = coordinator.currentFeedIndexPath { + tableView.selectRowAndScrollIfNotVisible(at: indexPath, animations: animations) + } else { + if let indexPath = tableView.indexPathForSelectedRow { + if animations.contains(.select) { + tableView.deselectRow(at: indexPath, animated: true) + } else { + tableView.deselectRow(at: indexPath, animated: false) + } } } } + + func reloadFeeds(initialLoad: Bool, changes: ShadowTableChanges, completion: (() -> Void)? = nil) { + updateUI() + + guard !initialLoad else { + tableView.reloadData() + completion?() + return + } + + tableView.performBatchUpdates { + if let deletes = changes.deletes, !deletes.isEmpty { + tableView.deleteSections(IndexSet(deletes), with: .middle) + } + + if let inserts = changes.inserts, !inserts.isEmpty { + tableView.insertSections(IndexSet(inserts), with: .middle) + } + + if let moves = changes.moves, !moves.isEmpty { + for move in moves { + tableView.moveSection(move.from, toSection: move.to) + } + } + + if let rowChanges = changes.rowChanges { + for rowChange in rowChanges { + if let deletes = rowChange.deleteIndexPaths, !deletes.isEmpty { + tableView.deleteRows(at: deletes, with: .middle) + } + + if let inserts = rowChange.insertIndexPaths, !inserts.isEmpty { + tableView.insertRows(at: inserts, with: .middle) + } + + if let moves = rowChange.moveIndexPaths, !moves.isEmpty { + for move in moves { + tableView.moveRow(at: move.0, to: move.1) + } + } + } + } + } + + completion?() + } func updateUI() { if coordinator.isReadFeedsFiltered { @@ -747,65 +782,6 @@ private extension MasterFeedViewController { filterButton?.accLabelText = NSLocalizedString("Filter Read Feeds", comment: "Filter Read Feeds") } - func makeIdentifier(_ node: Node) -> MasterFeedTableViewIdentifier { - let unreadCount = coordinator.unreadCountFor(node) - return MasterFeedTableViewIdentifier(node: node, unreadCount: unreadCount) - } - - func reload(_ identifier: MasterFeedTableViewIdentifier) { - var snapshot = dataSource.snapshot() - snapshot.reloadItems([identifier]) - queueApply(snapshot: snapshot, animatingDifferences: false) { [weak self] in - self?.restoreSelectionIfNecessary(adjustScroll: false) - } - } - - func applyChanges(animated: Bool, adjustScroll: Bool = false, completion: (() -> Void)? = nil) { - var snapshot = NSDiffableDataSourceSnapshot() - let sectionIdentifiers = Array(0...coordinator.rootNode.childNodes.count - 1) - snapshot.appendSections(sectionIdentifiers) - - for sectionIdentifer in sectionIdentifiers { - let identifiers = coordinator.shadowNodesFor(section: sectionIdentifer).map { makeIdentifier($0) } - snapshot.appendItems(identifiers, toSection: sectionIdentifer) - } - - queueApply(snapshot: snapshot, animatingDifferences: animated) { [weak self] in - self?.restoreSelectionIfNecessary(adjustScroll: adjustScroll) - completion?() - } - } - - func queueApply(snapshot: NSDiffableDataSourceSnapshot, animatingDifferences: Bool = true, completion: (() -> Void)? = nil) { - let operation = MasterFeedDataSourceOperation(dataSource: dataSource, snapshot: snapshot, animating: animatingDifferences) - operation.completionBlock = { [weak self] _ in - self?.enableTableViewSelection() - completion?() - } - disableTableViewSelectionIfNecessary() - operationQueue.add(operation) - } - - private func disableTableViewSelectionIfNecessary() { - // We only need to disable tableView selection if the feeds are filtered by unread - guard coordinator.isReadFeedsFiltered else { return } - tableView.allowsSelection = false - } - - private func enableTableViewSelection() { - tableView.allowsSelection = true - } - - func makeDataSource() -> MasterFeedDataSource { - let dataSource = MasterFeedDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, cellContents in - let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) as! MasterFeedTableViewCell - self?.configure(cell, cellContents) - return cell - }) - dataSource.defaultRowAnimation = .middle - return dataSource - } - func resetEstimatedRowHeight() { let titleLabel = NonIntrinsicLabel() titleLabel.text = "But I must explain" @@ -817,27 +793,30 @@ private extension MasterFeedViewController { tableView.estimatedRowHeight = layout.height } - func configure(_ cell: MasterFeedTableViewCell, _ identifier: MasterFeedTableViewIdentifier) { - + func configure(_ cell: MasterFeedTableViewCell, _ indexPath: IndexPath) { + guard let node = coordinator.nodeFor(indexPath) else { return } + cell.delegate = self - if identifier.isFolder { + if node.representedObject is Folder { cell.indentationLevel = 0 } else { cell.indentationLevel = 1 } - if let containerID = identifier.containerID { + if let containerID = (node.representedObject as? Container)?.containerID { cell.setDisclosure(isExpanded: coordinator.isExpanded(containerID), animated: false) cell.isDisclosureAvailable = true } else { cell.isDisclosureAvailable = false } - cell.name = identifier.nameForDisplay - cell.unreadCount = identifier.unreadCount - configureIcon(cell, identifier) - - guard let indexPath = dataSource.indexPath(for: identifier) else { return } + if let feed = node.representedObject as? Feed { + cell.name = feed.nameForDisplay + cell.unreadCount = feed.unreadCount + } + + configureIcon(cell, indexPath) + let rowsInSection = tableView.numberOfRows(inSection: indexPath.section) if indexPath.row == rowsInSection - 1 { cell.isSeparatorShown = false @@ -847,8 +826,8 @@ private extension MasterFeedViewController { } - func configureIcon(_ cell: MasterFeedTableViewCell, _ identifier: MasterFeedTableViewIdentifier) { - guard let feedID = identifier.feedID else { + func configureIcon(_ cell: MasterFeedTableViewCell, _ indexPath: IndexPath) { + guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? Feed, let feedID = feed.feedID else { return } cell.iconImage = IconImageCache.shared.imageFor(feedID) @@ -865,35 +844,30 @@ private extension MasterFeedViewController { applyToCellsForRepresentedObject(representedObject, configure) } - func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ completion: (MasterFeedTableViewCell, MasterFeedTableViewIdentifier) -> Void) { - applyToAvailableCells { (cell, identifier) in - if let representedFeed = representedObject as? Feed, representedFeed.feedID == identifier.feedID { - completion(cell, identifier) + func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ completion: (MasterFeedTableViewCell, IndexPath) -> Void) { + applyToAvailableCells { (cell, indexPath) in + if let node = coordinator.nodeFor(indexPath), + let representedFeed = representedObject as? Feed, + let candidate = node.representedObject as? Feed, + representedFeed.feedID == candidate.feedID { + completion(cell, indexPath) } } } - func applyToAvailableCells(_ completion: (MasterFeedTableViewCell, MasterFeedTableViewIdentifier) -> Void) { + func applyToAvailableCells(_ completion: (MasterFeedTableViewCell, IndexPath) -> Void) { tableView.visibleCells.forEach { cell in - guard let indexPath = tableView.indexPath(for: cell), let identifier = dataSource.itemIdentifier(for: indexPath) else { + guard let indexPath = tableView.indexPath(for: cell) else { return } - completion(cell as! MasterFeedTableViewCell, identifier) + completion(cell as! MasterFeedTableViewCell, indexPath) } } private func reloadAllVisibleCells(completion: (() -> Void)? = nil) { - let visibleNodes = tableView.indexPathsForVisibleRows!.compactMap { return dataSource.itemIdentifier(for: $0) } - reloadCells(visibleNodes, completion: completion) - } - - private func reloadCells(_ identifiers: [MasterFeedTableViewIdentifier], completion: (() -> Void)? = nil) { - var snapshot = dataSource.snapshot() - snapshot.reloadItems(identifiers) - queueApply(snapshot: snapshot, animatingDifferences: false) { [weak self] in - self?.restoreSelectionIfNecessary(adjustScroll: false) - completion?() - } + guard let indexPaths = tableView.indexPathsForVisibleRows else { return } + tableView.reloadRows(at: indexPaths, with: .none) + restoreSelectionIfNecessary(adjustScroll: false) } private func accountForNode(_ node: Node) -> Account? { @@ -917,32 +891,28 @@ private extension MasterFeedViewController { if coordinator.isExpanded(sectionNode) { headerView.disclosureExpanded = false coordinator.collapse(sectionNode) - self.applyChanges(animated: true) } else { headerView.disclosureExpanded = true coordinator.expand(sectionNode) - self.applyChanges(animated: true) } } func expand(_ cell: MasterFeedTableViewCell) { - guard let indexPath = tableView.indexPath(for: cell), let containerID = dataSource.itemIdentifier(for: indexPath)?.containerID else { + guard let indexPath = tableView.indexPath(for: cell), let node = coordinator.nodeFor(indexPath) else { return } - coordinator.expand(containerID) - applyChanges(animated: true) + coordinator.expand(node) } func collapse(_ cell: MasterFeedTableViewCell) { - guard let indexPath = tableView.indexPath(for: cell), let containerID = dataSource.itemIdentifier(for: indexPath)?.containerID else { + guard let indexPath = tableView.indexPath(for: cell), let node = coordinator.nodeFor(indexPath) else { return } - coordinator.collapse(containerID) - applyChanges(animated: true) + coordinator.collapse(node) } - func makeWebFeedContextMenu(identifier: MasterFeedTableViewIdentifier, indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration { - return UIContextMenuConfiguration(identifier: identifier as NSCopying, previewProvider: nil, actionProvider: { [ weak self] suggestedActions in + func makeWebFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration { + return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [ weak self] suggestedActions in guard let self = self else { return nil } @@ -986,8 +956,8 @@ private extension MasterFeedViewController { } - func makeFolderContextMenu(identifier: MasterFeedTableViewIdentifier, indexPath: IndexPath) -> UIContextMenuConfiguration { - return UIContextMenuConfiguration(identifier: identifier as NSCopying, previewProvider: nil, actionProvider: { [weak self] suggestedActions in + func makeFolderContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration { + return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [weak self] suggestedActions in guard let self = self else { return nil } @@ -1009,12 +979,12 @@ private extension MasterFeedViewController { }) } - func makePseudoFeedContextMenu(identifier: MasterFeedTableViewIdentifier, indexPath: IndexPath) -> UIContextMenuConfiguration? { + func makePseudoFeedContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration? { guard let markAllAction = self.markAllAsReadAction(indexPath: indexPath) else { return nil } - return UIContextMenuConfiguration(identifier: identifier as NSCopying, previewProvider: nil, actionProvider: { suggestedActions in + return UIContextMenuConfiguration(identifier: MasterFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { suggestedActions in return UIMenu(title: "", children: [markAllAction]) }) } @@ -1045,11 +1015,10 @@ private extension MasterFeedViewController { } func copyFeedPageAction(indexPath: IndexPath) -> UIAction? { - guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, - let webFeed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed, - let url = URL(string: webFeed.url) else { - return nil - } + guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed, + let url = URL(string: webFeed.url) else { + return nil + } let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL") let action = UIAction(title: title, image: AppAssets.copyImage) { action in @@ -1059,12 +1028,11 @@ private extension MasterFeedViewController { } func copyFeedPageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { - guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, - let webFeed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed, - let url = URL(string: webFeed.url) else { - return nil - } - + guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed, + let url = URL(string: webFeed.url) else { + return nil + } + let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL") let action = UIAlertAction(title: title, style: .default) { action in UIPasteboard.general.url = url @@ -1074,12 +1042,11 @@ private extension MasterFeedViewController { } func copyHomePageAction(indexPath: IndexPath) -> UIAction? { - guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, - let webFeed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed, - let homePageURL = webFeed.homePageURL, - let url = URL(string: homePageURL) else { - return nil - } + guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed, + let homePageURL = webFeed.homePageURL, + let url = URL(string: homePageURL) else { + return nil + } let title = NSLocalizedString("Copy Home Page URL", comment: "Copy Home Page URL") let action = UIAction(title: title, image: AppAssets.copyImage) { action in @@ -1089,13 +1056,12 @@ private extension MasterFeedViewController { } func copyHomePageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { - guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, - let webFeed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed, - let homePageURL = webFeed.homePageURL, - let url = URL(string: homePageURL) else { - return nil - } - + guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed, + let homePageURL = webFeed.homePageURL, + let url = URL(string: homePageURL) else { + return nil + } + let title = NSLocalizedString("Copy Home Page URL", comment: "Copy Home Page URL") let action = UIAlertAction(title: title, style: .default) { action in UIPasteboard.general.url = url @@ -1105,16 +1071,14 @@ private extension MasterFeedViewController { } func markAllAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { - guard let identifier = dataSource.itemIdentifier(for: indexPath), - identifier.unreadCount > 0, - let feedID = identifier.feedID, - let feed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed, - let articles = try? feed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { + guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed, + webFeed.unreadCount > 0, + let articles = try? webFeed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { return nil } let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") - let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String + let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, webFeed.nameForDisplay) as String let cancel = { completion(true) } @@ -1147,13 +1111,13 @@ private extension MasterFeedViewController { } func getInfoAction(indexPath: IndexPath) -> UIAction? { - guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, let feed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed else { + guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed else { return nil } let title = NSLocalizedString("Get Info", comment: "Get Info") let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] action in - self?.coordinator.showFeedInspector(for: feed) + self?.coordinator.showFeedInspector(for: webFeed) } return action } @@ -1175,41 +1139,25 @@ private extension MasterFeedViewController { } func getInfoAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { - guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, let feed = AccountManager.shared.existingFeed(with: feedID) as? WebFeed else { + guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed else { return nil } let title = NSLocalizedString("Get Info", comment: "Get Info") let action = UIAlertAction(title: title, style: .default) { [weak self] action in - self?.coordinator.showFeedInspector(for: feed) + self?.coordinator.showFeedInspector(for: webFeed) completion(true) } return action } func markAllAsReadAction(indexPath: IndexPath) -> UIAction? { - guard let identifier = dataSource.itemIdentifier(for: indexPath), identifier.unreadCount > 0 else { - return nil - } + guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed, + let contentView = self.tableView.cellForRow(at: indexPath)?.contentView, + feed.unreadCount > 0 else { + return nil + } - var smartFeed: Feed? - if identifier.isPsuedoFeed { - if SmartFeedsController.shared.todayFeed.feedID == identifier.feedID { - smartFeed = SmartFeedsController.shared.todayFeed - } else if SmartFeedsController.shared.unreadFeed.feedID == identifier.feedID { - smartFeed = SmartFeedsController.shared.unreadFeed - } else if SmartFeedsController.shared.starredFeed.feedID == identifier.feedID { - smartFeed = SmartFeedsController.shared.starredFeed - } - } - - guard let feedID = identifier.feedID, - let feed = smartFeed ?? AccountManager.shared.existingFeed(with: feedID), - feed.unreadCount > 0, - let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { - return nil - } - let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in @@ -1246,11 +1194,10 @@ private extension MasterFeedViewController { func rename(indexPath: IndexPath) { - guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, let feed = AccountManager.shared.existingFeed(with: feedID) else { return } + guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return } - let name = dataSource.itemIdentifier(for: indexPath)?.nameForDisplay ?? "" let formatString = NSLocalizedString("Rename “%@”", comment: "Rename feed") - let title = NSString.localizedStringWithFormat(formatString as NSString, name) as String + let title = NSString.localizedStringWithFormat(formatString as NSString, feed.nameForDisplay) as String let alertController = UIAlertController(title: title, message: nil, preferredStyle: .alert) @@ -1290,7 +1237,7 @@ private extension MasterFeedViewController { alertController.preferredAction = renameAction alertController.addTextField() { textField in - textField.text = name + textField.text = feed.nameForDisplay textField.placeholder = NSLocalizedString("Name", comment: "Name") } @@ -1301,7 +1248,7 @@ private extension MasterFeedViewController { } func delete(indexPath: IndexPath) { - guard let feedID = dataSource.itemIdentifier(for: indexPath)?.feedID, let feed = AccountManager.shared.existingFeed(with: feedID) else { return } + guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return } let title: String let message: String @@ -1322,7 +1269,7 @@ private extension MasterFeedViewController { let deleteTitle = NSLocalizedString("Delete", comment: "Delete") let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { [weak self] action in - self?.delete(indexPath: indexPath, feedID: feedID) + self?.performDelete(indexPath: indexPath) } alertController.addAction(deleteAction) alertController.preferredAction = deleteAction @@ -1330,9 +1277,9 @@ private extension MasterFeedViewController { self.present(alertController, animated: true) } - func delete(indexPath: IndexPath, feedID: FeedIdentifier) { + func performDelete(indexPath: IndexPath) { guard let undoManager = undoManager, - let deleteNode = coordinator.nodeFor(feedID: feedID), + let deleteNode = coordinator.nodeFor(indexPath), let deleteCommand = DeleteCommand(nodesToDelete: [deleteNode], undoManager: undoManager, errorHandler: ErrorHandler.present(self)) else { return } diff --git a/iOS/MasterFeed/ShadowTableChanges.swift b/iOS/MasterFeed/ShadowTableChanges.swift new file mode 100644 index 000000000..dd8a12801 --- /dev/null +++ b/iOS/MasterFeed/ShadowTableChanges.swift @@ -0,0 +1,70 @@ +// +// ShadowTableChanges.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 10/20/21. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import Foundation + +struct ShadowTableChanges { + + struct Move: Hashable { + var from: Int + var to: Int + + init(_ from: Int, _ to: Int) { + self.from = from + self.to = to + } + } + + struct RowChanges { + + var section: Int + var deletes: Set? + var inserts: Set? + var moves: Set? + + var isEmpty: Bool { + return (deletes?.isEmpty ?? true) && (inserts?.isEmpty ?? true) && (moves?.isEmpty ?? true) + } + + var deleteIndexPaths: [IndexPath]? { + guard let deletes = deletes else { return nil } + return deletes.map { IndexPath(row: $0, section: section) } + } + + var insertIndexPaths: [IndexPath]? { + guard let inserts = inserts else { return nil } + return inserts.map { IndexPath(row: $0, section: section) } + } + + var moveIndexPaths: [(IndexPath, IndexPath)]? { + guard let moves = moves else { return nil } + return moves.map { (IndexPath(row: $0.from, section: section), IndexPath(row: $0.to, section: section)) } + } + + init(section: Int, deletes: Set?, inserts: Set?, moves: Set?) { + self.section = section + self.deletes = deletes + self.inserts = inserts + self.moves = moves + } + + } + + var deletes: Set? + var inserts: Set? + var moves: Set? + var rowChanges: [RowChanges]? + + init(deletes: Set?, inserts: Set?, moves: Set?, rowChanges: [RowChanges]?) { + self.deletes = deletes + self.inserts = inserts + self.moves = moves + self.rowChanges = rowChanges + } + +} diff --git a/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift b/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift index 49b9f9ae8..2ec879473 100644 --- a/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift +++ b/iOS/MasterTimeline/Cell/MasterTimelineTableViewCell.swift @@ -229,12 +229,13 @@ private extension MasterTimelineTableViewCell { } unreadIndicatorPropertyAnimator?.startAnimation() } else { + unreadIndicatorView.alpha = 1 showOrHideView(unreadIndicatorView, cellData.read || cellData.starred) } } func updateStarView() { - if !starView.isHidden && cellData.read && !cellData.starred { + if !starView.isHidden && cellData.read && !cellData.starred { starViewPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in self?.starView.alpha = 0 } @@ -245,6 +246,7 @@ private extension MasterTimelineTableViewCell { } starViewPropertyAnimator?.startAnimation() } else { + starView.alpha = 1 showOrHideView(starView, !cellData.starred) } } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 1a99608bd..72a019828 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -33,6 +33,12 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner private let keyboardManager = KeyboardManager(type: .timeline) override var keyCommands: [UIKeyCommand]? { + + // If the first responder is the WKWebView (PreloadedWebView) we don't want to supply any keyboard + // commands that the system is looking for by going up the responder chain. They will interfere with + // the WKWebViews built in hardware keyboard shortcuts, specifically the up and down arrow keys. + guard let current = UIResponder.currentFirstResponder, !(current is PreloadedWebView) else { return nil } + return keyboardManager.keyCommands } @@ -123,6 +129,15 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner } // MARK: Actions + + @objc func openInBrowser(_ sender: Any?) { + coordinator.showBrowserForCurrentArticle() + } + + @objc func openInAppBrowser(_ sender: Any?) { + coordinator.showInAppBrowser() + } + @IBAction func toggleFilter(_ sender: Any) { coordinator.toggleReadArticlesFilter() } @@ -362,6 +377,17 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner menuElements.append(UIMenu(title: "", options: .displayInline, children: secondaryActions)) } + var copyActions = [UIAction]() + if let action = self.copyArticleURLAction(article) { + copyActions.append(action) + } + if let action = self.copyExternalURLAction(article) { + copyActions.append(action) + } + if !copyActions.isEmpty { + menuElements.append(UIMenu(title: "", options: .displayInline, children: copyActions)) + } + if let action = self.openInBrowserAction(article) { menuElements.append(UIMenu(title: "", options: .displayInline, children: [action])) } @@ -662,7 +688,7 @@ private extension MasterTimelineViewController { func updateTitleUnreadCount() { if let titleView = navigationItem.titleView as? MasterTimelineTitleView { - titleView.unreadCountView.unreadCount = coordinator.unreadCount + titleView.unreadCountView.unreadCount = coordinator.timelineUnreadCount } } @@ -713,7 +739,7 @@ private extension MasterTimelineViewController { } func featuredImageFor(_ article: Article) -> UIImage? { - if let url = article.imageURL, let data = appDelegate.imageDownloader.image(for: url) { + if let link = article.imageLink, let data = appDelegate.imageDownloader.image(for: link) { return RSImage(data: data) } return nil @@ -887,6 +913,25 @@ private extension MasterTimelineViewController { } return action } + + func copyArticleURLAction(_ article: Article) -> UIAction? { + guard let url = article.preferredURL else { return nil } + let title = NSLocalizedString("Copy Article URL", comment: "Copy Article URL") + let action = UIAction(title: title, image: AppAssets.copyImage) { action in + UIPasteboard.general.url = url + } + return action + } + + func copyExternalURLAction(_ article: Article) -> UIAction? { + guard let externalLink = article.externalLink, externalLink != article.preferredLink, let url = URL(string: externalLink) else { return nil } + let title = NSLocalizedString("Copy External URL", comment: "Copy External URL") + let action = UIAction(title: title, image: AppAssets.copyImage) { action in + UIPasteboard.general.url = url + } + return action + } + func openInBrowserAction(_ article: Article) -> UIAction? { guard let _ = article.preferredURL else { return nil } @@ -899,6 +944,7 @@ private extension MasterTimelineViewController { func openInBrowserAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? { guard let _ = article.preferredURL else { return nil } + let title = NSLocalizedString("Open in Browser", comment: "Open in Browser") let action = UIAlertAction(title: title, style: .default) { [weak self] action in self?.coordinator.showBrowserForArticle(article) diff --git a/iOS/Resources/Credits.rtf b/iOS/Resources/Credits.rtf index 828314616..1124bdb6f 100644 --- a/iOS/Resources/Credits.rtf +++ b/iOS/Resources/Credits.rtf @@ -13,5 +13,5 @@ NewsBlur syncing: {\field{\*\fldinst{HYPERLINK "https://twitter.com/quanganhdo"} Under-the-hood magic and CSS stylin\'92s: {\field{\*\fldinst{HYPERLINK "https://github.com/wevah"}}{\fldrslt Nate Weaver}}\ Newsfoot (JS footnote displayer): {\field{\*\fldinst{HYPERLINK "https://github.com/brehaut/"}}{\fldrslt Andrew Brehaut}}\ Help book: {\field{\*\fldinst{HYPERLINK "https://nostodnayr.net/"}}{\fldrslt Ryan Dotson}}\ -And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://github.com/danielpunkass"}}{\fldrslt Daniel Jalkut}}, {\field{\*\fldinst{HYPERLINK "https://rhonabwy.com/"}}{\fldrslt Joe Heck}}, {\field{\*\fldinst{HYPERLINK "https://github.com/olofhellman"}}{\fldrslt Olof Hellman}}, {\field{\*\fldinst{HYPERLINK "https://blog.rizwan.dev/"}}{\fldrslt Rizwan Mohamed Ibrahim}}, {\field{\*\fldinst{HYPERLINK "https://stuartbreckenridge.com/"}}{\fldrslt Stuart Breckenridge}}, {\field{\*\fldinst{HYPERLINK "https://twitter.com/philviso"}}{\fldrslt Phil Viso}}, and {\field{\*\fldinst{HYPERLINK "https://github.com/Ranchero-Software/NetNewsWire/graphs/contributors"}}{\fldrslt many more}}!\ -} \ No newline at end of file +And featuring contributions from {\field{\*\fldinst{HYPERLINK "https://github.com/danielpunkass"}}{\fldrslt Daniel Jalkut}}, {\field{\*\fldinst{HYPERLINK "https://rhonabwy.com/"}}{\fldrslt Joe Heck}}, {\field{\*\fldinst{HYPERLINK "https://github.com/olofhellman"}}{\fldrslt Olof Hellman}}, {\field{\*\fldinst{HYPERLINK "https://blog.rizwan.dev/"}}{\fldrslt Rizwan Mohamed Ibrahim}}, {\field{\*\fldinst{HYPERLINK "https://mynameisstuart.com/"}}{\fldrslt Stuart Breckenridge}}, {\field{\*\fldinst{HYPERLINK "https://twitter.com/philviso"}}{\fldrslt Phil Viso}}, and {\field{\*\fldinst{HYPERLINK "https://github.com/Ranchero-Software/NetNewsWire/graphs/contributors"}}{\fldrslt many more}}!\ +} diff --git a/iOS/Resources/Info.plist b/iOS/Resources/Info.plist index 2ccfb374d..9489e575e 100644 --- a/iOS/Resources/Info.plist +++ b/iOS/Resources/Info.plist @@ -2,10 +2,6 @@ - OrganizationIdentifier - $(ORGANIZATION_IDENTIFIER) - DeveloperEntitlements - $(DEVELOPER_ENTITLEMENTS) AppGroup group.$(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS AppIdentifierPrefix @@ -54,6 +50,8 @@ CFBundleVersion $(CURRENT_PROJECT_VERSION) + DeveloperEntitlements + $(DEVELOPER_ENTITLEMENTS) LSApplicationQueriesSchemes mailto @@ -76,6 +74,8 @@ Restoration SelectFeed + OrganizationIdentifier + $(ORGANIZATION_IDENTIFIER) UIApplicationSceneManifest UIApplicationSupportsMultipleScenes @@ -175,7 +175,45 @@ + + UTTypeConformsTo + + com.apple.package + + UTTypeDescription + NetNewsWire Theme + UTTypeIdentifier + com.ranchero.netnewswire.theme + UTTypeTagSpecification + + public.filename-extension + + nnwtheme + + + + CFBundleDocumentTypes + + + CFBundleTypeExtensions + + nnwtheme + + CFBundleTypeName + NetNewsWire Theme + CFBundleTypeRole + Viewer + LSItemContentTypes + + com.ranchero.netnewswire.theme + + LSTypeIsPackage + + + + LSSupportsOpeningDocumentsInPlace + UserAgent NetNewsWire (RSS Reader; https://netnewswire.com/) diff --git a/iOS/Resources/styleSheet.css b/iOS/Resources/styleSheet.css deleted file mode 100644 index bf5f31391..000000000 --- a/iOS/Resources/styleSheet.css +++ /dev/null @@ -1,66 +0,0 @@ -body { - margin-top: 3px; - margin-bottom: 20px; - padding-left: 20px; - padding-right: 20px; - - word-break: break-word; - -webkit-hyphens: auto; - -webkit-text-size-adjust: none; -} - -:root { - color-scheme: light dark; - font: -apple-system-body; - font-size: [[font-size]]px; - --primary-accent-color: #086AEE; - --secondary-accent-color: #086AEE; - --block-quote-border-color: rgba(8, 106, 238, 0.75); -} - -@media(prefers-color-scheme: dark) { - :root { - --primary-accent-color: #2D80F1; - --secondary-accent-color: #5E9EF4; - --block-quote-border-color: rgba(94, 158, 244, 0.75); - --header-table-border-color: rgba(255, 255, 255, 0.2); - } -} - -body a, body a:visited { - color: var(--secondary-accent-color); -} -body .header { - font: -apple-system-body; - font-size: [[font-size]]px; -} -body .header a:link, body .header a:visited { - color: var(--primary-accent-color); -} - -.avatar img { - border-radius: 4px; -} - -pre { - border: 1px solid var(--secondary-accent-color); - padding: 5px; -} - -.nnw-overflow table { - border: 1px solid var(--secondary-accent-color); -} - -.activityIndicatorWrap { - position: relative; -} - -.activityIndicator { - z-index: 1; - width: 64px; - height: 64px; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%); -} diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 7bc4b4037..2f411e3be 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -19,6 +19,7 @@ enum PanelMode { case three case standard } + enum SearchScope: Int { case timeline = 0 case global = 1 @@ -30,7 +31,21 @@ enum ShowFeedName { case feed } -class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { +struct FeedNode: Hashable { + var node: Node + var feedID: FeedIdentifier + + init(_ node: Node) { + self.node = node + self.feedID = (node.representedObject as! Feed).feedID! + } + + func hash(into hasher: inout Hasher) { + hasher.combine(feedID) + } +} + +class SceneCoordinator: NSObject, UndoableCommandRunner { var undoableCommands = [UndoableCommand]() var undoManager: UndoManager? { @@ -72,10 +87,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { private var fetchSerialNumber = 0 private let fetchRequestQueue = FetchRequestQueue() - private var animatingChanges = false private var expandedTable = Set() private var readFilterEnabledTable = [FeedIdentifier: Bool]() - private var shadowTable = [[Node]]() + private var shadowTable = [(sectionID: String, feedNodes: [FeedNode])]() private(set) var preSearchTimelineFeed: Feed? private var lastSearchString = "" @@ -110,8 +124,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { var stateRestorationActivity: NSUserActivity { let activity = activityManager.stateRestorationActivity - var userInfo = activity.userInfo == nil ? [AnyHashable: Any]() : activity.userInfo - userInfo![UserInfoKey.windowState] = windowState() + var userInfo = activity.userInfo ?? [AnyHashable: Any]() + + userInfo[UserInfoKey.windowState] = windowState() + + let articleState = articleViewController?.currentState + userInfo[UserInfoKey.isShowingExtractedArticle] = articleState?.isShowingExtractedArticle ?? false + userInfo[UserInfoKey.articleWindowScrollY] = articleState?.windowScrollY ?? 0 + activity.userInfo = userInfo return activity } @@ -170,8 +190,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { let prevIndexPath: IndexPath? = { if indexPath.row - 1 < 0 { for i in (0.. 0 { - return IndexPath(row: shadowTable[i].count - 1, section: i) + if shadowTable[i].feedNodes.count > 0 { + return IndexPath(row: shadowTable[i].feedNodes.count - 1, section: i) } } return nil @@ -189,9 +209,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } let nextIndexPath: IndexPath? = { - if indexPath.row + 1 >= shadowTable[indexPath.section].count { + if indexPath.row + 1 >= shadowTable[indexPath.section].feedNodes.count { for i in indexPath.section + 1.. 0 { + if shadowTable[i].feedNodes.count > 0 { return IndexPath(row: 0, section: i) } } @@ -265,20 +285,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } var isTimelineUnreadAvailable: Bool { - return unreadCount > 0 + return timelineUnreadCount > 0 } var isAnyUnreadAvailable: Bool { return appDelegate.unreadCount > 0 } - var unreadCount: Int = 0 { - didSet { - if unreadCount != oldValue { - postUnreadCountDidChangeNotification() - } - } - } + var timelineUnreadCount: Int = 0 override init() { treeController = TreeController(delegate: treeControllerDelegate) @@ -287,7 +301,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { for sectionNode in treeController.rootNode.childNodes { markExpanded(sectionNode) - shadowTable.append([Node]()) + shadowTable.append((sectionID: "", feedNodes: [FeedNode]())) } NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidInitialize(_:)), name: .UnreadCountDidInitialize, object: nil) @@ -303,7 +317,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) - + NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(themeDownloadDidFail(_:)), name: .didFailToImportThemeWithError, object: nil) } func start(for size: CGSize) -> UIViewController { @@ -436,8 +451,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { guard notification.object is AccountManager else { return } - rebuildBackingStores(initialLoad: true) - treeControllerDelegate.resetFilterExceptions() + + if isReadFeedsFiltered { + rebuildBackingStores() + } } @objc func unreadCountDidChange(_ note: Notification) { @@ -473,23 +490,14 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } @objc func accountStateDidChange(_ note: Notification) { - let expandNewlyActivatedAccount = { - if let account = note.userInfo?[Account.UserInfoKey.account] as? Account, - account.isActive, - let node = self.treeController.rootNode.childNodeRepresentingObject(account) { - self.markExpanded(node) - } - } - if timelineFetcherContainsAnyPseudoFeed() { fetchAndMergeArticlesAsync(animated: true) { self.masterTimelineViewController?.reinitializeArticles(resetScroll: false) - self.rebuildBackingStores(updateExpandedNodes: expandNewlyActivatedAccount) + self.rebuildBackingStores() } } else { - self.rebuildBackingStores(updateExpandedNodes: expandNewlyActivatedAccount) + self.rebuildBackingStores() } - } @objc func userDidAddAccount(_ note: Notification) { @@ -559,6 +567,27 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { queueFetchAndMergeArticles() } } + + @objc func importDownloadedTheme(_ note: Notification) { + guard let userInfo = note.userInfo, + let url = userInfo["url"] as? URL else { + return + } + + DispatchQueue.main.async { + self.importTheme(filename: url.path) + } + } + + @objc func themeDownloadDidFail(_ note: Notification) { + guard let userInfo = note.userInfo, + let error = userInfo["error"] as? Error else { + return + } + DispatchQueue.main.async { + self.rootSplitViewController.presentError(error, dismiss: nil) + } + } // MARK: API @@ -600,20 +629,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { refreshTimeline(resetScroll: false) } - - func shadowNodesFor(section: Int) -> [Node] { - return shadowTable[section] - } - - func nodeFor(containerID: ContainerIdentifier) -> Node? { - return treeController.rootNode.descendantNode(where: { node in - if let container = node.representedObject as? Container { - return container.containerID == containerID - } else { - return false - } - }) - } func nodeFor(feedID: FeedIdentifier) -> Node? { return treeController.rootNode.descendantNode(where: { node in @@ -625,13 +640,37 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { }) } + func numberOfSections() -> Int { + return shadowTable.count + } + + func numberOfRows(in section: Int) -> Int { + return shadowTable[section].feedNodes.count + } + + func nodeFor(_ indexPath: IndexPath) -> Node? { + guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].feedNodes.count else { + return nil + } + return shadowTable[indexPath.section].feedNodes[indexPath.row].node + } + + func indexPathFor(_ node: Node) -> IndexPath? { + for i in 0.. Article? { return idToAticleDictionary[articleID] } func cappedIndexPath(_ indexPath: IndexPath) -> IndexPath { - guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].count else { - return IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1) + guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].feedNodes.count else { + return IndexPath(row: shadowTable[shadowTable.count - 1].feedNodes.count - 1, section: shadowTable.count - 1) } return indexPath } @@ -639,7 +678,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func unreadCountFor(_ node: Node) -> Int { // The coordinator supplies the unread count for the currently selected feed if node.representedObject === timelineFeed as AnyObject { - return unreadCount + return timelineUnreadCount } if let unreadCountProvider = node.representedObject as? UnreadCountProvider { return unreadCountProvider.unreadCount @@ -677,9 +716,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func expand(_ containerID: ContainerIdentifier) { markExpanded(containerID) - animatingChanges = true - rebuildShadowTable() - animatingChanges = false + rebuildBackingStores() } func expand(_ node: Node) { @@ -696,16 +733,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } } } - animatingChanges = true - rebuildShadowTable() - animatingChanges = false + rebuildBackingStores() } func collapse(_ containerID: ContainerIdentifier) { unmarkExpanded(containerID) - animatingChanges = true - rebuildShadowTable() - animatingChanges = false + rebuildBackingStores() clearTimelineIfNoLongerAvailable() } @@ -716,16 +749,13 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { func collapseAllFolders() { for sectionNode in treeController.rootNode.childNodes { - unmarkExpanded(sectionNode) for topLevelNode in sectionNode.childNodes { if topLevelNode.representedObject is Folder { unmarkExpanded(topLevelNode) } } } - animatingChanges = true - rebuildShadowTable() - animatingChanges = false + rebuildBackingStores() clearTimelineIfNoLongerAvailable() } @@ -765,6 +795,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { self.activityManager.selecting(feed: feed) self.installTimelineControllerIfNecessary(animated: animations.contains(.navigation)) setTimelineFeed(feed, animated: false) { + if self.isReadFeedsFiltered { + self.rebuildBackingStores() + } completion?() } @@ -818,7 +851,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } } - func selectArticle(_ article: Article?, animations: Animations = []) { + func selectArticle(_ article: Article?, animations: Animations = [], isShowingExtractedArticle: Bool? = nil, articleWindowScrollY: Int? = nil) { guard article != currentArticle else { return } currentArticle = article @@ -848,6 +881,9 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { masterTimelineViewController?.updateArticleSelection(animations: animations) currentArticleViewController.article = article + if let isShowingExtractedArticle = isShowingExtractedArticle, let articleWindowScrollY = articleWindowScrollY { + currentArticleViewController.restoreScrollPosition = (isShowingExtractedArticle, articleWindowScrollY) + } } func beginSearching() { @@ -1266,11 +1302,21 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { rootSplitViewController.preferredDisplayMode = rootSplitViewController.displayMode == .allVisible ? .primaryHidden : .allVisible } - func selectArticleInCurrentFeed(_ articleID: String) { + func selectArticleInCurrentFeed(_ articleID: String, isShowingExtractedArticle: Bool? = nil, articleWindowScrollY: Int? = nil) { if let article = self.articles.first(where: { $0.articleID == articleID }) { - self.selectArticle(article) + self.selectArticle(article, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY) } } + + func importTheme(filename: String) { + do { + try ArticleThemeImporter.importTheme(controller: rootSplitViewController, filename: filename) + } catch { + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error]) + } + + } + } // MARK: UISplitViewControllerDelegate @@ -1376,7 +1422,7 @@ private extension SceneCoordinator { count += 1 } } - unreadCount = count + timelineUnreadCount = count } func rebuildArticleDictionaries() { @@ -1429,8 +1475,8 @@ private extension SceneCoordinator { func addShadowTableToFilterExceptions() { for section in shadowTable { - for node in section { - if let feed = node.representedObject as? Feed, let feedID = feed.feedID { + for feedNode in section.feedNodes { + if let feed = feedNode.node.representedObject as? Feed, let feedID = feed.feedID { treeControllerDelegate.addFilterException(feedID) } } @@ -1446,52 +1492,110 @@ private extension SceneCoordinator { } func rebuildBackingStores(initialLoad: Bool = false, updateExpandedNodes: (() -> Void)? = nil, completion: (() -> Void)? = nil) { - if !animatingChanges && !BatchUpdate.shared.isPerforming { - + if !BatchUpdate.shared.isPerforming { addToFilterExeptionsIfNecessary(timelineFeed) treeController.rebuild() treeControllerDelegate.resetFilterExceptions() updateExpandedNodes?() - rebuildShadowTable() - masterFeedViewController.reloadFeeds(initialLoad: initialLoad, completion: completion) - + let changes = rebuildShadowTable() + masterFeedViewController.reloadFeeds(initialLoad: initialLoad, changes: changes, completion: completion) } } - func rebuildShadowTable() { - shadowTable = [[Node]]() + func rebuildShadowTable() -> ShadowTableChanges { + var newShadowTable = [(sectionID: String, feedNodes: [FeedNode])]() for i in 0..() + var inserts = Set() + var deletes = Set() + + let oldFeedNodes = shadowTable.first(where: { $0.sectionID == newSectionRows.sectionID })?.feedNodes ?? [FeedNode]() + + let diff = newSectionRows.feedNodes.difference(from: oldFeedNodes).inferringMoves() + for change in diff { + switch change { + case .insert(let offset, _, let associated): + if let associated = associated { + moves.insert(ShadowTableChanges.Move(associated, offset)) + } else { + inserts.insert(offset) + } + case .remove(let offset, _, let associated): + if let associated = associated { + moves.insert(ShadowTableChanges.Move(offset, associated)) + } else { + deletes.insert(offset) + } + } + } + + changes.append(ShadowTableChanges.RowChanges(section: section, deletes: deletes, inserts: inserts, moves: moves)) + } + + // Compute the difference in the shadow table sections + var moves = Set() + var inserts = Set() + var deletes = Set() + + let oldSections = shadowTable.map { $0.sectionID } + let newSections = newShadowTable.map { $0.sectionID } + let diff = newSections.difference(from: oldSections).inferringMoves() + for change in diff { + switch change { + case .insert(let offset, _, let associated): + if let associated = associated { + moves.insert(ShadowTableChanges.Move(associated, offset)) + } else { + inserts.insert(offset) + } + case .remove(let offset, _, let associated): + if let associated = associated { + moves.insert(ShadowTableChanges.Move(offset, associated)) + } else { + deletes.insert(offset) + } + } + } + + shadowTable = newShadowTable + + return ShadowTableChanges(deletes: deletes, inserts: inserts, moves: moves, rowChanges: changes) } func shadowTableContains(_ feed: Feed) -> Bool { for section in shadowTable { - for node in section { - if let nodeFeed = node.representedObject as? Feed, nodeFeed.feedID == feed.feedID { + for feedNode in section.feedNodes { + if let nodeFeed = feedNode.node.representedObject as? Feed, nodeFeed.feedID == feed.feedID { return true } } @@ -1505,22 +1609,6 @@ private extension SceneCoordinator { } } - func nodeFor(_ indexPath: IndexPath) -> Node? { - guard indexPath.section < shadowTable.count && indexPath.row < shadowTable[indexPath.section].count else { - return nil - } - return shadowTable[indexPath.section][indexPath.row] - } - - func indexPathFor(_ node: Node) -> IndexPath? { - for i in 0.. IndexPath? { guard let node = treeController.rootNode.descendantNodeRepresentingObject(object) else { return nil @@ -1655,9 +1743,9 @@ private extension SceneCoordinator { let nextIndexPath: IndexPath = { if indexPath.row - 1 < 0 { if indexPath.section - 1 < 0 { - return IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1) + return IndexPath(row: shadowTable[shadowTable.count - 1].feedNodes.count - 1, section: shadowTable.count - 1) } else { - return IndexPath(row: shadowTable[indexPath.section - 1].count - 1, section: indexPath.section - 1) + return IndexPath(row: shadowTable[indexPath.section - 1].feedNodes.count - 1, section: indexPath.section - 1) } } else { return IndexPath(row: indexPath.row - 1, section: indexPath.section) @@ -1667,7 +1755,7 @@ private extension SceneCoordinator { if selectPrevUnreadFeedFetcher(startingWith: nextIndexPath) { return } - let maxIndexPath = IndexPath(row: shadowTable[shadowTable.count - 1].count - 1, section: shadowTable.count - 1) + let maxIndexPath = IndexPath(row: shadowTable[shadowTable.count - 1].feedNodes.count - 1, section: shadowTable.count - 1) selectPrevUnreadFeedFetcher(startingWith: maxIndexPath) } @@ -1681,7 +1769,7 @@ private extension SceneCoordinator { if indexPath.section == i { return indexPath.row } else { - return shadowTable[i].count - 1 + return shadowTable[i].feedNodes.count - 1 } }() @@ -1760,7 +1848,7 @@ private extension SceneCoordinator { // Increment or wrap around the IndexPath let nextIndexPath: IndexPath = { - if indexPath.row + 1 >= shadowTable[indexPath.section].count { + if indexPath.row + 1 >= shadowTable[indexPath.section].feedNodes.count { if indexPath.section + 1 >= shadowTable.count { return IndexPath(row: 0, section: 0) } else { @@ -1795,7 +1883,7 @@ private extension SceneCoordinator { } }() - for j in startingRow.. Bool { guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable], - let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else { - return false - } + let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo), + let isShowingExtractedArticle = userInfo[UserInfoKey.isShowingExtractedArticle] as? Bool, + let articleWindowScrollY = userInfo[UserInfoKey.articleWindowScrollY] as? Int else { + return false + } switch feedIdentifier { - case .smartFeed: - guard let smartFeed = SmartFeedsController.shared.find(by: feedIdentifier) else { return false } - if let indexPath = indexPathFor(smartFeed) { - selectFeed(indexPath: indexPath) { - self.selectArticleInCurrentFeed(articleID) - } - treeControllerDelegate.addFilterException(feedIdentifier) - return true - } - case .script: return false - - case .folder(let accountID, let folderName): - guard let accountNode = findAccountNode(accountID: accountID), - let folderNode = findFolderNode(folderName: folderName, beginningAt: accountNode) else { - return false - } - let found = selectFeedAndArticle(feedNode: folderNode, articleID: articleID) + + case .smartFeed, .folder: + let found = selectFeedAndArticle(feedIdentifier: feedIdentifier, articleID: articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY) if found { treeControllerDelegate.addFilterException(feedIdentifier) } return found case .webFeed: - guard let accountNode = findAccountNode(accountID: accountID), let webFeedNode = findWebFeedNode(webFeedID: webFeedID, beginningAt: accountNode) else { - return false - } - let found = selectFeedAndArticle(feedNode: webFeedNode, articleID: articleID) + let found = selectFeedAndArticle(feedIdentifier: feedIdentifier, articleID: articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY) if found { treeControllerDelegate.addFilterException(feedIdentifier) - if let folder = webFeedNode.parent?.representedObject as? Folder, let folderFeedID = folder.feedID { + if let webFeedNode = nodeFor(feedID: feedIdentifier), let folder = webFeedNode.parent?.representedObject as? Folder, let folderFeedID = folder.feedID { treeControllerDelegate.addFilterException(folderFeedID) } } @@ -2248,7 +2321,6 @@ private extension SceneCoordinator { } - return false } func findAccountNode(accountID: String, accountName: String? = nil) -> Node? { @@ -2277,14 +2349,14 @@ private extension SceneCoordinator { return nil } - func selectFeedAndArticle(feedNode: Node, articleID: String) -> Bool { - if let feedIndexPath = indexPathFor(feedNode) { - selectFeed(indexPath: feedIndexPath) { - self.selectArticleInCurrentFeed(articleID) - } - return true + func selectFeedAndArticle(feedIdentifier: FeedIdentifier, articleID: String, isShowingExtractedArticle: Bool, articleWindowScrollY: Int) -> Bool { + guard let feedNode = nodeFor(feedID: feedIdentifier), let feedIndexPath = indexPathFor(feedNode) else { return false } + + selectFeed(indexPath: feedIndexPath) { + self.selectArticleInCurrentFeed(articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY) } - return false + + return true } } diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 39a26aa2d..3da542328 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -9,15 +9,16 @@ import UIKit import UserNotifications import Account +import Zip class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? + var window: UIWindow? var coordinator = SceneCoordinator() - // UIWindowScene delegate - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // UIWindowScene delegate + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { window = UIWindow(windowScene: scene as! UIWindowScene) window!.tintColor = AppAssets.primaryAccentColor @@ -46,12 +47,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return } - if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { + if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { coordinator.handle(userActivity) } window!.makeKeyAndVisible() - } + } func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { appDelegate.resumeDatabaseProcessingIfNecessary() @@ -79,9 +80,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { coordinator.resetFocus() } - func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { + func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { return coordinator.stateRestorationActivity - } + } // API @@ -89,7 +90,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { appDelegate.resumeDatabaseProcessingIfNecessary() coordinator.handle(response) } - + func suspend() { coordinator.suspend() } @@ -162,9 +163,47 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } + let filename = context.url.standardizedFileURL.path + if filename.hasSuffix(ArticleTheme.nnwThemeSuffix) { + self.coordinator.importTheme(filename: filename) + return + } + + // Handle theme URLs: netnewswire://theme/add?url={url} + guard let comps = URLComponents(url: context.url, resolvingAgainstBaseURL: false), + "theme" == comps.host, + let queryItems = comps.queryItems else { + return + } + + if let providedThemeURL = queryItems.first(where: { $0.name == "url" })?.value { + if let themeURL = URL(string: providedThemeURL) { + let request = URLRequest(url: themeURL) + + DispatchQueue.main.async { + NotificationCenter.default.post(name: .didBeginDownloadingTheme, object: nil) + } + let task = URLSession.shared.downloadTask(with: request) { location, response, error in + guard + let location = location else { return } + + do { + try ArticleThemeDownloader.shared.handleFile(at: location) + } catch { + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) + } + } + task.resume() + } else { + print("No theme URL") + return + } + } else { + return + } + } } - } private extension SceneDelegate { @@ -199,4 +238,6 @@ private extension SceneDelegate { } } + + } diff --git a/iOS/Settings/ArticleThemeImporter.swift b/iOS/Settings/ArticleThemeImporter.swift new file mode 100644 index 000000000..135d19989 --- /dev/null +++ b/iOS/Settings/ArticleThemeImporter.swift @@ -0,0 +1,96 @@ +// +// ArticleThemeImporter.swift +// NetNewsWire +// +// Created by Maurice Parker on 9/18/21. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import UIKit + +struct ArticleThemeImporter { + + static func importTheme(controller: UIViewController, filename: String) throws { + let theme = try ArticleTheme(path: filename) + + let localizedTitleText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text") + let title = NSString.localizedStringWithFormat(localizedTitleText as NSString, theme.name, theme.creatorName) as String + + let localizedMessageText = NSLocalizedString("Author's Website:\n%@", comment: "Authors website") + let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.creatorHomePage) as String + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") + alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) + + if let url = URL(string: theme.creatorHomePage) { + let visitSiteTitle = NSLocalizedString("Show Website", comment: "Show Website") + let visitSiteAction = UIAlertAction(title: visitSiteTitle, style: .default) { action in + UIApplication.shared.open(url) + try? Self.importTheme(controller: controller, filename: filename) + } + alertController.addAction(visitSiteAction) + } + + func importTheme() { + do { + try ArticleThemesManager.shared.importTheme(filename: filename) + confirmImportSuccess(controller: controller, themeName: theme.name) + } catch { + controller.presentError(error) + } + } + + let installThemeTitle = NSLocalizedString("Install Theme", comment: "Install Theme") + let installThemeAction = UIAlertAction(title: installThemeTitle, style: .default) { action in + + if ArticleThemesManager.shared.themeExists(filename: filename) { + let title = NSLocalizedString("Duplicate Theme", comment: "Duplicate Theme") + let localizedMessageText = NSLocalizedString("The theme “%@” already exists. Overwrite it?", comment: "Overwrite theme") + let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name) as String + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") + alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) + + let overwriteAction = UIAlertAction(title: NSLocalizedString("Overwrite", comment: "Overwrite"), style: .default) { action in + importTheme() + } + alertController.addAction(overwriteAction) + alertController.preferredAction = overwriteAction + + controller.present(alertController, animated: true) + } else { + importTheme() + } + + } + + alertController.addAction(installThemeAction) + alertController.preferredAction = installThemeAction + + controller.present(alertController, animated: true) + + } + +} + +private extension ArticleThemeImporter { + + static func confirmImportSuccess(controller: UIViewController, themeName: String) { + let title = NSLocalizedString("Theme installed", comment: "Theme installed") + + let localizedMessageText = NSLocalizedString("The theme “%@” has been installed.", comment: "Theme installed") + let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, themeName) as String + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + let doneTitle = NSLocalizedString("Done", comment: "Done") + alertController.addAction(UIAlertAction(title: doneTitle, style: .default)) + + controller.present(alertController, animated: true) + } + +} diff --git a/iOS/Settings/ArticleThemesTableViewController.swift b/iOS/Settings/ArticleThemesTableViewController.swift new file mode 100644 index 000000000..26e28944f --- /dev/null +++ b/iOS/Settings/ArticleThemesTableViewController.swift @@ -0,0 +1,122 @@ +// +// ArticleThemesTableViewController.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 9/12/21. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import Foundation + +import UIKit + +class ArticleThemesTableViewController: UITableViewController { + + override func viewDidLoad() { + let importBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(importTheme(_:))); + importBarButtonItem.title = NSLocalizedString("Import Theme", comment: "Import Theme"); + navigationItem.rightBarButtonItem = importBarButtonItem + + NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil) + } + + // MARK: Notifications + + @objc func articleThemeNamesDidChangeNotification(_ note: Notification) { + tableView.reloadData() + } + + @objc func importTheme(_ sender: Any?) { + let docPicker = UIDocumentPickerViewController(documentTypes: ["com.ranchero.netnewswire.theme"], in: .import) + docPicker.delegate = self + docPicker.modalPresentationStyle = .formSheet + self.present(docPicker, animated: true) + } + + // MARK: - Table view data source + + override func numberOfSections(in tableView: UITableView) -> Int { + return 1 + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + return ArticleThemesManager.shared.themeNames.count + 1 + } + + override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { + let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) + + let themeName: String + if indexPath.row == 0 { + themeName = ArticleTheme.defaultTheme.name + } else { + themeName = ArticleThemesManager.shared.themeNames[indexPath.row - 1] + } + + cell.textLabel?.text = themeName + if themeName == ArticleThemesManager.shared.currentTheme.name { + cell.accessoryType = .checkmark + } else { + cell.accessoryType = .none + } + + return cell + } + + override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { + guard let cell = tableView.cellForRow(at: indexPath), let themeName = cell.textLabel?.text else { return } + ArticleThemesManager.shared.currentThemeName = themeName + navigationController?.popViewController(animated: true) + } + + override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { + guard indexPath.row != 0, + let cell = tableView.cellForRow(at: indexPath), + let themeName = cell.textLabel?.text else { return nil } + + let deleteTitle = NSLocalizedString("Delete", comment: "Delete") + let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, completion) in + let title = NSLocalizedString("Delete Theme?", comment: "Delete Theme") + + let localizedMessageText = NSLocalizedString("Are you sure you want to delete the theme “%@”?.", comment: "Delete Theme Message") + let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, themeName) as String + + let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) + + let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") + let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { action in + completion(true) + } + alertController.addAction(cancelAction) + + let deleteTitle = NSLocalizedString("Delete", comment: "Delete") + let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { action in + ArticleThemesManager.shared.deleteTheme(themeName: themeName) + completion(true) + } + alertController.addAction(deleteAction) + + self?.present(alertController, animated: true) + } + + deleteAction.image = AppAssets.trashImage + deleteAction.backgroundColor = UIColor.systemRed + + return UISwipeActionsConfiguration(actions: [deleteAction]) + } +} + +// MARK: UIDocumentPickerDelegate + +extension ArticleThemesTableViewController: UIDocumentPickerDelegate { + + func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { + guard let url = urls.first else { return } + do { + try ArticleThemeImporter.importTheme(controller: self, filename: url.standardizedFileURL.path) + } catch { + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) + } + } + +} diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index 393853548..9cb9246b5 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -1,9 +1,9 @@ - + - + @@ -21,7 +21,7 @@ - + @@ -42,14 +42,14 @@ - + - + + + @@ -557,7 +625,7 @@ - + @@ -568,14 +636,14 @@ - + - + @@ -586,7 +654,7 @@ - + - + - + @@ -975,14 +1043,14 @@ - + - + @@ -993,7 +1061,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -1137,7 +1244,7 @@ - + diff --git a/iOS/Settings/SettingsTableViewCell.xib b/iOS/Settings/SettingsTableViewCell.xib index 76a7cdfcc..02fe21099 100644 --- a/iOS/Settings/SettingsTableViewCell.xib +++ b/iOS/Settings/SettingsTableViewCell.xib @@ -1,37 +1,44 @@ - + - + + + - + - + + + + + + diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift index 7cfc36d15..23811c861 100644 --- a/iOS/Settings/SettingsViewController.swift +++ b/iOS/Settings/SettingsViewController.swift @@ -10,6 +10,7 @@ import UIKit import Account import CoreServices import SafariServices +import SwiftUI class SettingsViewController: UITableViewController { @@ -18,9 +19,11 @@ class SettingsViewController: UITableViewController { @IBOutlet weak var timelineSortOrderSwitch: UISwitch! @IBOutlet weak var groupByFeedSwitch: UISwitch! @IBOutlet weak var refreshClearsReadArticlesSwitch: UISwitch! + @IBOutlet weak var articleThemeDetailLabel: UILabel! @IBOutlet weak var confirmMarkAllAsReadSwitch: UISwitch! @IBOutlet weak var showFullscreenArticlesSwitch: UISwitch! @IBOutlet weak var colorPaletteDetailLabel: UILabel! + @IBOutlet weak var openLinksInNetNewsWire: UISwitch! var scrollToArticlesSection = false weak var presentingParentController: UIViewController? @@ -34,6 +37,7 @@ class SettingsViewController: UITableViewController { NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange), name: .UserDidDeleteAccount, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange), name: .DisplayNameDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(activeExtensionPointsDidChange), name: .ActiveExtensionPointsDidChange, object: nil) + tableView.register(UINib(nibName: "SettingsComboTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsComboTableViewCell") tableView.register(UINib(nibName: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell") @@ -62,6 +66,8 @@ class SettingsViewController: UITableViewController { } else { refreshClearsReadArticlesSwitch.isOn = false } + + articleThemeDetailLabel.text = ArticleThemesManager.shared.currentTheme.name if AppDefaults.shared.confirmMarkAllAsRead { confirmMarkAllAsReadSwitch.isOn = true @@ -76,6 +82,9 @@ class SettingsViewController: UITableViewController { } colorPaletteDetailLabel.text = String(describing: AppDefaults.userInterfaceColorPalette) + + openLinksInNetNewsWire.isOn = !AppDefaults.shared.useSystemBrowser + let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.0)) buildLabel.font = UIFont.systemFont(ofSize: 11.0) @@ -118,7 +127,7 @@ class SettingsViewController: UITableViewController { } return defaultNumberOfRows case 5: - return traitCollection.userInterfaceIdiom == .phone ? 2 : 1 + return traitCollection.userInterfaceIdiom == .phone ? 4 : 3 default: return super.tableView(tableView, numberOfRowsInSection: section) } @@ -157,7 +166,6 @@ class SettingsViewController: UITableViewController { acctCell.comboNameLabel?.text = extensionPoint.title cell = acctCell } - default: cell = super.tableView(tableView, cellForRowAt: indexPath) @@ -220,6 +228,14 @@ class SettingsViewController: UITableViewController { default: break } + case 5: + switch indexPath.row { + case 0: + let articleThemes = UIStoryboard.settings.instantiateController(ofType: ArticleThemesTableViewController.self) + self.navigationController?.pushViewController(articleThemes, animated: true) + default: + break + } case 6: let colorPalette = UIStoryboard.settings.instantiateController(ofType: ColorPaletteTableViewController.self) self.navigationController?.pushViewController(colorPalette, animated: true) @@ -326,6 +342,15 @@ class SettingsViewController: UITableViewController { } } + @IBAction func switchBrowserPreference(_ sender: Any) { + if openLinksInNetNewsWire.isOn { + AppDefaults.shared.useSystemBrowser = false + } else { + AppDefaults.shared.useSystemBrowser = true + } + } + + // MARK: Notifications @objc func contentSizeCategoryDidChange() { @@ -344,6 +369,10 @@ class SettingsViewController: UITableViewController { tableView.reloadData() } + @objc func browserPreferenceDidChange() { + tableView.reloadData() + } + } // MARK: OPML Document Picker diff --git a/iOS/UIKit Extensions/UIViewController-Extensions.swift b/iOS/UIKit Extensions/UIViewController-Extensions.swift index a21bc05eb..755265ad7 100644 --- a/iOS/UIKit Extensions/UIViewController-Extensions.swift +++ b/iOS/UIKit Extensions/UIViewController-Extensions.swift @@ -15,6 +15,37 @@ extension UIViewController { func presentError(_ error: Error, dismiss: (() -> Void)? = nil) { if let accountError = error as? AccountError, accountError.isCredentialsError { presentAccountError(accountError, dismiss: dismiss) + } else if let decodingError = error as? DecodingError { + let errorTitle = NSLocalizedString("Error", comment: "Error") + var informativeText: String = "" + switch decodingError { + case .typeMismatch(let type, _): + let localizedError = NSLocalizedString("This theme cannot be used because the the type—“%@”—is mismatched in the Info.plist", comment: "Type mismatch") + informativeText = NSString.localizedStringWithFormat(localizedError as NSString, type as! CVarArg) as String + presentError(title: errorTitle, message: informativeText, dismiss: dismiss) + case .valueNotFound(let value, _): + let localizedError = NSLocalizedString("This theme cannot be used because the the value—“%@”—is not found in the Info.plist.", comment: "Decoding value missing") + informativeText = NSString.localizedStringWithFormat(localizedError as NSString, value as! CVarArg) as String + presentError(title: errorTitle, message: informativeText, dismiss: dismiss) + case .keyNotFound(let codingKey, _): + let localizedError = NSLocalizedString("This theme cannot be used because the the key—“%@”—is not found in the Info.plist.", comment: "Decoding key missing") + informativeText = NSString.localizedStringWithFormat(localizedError as NSString, codingKey.stringValue) as String + presentError(title: errorTitle, message: informativeText, dismiss: dismiss) + case .dataCorrupted(let context): + guard let error = context.underlyingError as NSError?, + let debugDescription = error.userInfo["NSDebugDescription"] as? String else { + informativeText = error.localizedDescription + presentError(title: errorTitle, message: informativeText, dismiss: dismiss) + return + } + 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 + presentError(title: errorTitle, message: informativeText, dismiss: dismiss) + + default: + informativeText = error.localizedDescription + presentError(title: errorTitle, message: informativeText, dismiss: dismiss) + } } else { let errorTitle = NSLocalizedString("Error", comment: "Error") presentError(title: errorTitle, message: error.localizedDescription, dismiss: dismiss) diff --git a/xcconfig/NetNewsWire_macapp_target.xcconfig b/xcconfig/NetNewsWire_macapp_target.xcconfig index 604d5b147..600a8a2a1 100644 --- a/xcconfig/NetNewsWire_macapp_target.xcconfig +++ b/xcconfig/NetNewsWire_macapp_target.xcconfig @@ -1,8 +1,8 @@ -CODE_SIGN_IDENTITY = Developer ID Application +CODE_SIGN_IDENTITY = Mac Developer DEVELOPMENT_TEAM = M8L2WTLA8W -CODE_SIGN_STYLE = Manual +CODE_SIGN_STYLE = Automatic ORGANIZATION_IDENTIFIER = com.ranchero -PROVISIONING_PROFILE_SPECIFIER = NetNewsWire +PROVISIONING_PROFILE_SPECIFIER = DEVELOPER_ENTITLEMENTS = // developers can locally override the Xcode settings for code signing diff --git a/xcconfig/NetNewsWire_multiplatform_iOSapp_target.xcconfig b/xcconfig/NetNewsWire_multiplatform_iOSapp_target.xcconfig deleted file mode 100644 index 0880a7223..000000000 --- a/xcconfig/NetNewsWire_multiplatform_iOSapp_target.xcconfig +++ /dev/null @@ -1,48 +0,0 @@ -CODE_SIGN_IDENTITY= iPhone Developer -DEVELOPMENT_TEAM = M8L2WTLA8W -CODE_SIGN_STYLE = Automatic -ORGANIZATION_IDENTIFIER = com.ranchero -DEVELOPER_ENTITLEMENTS = -PROVISIONING_PROFILE_SPECIFIER = - -// developers can locally override the Xcode settings for code signing -// by creating a DeveloperSettings.xcconfig file locally at the appropriate path -// This allows a pristine project to have code signing set up with the appropriate -// developer ID and certificates, and for dev to be able to have local settings -// without needing to check in anything into source control -// -// As an example, make a ../../SharedXcodeSettings/DeveloperSettings.xcconfig file and -// give it the contents -// -// CODE_SIGN_IDENTITY[sdk=macosx*] = Mac Developer -// CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer -// CODE_SIGN_IDENTITY[sdk=iphonesimulator*] = iPhone Developer -// DEVELOPMENT_TEAM = -// ORGANIZATION_IDENTIFIER = -// CODE_SIGN_STYLE = Automatic -// DEVELOPER_ENTITLEMENTS = -dev -// PROVISIONING_PROFILE_SPECIFIER = -// -// And you should be able to build without code signing errors and without modifying -// the NetNewsWire Xcode project. -// -// Example: if your NetNewsWire Xcode project file is at -// /Users/Shared/git/NetNewsWire/NetNewsWire.xcodeproj -// create your DeveloperSettings.xcconfig file at -// /Users/Shared/git/SharedXcodeSettings/DeveloperSettings.xcconfig -// - -#include? "../../SharedXcodeSettings/ProjectSettings.xcconfig" -#include? "../../SharedXcodeSettings/DeveloperSettings.xcconfig" -#include "./common/NetNewsWire_ios_target_common.xcconfig" - -LD_RUNPATH_SEARCH_PATHS = $(inherited) @executable_path/Frameworks -INFOPLIST_FILE = Multiplatform/iOS/Info.plist -CODE_SIGN_ENTITLEMENTS = Multiplatform/iOS/iOS$(DEVELOPER_ENTITLEMENTS).entitlements -PRODUCT_BUNDLE_IDENTIFIER = $(ORGANIZATION_IDENTIFIER).NetNewsWire.iOS -PRODUCT_NAME = NetNewsWire - -// Override NetNewsWire_project.xcconfig until we are ready to only target 10.16 -IPHONEOS_DEPLOYMENT_TARGET = 14.0 -SWIFT_SWIFT3_OBJC_INFERENCE = Off -SWIFT_VERSION = 5.3 diff --git a/xcconfig/NetNewsWire_multiplatform_macOSapp_target.xcconfig b/xcconfig/NetNewsWire_multiplatform_macOSapp_target.xcconfig deleted file mode 100644 index 33cdf6669..000000000 --- a/xcconfig/NetNewsWire_multiplatform_macOSapp_target.xcconfig +++ /dev/null @@ -1,46 +0,0 @@ -CODE_SIGN_IDENTITY = Developer ID Application -DEVELOPMENT_TEAM = M8L2WTLA8W -CODE_SIGN_STYLE = Manual -ORGANIZATION_IDENTIFIER = com.ranchero -PROVISIONING_PROFILE_SPECIFIER = NetNewsWire -DEVELOPER_ENTITLEMENTS = - -// developers can locally override the Xcode settings for code signing -// by creating a DeveloperSettings.xcconfig file locally at the appropriate path -// This allows a pristine project to have code signing set up with the appropriate -// developer ID and certificates, and for dev to be able to have local settings -// without needing to check in anything into source control -// -// As an example, make a ../../SharedXcodeSettings/DeveloperSettings.xcconfig file and -// give it the contents -// -// CODE_SIGN_IDENTITY[sdk=macosx*] = Mac Developer -// CODE_SIGN_IDENTITY[sdk=iphoneos*] = iPhone Developer -// CODE_SIGN_IDENTITY[sdk=iphonesimulator*] = iPhone Developer -// DEVELOPMENT_TEAM = -// ORGANIZATION_IDENTIFIER = -// CODE_SIGN_STYLE = Automatic -// DEVELOPER_ENTITLEMENTS = -dev -// PROVISIONING_PROFILE_SPECIFIER = -// -// And you should be able to build without code signing errors and without modifying -// the NetNewsWire Xcode project. -// -// Example: if your NetNewsWire Xcode project file is at -// /Users/Shared/git/NetNewsWire/NetNewsWire.xcodeproj -// create your DeveloperSettings.xcconfig file at -// /Users/Shared/git/SharedXcodeSettings/DeveloperSettings.xcconfig -// - -#include? "../../SharedXcodeSettings/DeveloperSettings.xcconfig" -#include "./common/NetNewsWire_macapp_target_common.xcconfig" - -INFOPLIST_FILE = Multiplatform/macOS/Info.plist -CODE_SIGN_ENTITLEMENTS = Multiplatform/macOS/macOS$(DEVELOPER_ENTITLEMENTS).entitlements -PRODUCT_BUNDLE_IDENTIFIER = $(ORGANIZATION_IDENTIFIER).NetNewsWire-Evergreen -PRODUCT_NAME = NetNewsWire - -// Override NetNewsWire_project.xcconfig until we are ready to only target 10.16 -MACOSX_DEPLOYMENT_TARGET = 11.0 -SWIFT_SWIFT3_OBJC_INFERENCE = Off -SWIFT_VERSION = 5.3 diff --git a/xcconfig/NetNewsWire_safariextension_target.xcconfig b/xcconfig/NetNewsWire_safariextension_target.xcconfig index afc9c1288..ead1b8831 100644 --- a/xcconfig/NetNewsWire_safariextension_target.xcconfig +++ b/xcconfig/NetNewsWire_safariextension_target.xcconfig @@ -1,9 +1,9 @@ -CODE_SIGN_IDENTITY = Developer ID Application +CODE_SIGN_IDENTITY = Mac Developer DEVELOPMENT_TEAM = M8L2WTLA8W -CODE_SIGN_STYLE = Manual +CODE_SIGN_STYLE = Automatic ORGANIZATION_IDENTIFIER = com.ranchero -PROVISIONING_PROFILE_SPECIFIER = +PROVISIONING_PROFILE_SPECIFIER = // developers can locally override the Xcode settings for code signing // by creating a DeveloperSettings.xcconfig file locally at the appropriate path diff --git a/xcconfig/NetNewsWire_shareextension_target.xcconfig b/xcconfig/NetNewsWire_shareextension_target.xcconfig index fa2973248..d5515df46 100644 --- a/xcconfig/NetNewsWire_shareextension_target.xcconfig +++ b/xcconfig/NetNewsWire_shareextension_target.xcconfig @@ -1,8 +1,8 @@ -CODE_SIGN_IDENTITY = Developer ID Application +CODE_SIGN_IDENTITY = Mac Developer DEVELOPMENT_TEAM = M8L2WTLA8W -CODE_SIGN_STYLE = Manual +CODE_SIGN_STYLE = Automatic ORGANIZATION_IDENTIFIER = com.ranchero -PROVISIONING_PROFILE_SPECIFIER = NetNewsWire Mac Share Extension +PROVISIONING_PROFILE_SPECIFIER = DEVELOPER_ENTITLEMENTS = // developers can locally override the Xcode settings for code signing diff --git a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig index ef5675d42..3d6d7a3de 100644 --- a/xcconfig/common/NetNewsWire_ios_target_common.xcconfig +++ b/xcconfig/common/NetNewsWire_ios_target_common.xcconfig @@ -1,7 +1,7 @@ // High Level Settings common to both the iOS application and any extensions we bundle with it -MARKETING_VERSION = 6.0 -CURRENT_PROJECT_VERSION = 606 +MARKETING_VERSION = 6.0.2 +CURRENT_PROJECT_VERSION = 609 ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon