diff --git a/AppleScript/Safari-OpenAllStarredArticles.applescript b/AppleScript/Safari-OpenAllStarredArticles.applescript new file mode 100644 index 000000000..cad33bb4d --- /dev/null +++ b/AppleScript/Safari-OpenAllStarredArticles.applescript @@ -0,0 +1,49 @@ +-- This script creates a new Safari window with all the starred articles in a NetNewsWire instance, each in its own tab + +-- declare the safariWindow property here so we can use is throughout the whole script + +property safariWindow : missing value + +-- the openTabInSafari() function opens a new tab in the appropriate window + +to openTabInSafari(theUrl) + tell application "Safari" + -- test if this is the first call to openTabInSafari() + if (my safariWindow is missing value) then + -- first time through, make a new window with the given url in the only tab + set newdoc to make new document at front with properties {URL:theUrl} + -- because we created the doucument "at front", we know it is window 1 + set safariWindow to window 1 + else + -- after the first time, make a new tab in the wndow we created the first tim + tell safariWindow + make new tab with properties {URL:theUrl} + end tell + end if + end tell +end openTabInSafari + + +-- the script starts here +-- First, initialize safariWindow to be missing value, so that the first time through +-- openTabInSafari() we'll make a new window to hold all our articles + +set safariWindow to missing value + + +-- Then we loop though all the feeds of all the accounts +-- for each feed, we find all the starred articles +--for each one of those, open a new tab in Safari + +tell application "NetNewsWire" + set allAccounts to every account + repeat with nthAccount in allAccounts + set allFeeds to every feed of nthAccount + repeat with nthFeed in allFeeds + set starredArticles to (get every article of nthFeed where starred is true) + repeat with nthArticle in starredArticles + my openTabInSafari(url of nthArticle) + end repeat + end repeat + end repeat +end tell diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index fb6a47caa..74c165694 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -449,7 +449,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, return feed } - public func removeFeed(_ feed: Feed, from container: Container?, completion: @escaping (Result) -> Void) { + public func removeFeed(_ feed: Feed, from container: Container, completion: @escaping (Result) -> Void) { delegate.removeFeed(for: self, with: feed, from: container, completion: completion) } @@ -557,8 +557,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, } public func fetchUnreadCountForToday(_ callback: @escaping (Int) -> Void) { - let startOfToday = NSCalendar.startOfToday() - database.fetchUnreadCount(for: flattenedFeeds().feedIDs(), since: startOfToday, callback: callback) + database.fetchUnreadCountForToday(for: flattenedFeeds().feedIDs(), callback: callback) } public func fetchUnreadCountForStarredArticles(_ callback: @escaping (Int) -> Void) { diff --git a/Frameworks/Account/AccountDelegate.swift b/Frameworks/Account/AccountDelegate.swift index b4af6e7f0..5be29ebe6 100644 --- a/Frameworks/Account/AccountDelegate.swift +++ b/Frameworks/Account/AccountDelegate.swift @@ -37,7 +37,7 @@ protocol AccountDelegate { func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result) -> Void) func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result) -> Void) func addFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result) -> Void) - func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result) -> Void) + func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result) -> Void) func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result) -> Void) func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result) -> Void) diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index beac0674e..9ff14275a 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -396,8 +396,7 @@ final class FeedbinAccountDelegate: AccountDelegate { } - func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result) -> Void) { - retrieveCredentialsIfNecessary(account) + func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result) -> Void) { if feed.folderRelationship?.count ?? 0 > 1 { deleteTagging(for: account, with: feed, from: container, completion: completion) } else { diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index af5db8134..a9d2df907 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -140,8 +140,8 @@ final class LocalAccountDelegate: AccountDelegate { completion(.success(())) } - func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result) -> Void) { - container?.removeFeed(feed) + func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result) -> Void) { + container.removeFeed(feed) completion(.success(())) } diff --git a/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift b/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift index fdbfb6f74..4d1f7acf7 100644 --- a/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift +++ b/Frameworks/Account/ReaderAPI/ReaderAPIAccountDelegate.swift @@ -304,7 +304,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate { } - func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result) -> Void) { + func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result) -> Void) { if feed.folderRelationship?.count ?? 0 > 1 { deleteTagging(for: account, with: feed, from: container, completion: completion) } else { @@ -454,8 +454,8 @@ private extension ReaderAPIAccountDelegate { } func syncFolders(_ account: Account, _ tags: [ReaderAPITag]?) { - guard let tags = tags else { return } + assert(Thread.isMainThread) os_log(.debug, log: log, "Syncing folders with %ld tags.", tags.count) @@ -465,13 +465,11 @@ private extension ReaderAPIAccountDelegate { if let folders = account.folders { folders.forEach { folder in if !tagNames.contains(folder.name ?? "") { - DispatchQueue.main.sync { - for feed in folder.topLevelFeeds { - account.addFeed(feed) - clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") - } - account.removeFolder(folder) + for feed in folder.topLevelFeeds { + account.addFeed(feed) + clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") } + account.removeFolder(folder) } } } @@ -487,9 +485,7 @@ private extension ReaderAPIAccountDelegate { // Make any folders Reader has, but we don't tagNames.forEach { tagName in if !folderNames.contains(tagName) { - DispatchQueue.main.sync { - _ = account.ensureFolder(with: tagName) - } + _ = account.ensureFolder(with: tagName) } } @@ -523,7 +519,8 @@ private extension ReaderAPIAccountDelegate { func syncFeeds(_ account: Account, _ subscriptions: [ReaderAPISubscription]?) { guard let subscriptions = subscriptions else { return } - + assert(Thread.isMainThread) + os_log(.debug, log: log, "Syncing feeds with %ld subscriptions.", subscriptions.count) let subFeedIds = subscriptions.map { String($0.feedID) } @@ -533,9 +530,7 @@ private extension ReaderAPIAccountDelegate { for folder in folders { for feed in folder.topLevelFeeds { if !subFeedIds.contains(feed.feedID) { - DispatchQueue.main.sync { - folder.removeFeed(feed) - } + folder.removeFeed(feed) } } } @@ -543,9 +538,7 @@ private extension ReaderAPIAccountDelegate { for feed in account.topLevelFeeds { if !subFeedIds.contains(feed.feedID) { - DispatchQueue.main.sync { - account.removeFeed(feed) - } + account.removeFeed(feed) } } @@ -553,17 +546,14 @@ private extension ReaderAPIAccountDelegate { subscriptions.forEach { subscription in let subFeedId = String(subscription.feedID) - - DispatchQueue.main.sync { - if let feed = account.idToFeedDictionary[subFeedId] { - feed.name = subscription.name - feed.homePageURL = subscription.homePageURL - } else { - let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: subFeedId, homePageURL: subscription.homePageURL) - feed.iconURL = subscription.iconURL - feed.subscriptionID = String(subscription.feedID) - account.addFeed(feed) - } + if let feed = account.idToFeedDictionary[subFeedId] { + feed.name = subscription.name + feed.homePageURL = subscription.homePageURL + } else { + let feed = account.createFeed(with: subscription.name, url: subscription.url, feedID: subFeedId, homePageURL: subscription.homePageURL) + feed.iconURL = subscription.iconURL + feed.subscriptionID = String(subscription.feedID) + account.addFeed(feed) } } @@ -573,6 +563,7 @@ private extension ReaderAPIAccountDelegate { func syncTaggings(_ account: Account, _ subscriptions: [ReaderAPISubscription]?) { guard let subscriptions = subscriptions else { return } + assert(Thread.isMainThread) os_log(.debug, log: log, "Syncing taggings with %ld subscriptions.", subscriptions.count) @@ -613,11 +604,9 @@ private extension ReaderAPIAccountDelegate { // Move any feeds not in the folder to the account for feed in folder.topLevelFeeds { if !taggingFeedIDs.contains(feed.feedID) { - DispatchQueue.main.sync { - folder.removeFeed(feed) - clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") - account.addFeed(feed) - } + folder.removeFeed(feed) + clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") + account.addFeed(feed) } } @@ -631,10 +620,8 @@ private extension ReaderAPIAccountDelegate { guard let feed = idDictionary[taggingFeedID] else { continue } - DispatchQueue.main.sync { - saveFolderRelationship(for: feed, withFolderName: folderName, id: String(subscription.feedID)) - folder.addFeed(feed) - } + saveFolderRelationship(for: feed, withFolderName: folderName, id: String(subscription.feedID)) + folder.addFeed(feed) } } @@ -643,11 +630,9 @@ private extension ReaderAPIAccountDelegate { let taggedFeedIDs = Set(subscriptions.map { String($0.feedID) }) // Remove all feeds from the account container that have a tag - DispatchQueue.main.sync { - for feed in account.topLevelFeeds { - if taggedFeedIDs.contains(feed.feedID) { - account.removeFeed(feed) - } + for feed in account.topLevelFeeds { + if taggedFeedIDs.contains(feed.feedID) { + account.removeFeed(feed) } } diff --git a/Frameworks/Account/ReaderAPI/ReaderAPICaller.swift b/Frameworks/Account/ReaderAPI/ReaderAPICaller.swift index b2491a0a0..6d92e84f7 100644 --- a/Frameworks/Account/ReaderAPI/ReaderAPICaller.swift +++ b/Frameworks/Account/ReaderAPI/ReaderAPICaller.swift @@ -321,79 +321,102 @@ final class ReaderAPICaller: NSObject { return } - self.requestAuthorizationToken(endpoint: baseURL) { (result) in + guard let url = URL(string: url) else { + completion(.failure(LocalAccountDelegateError.invalidParameter)) + return + } + + FeedFinder.find(url: url) { result in + switch result { - case .success(let token): - guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionAdd.rawValue), resolvingAgainstBaseURL: false) else { - completion(.failure(TransportError.noURL)) - return + case .success(let feedSpecifiers): + guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers), + let url = URL(string: bestFeedSpecifier.urlString) else { + completion(.failure(AccountError.createErrorNotFound)) + return } - - components.queryItems = [ - URLQueryItem(name: "quickadd", value: url) - ] - - guard let callURL = components.url else { - completion(.failure(TransportError.noURL)) - return - } - - var request = URLRequest(url: callURL, credentials: self.credentials) - request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") - request.httpMethod = "POST" - - let postData = "T=\(token)".data(using: String.Encoding.utf8) - - self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: ReaderAPIQuickAddResult.self, completion: { (result) in + self.requestAuthorizationToken(endpoint: baseURL) { (result) in switch result { - case .success(let (_, subResult)): - - switch subResult?.numResults { - case 0: - completion(.success(.alreadySubscribed)) - default: - // We have a feed ID but need to get feed information - guard let streamId = subResult?.streamId else { - completion(.failure(AccountError.createErrorNotFound)) - return - } - - // There is no call to get a single subscription entry, so we get them all, - // look up the one we just subscribed to and return that - self.retrieveSubscriptions(completion: { (result) in - switch result { - case .success(let subscriptions): - guard let subscriptions = subscriptions else { + case .success(let token): + guard var components = URLComponents(url: baseURL.appendingPathComponent(ReaderAPIEndpoints.subscriptionAdd.rawValue), resolvingAgainstBaseURL: false) else { + completion(.failure(TransportError.noURL)) + return + } + + components.queryItems = [ + URLQueryItem(name: "quickadd", value: url.absoluteString) + ] + + guard let callURL = components.url else { + completion(.failure(TransportError.noURL)) + return + } + + var request = URLRequest(url: callURL, credentials: self.credentials) + request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type") + request.httpMethod = "POST" + + let postData = "T=\(token)".data(using: String.Encoding.utf8) + + self.transport.send(request: request, method: HTTPMethod.post, data: postData!, resultType: ReaderAPIQuickAddResult.self, completion: { (result) in + switch result { + case .success(let (_, subResult)): + + switch subResult?.numResults { + case 0: + completion(.success(.alreadySubscribed)) + default: + // We have a feed ID but need to get feed information + guard let streamId = subResult?.streamId else { completion(.failure(AccountError.createErrorNotFound)) return } - - let newStreamId = "feed/\(streamId)" - - guard let subscription = subscriptions.first(where: { (sub) -> Bool in - sub.feedID == newStreamId - }) else { - completion(.failure(AccountError.createErrorNotFound)) - return + + // There is no call to get a single subscription entry, so we get them all, + // look up the one we just subscribed to and return that + self.retrieveSubscriptions(completion: { (result) in + switch result { + case .success(let subscriptions): + guard let subscriptions = subscriptions else { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + let newStreamId = "feed/\(streamId)" + + guard let subscription = subscriptions.first(where: { (sub) -> Bool in + sub.feedID == newStreamId + }) else { + completion(.failure(AccountError.createErrorNotFound)) + return + } + + completion(.success(.created(subscription))) + + case .failure(let error): + completion(.failure(error)) + } + }) } - - completion(.success(.created(subscription))) - - case .failure(let error): - completion(.failure(error)) - } - }) + case .failure(let error): + completion(.failure(error)) } + }) + + case .failure(let error): completion(.failure(error)) } - }) + } - case .failure(let error): - completion(.failure(error)) + + case .failure: + completion(.failure(AccountError.createErrorNotFound)) } + } +// } func renameSubscription(subscriptionID: String, newName: String, completion: @escaping (Result) -> Void) { diff --git a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift index 19ba89f6a..951162def 100644 --- a/Frameworks/ArticlesDatabase/ArticlesDatabase.swift +++ b/Frameworks/ArticlesDatabase/ArticlesDatabase.swift @@ -57,7 +57,7 @@ public final class ArticlesDatabase { } public func fetchTodayArticles(_ feedIDs: Set) -> Set
{ - return articlesTable.fetchTodayArticles(feedIDs) + return articlesTable.fetchArticlesSince(feedIDs, todayCutoffDate()) } public func fetchStarredArticles(_ feedIDs: Set) -> Set
{ @@ -83,7 +83,7 @@ public final class ArticlesDatabase { } public func fetchTodayArticlesAsync(_ feedIDs: Set, _ callback: @escaping ArticleSetBlock) { - articlesTable.fetchTodayArticlesAsync(feedIDs, callback) + articlesTable.fetchArticlesSinceAsync(feedIDs, todayCutoffDate(), callback) } public func fetchedStarredArticlesAsync(_ feedIDs: Set, _ callback: @escaping ArticleSetBlock) { @@ -100,6 +100,10 @@ public final class ArticlesDatabase { articlesTable.fetchUnreadCounts(feedIDs, callback) } + public func fetchUnreadCountForToday(for feedIDs: Set, callback: @escaping (Int) -> Void) { + fetchUnreadCount(for: feedIDs, since: todayCutoffDate(), callback: callback) + } + public func fetchUnreadCount(for feedIDs: Set, since: Date, callback: @escaping (Int) -> Void) { articlesTable.fetchUnreadCount(feedIDs, since, callback) } @@ -164,4 +168,9 @@ private extension ArticlesDatabase { CREATE TRIGGER if not EXISTS articles_after_delete_trigger_delete_search_text after delete on articles begin delete from search where rowid = OLD.searchRowID; end; """ + + func todayCutoffDate() -> Date { + // 24 hours previous. This is used by the Today smart feed, which should not actually empty out at midnight. + return Date(timeIntervalSinceNow: -(60 * 60 * 24)) // This does not need to be more precise. + } } diff --git a/Frameworks/ArticlesDatabase/ArticlesTable.swift b/Frameworks/ArticlesDatabase/ArticlesTable.swift index 9b33a3289..444337e20 100644 --- a/Frameworks/ArticlesDatabase/ArticlesTable.swift +++ b/Frameworks/ArticlesDatabase/ArticlesTable.swift @@ -102,23 +102,22 @@ final class ArticlesTable: DatabaseTable { // MARK: - Fetching Today Articles - func fetchTodayArticles(_ feedIDs: Set) -> Set
{ - return fetchArticles{ self.fetchTodayArticles(feedIDs, $0) } + func fetchArticlesSince(_ feedIDs: Set, _ cutoffDate: Date) -> Set
{ + return fetchArticles{ self.fetchArticlesSince(feedIDs, cutoffDate, $0) } } - func fetchTodayArticlesAsync(_ feedIDs: Set, _ callback: @escaping ArticleSetBlock) { - fetchArticlesAsync({ self.fetchTodayArticles(feedIDs, $0) }, callback) + func fetchArticlesSinceAsync(_ feedIDs: Set, _ cutoffDate: Date, _ callback: @escaping ArticleSetBlock) { + fetchArticlesAsync({ self.fetchArticlesSince(feedIDs, cutoffDate, $0) }, callback) } - private func fetchTodayArticles(_ feedIDs: Set, _ database: FMDatabase) -> Set
{ + private func fetchArticlesSince(_ feedIDs: Set, _ cutoffDate: Date, _ database: FMDatabase) -> Set
{ // select * from articles natural join statuses where feedID in ('http://ranchero.com/xml/rss.xml') and (datePublished > ? || (datePublished is null and dateArrived > ?) // // datePublished may be nil, so we fall back to dateArrived. if feedIDs.isEmpty { return Set
() } - let startOfToday = NSCalendar.startOfToday() - let parameters = feedIDs.map { $0 as AnyObject } + [startOfToday as AnyObject, startOfToday as AnyObject] + let parameters = feedIDs.map { $0 as AnyObject } + [cutoffDate as AnyObject, cutoffDate as AnyObject] let placeholders = NSString.rs_SQLValueList(withPlaceholders: UInt(feedIDs.count))! let whereClause = "feedID in \(placeholders) and (datePublished > ? or (datePublished is null and dateArrived > ?)) and userDeleted = 0" return fetchArticlesWithWhereClause(database, whereClause: whereClause, parameters: parameters, withLimits: false) diff --git a/Mac/MainWindow/Detail/DetailContainerView.swift b/Mac/MainWindow/Detail/DetailContainerView.swift index a33ee835b..ffc330b37 100644 --- a/Mac/MainWindow/Detail/DetailContainerView.swift +++ b/Mac/MainWindow/Detail/DetailContainerView.swift @@ -12,10 +12,6 @@ final class DetailContainerView: NSView { @IBOutlet var detailStatusBarView: DetailStatusBarView! - override var isOpaque: Bool { - return true - } - var contentViewConstraints: [NSLayoutConstraint]? var contentView: NSView? { @@ -39,9 +35,4 @@ final class DetailContainerView: NSView { } } } - - override func draw(_ dirtyRect: NSRect) { - NSColor.textBackgroundColor.setFill() - dirtyRect.fill() - } } diff --git a/Mac/MainWindow/Timeline/TimelineContainerView.swift b/Mac/MainWindow/Timeline/TimelineContainerView.swift index b24710675..e75475405 100644 --- a/Mac/MainWindow/Timeline/TimelineContainerView.swift +++ b/Mac/MainWindow/Timeline/TimelineContainerView.swift @@ -33,14 +33,5 @@ final class TimelineContainerView: NSView { } } } - - override var isOpaque: Bool { - return true - } - - override func draw(_ dirtyRect: NSRect) { - NSColor.textBackgroundColor.setFill() - dirtyRect.fill() - } } diff --git a/Mac/MainWindow/Timeline/TimelineViewController.swift b/Mac/MainWindow/Timeline/TimelineViewController.swift index 092a12906..7249da5bb 100644 --- a/Mac/MainWindow/Timeline/TimelineViewController.swift +++ b/Mac/MainWindow/Timeline/TimelineViewController.swift @@ -159,7 +159,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner { NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .AccountsDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(calendarDayChanged(_:)), name: .NSCalendarDayChanged, object: nil) DistributedNotificationCenter.default.addObserver(self, selector: #selector(appleInterfaceThemeChanged), name: .AppleInterfaceThemeChangedNotification, object: nil) didRegisterForNotifications = true @@ -526,14 +525,6 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner { self.sortDirection = AppDefaults.timelineSortDirection } - @objc func calendarDayChanged(_ note: Notification) { - if representedObjectsContainsTodayFeed() { - DispatchQueue.main.async { [weak self] in - self?.fetchAndReplaceArticlesAsync() - } - } - } - @objc func appleInterfaceThemeChanged(_ note: Notification) { appDelegate.authorAvatarDownloader.resetCache() appDelegate.feedIconDownloader.resetCache() diff --git a/Mac/Resources/NetNewsWire.sdef b/Mac/Resources/NetNewsWire.sdef index eecc40413..f43178099 100644 --- a/Mac/Resources/NetNewsWire.sdef +++ b/Mac/Resources/NetNewsWire.sdef @@ -71,6 +71,7 @@ + diff --git a/Mac/Scriptability/Account+Scriptability.swift b/Mac/Scriptability/Account+Scriptability.swift index 7b2fde624..eed7b5c6e 100644 --- a/Mac/Scriptability/Account+Scriptability.swift +++ b/Mac/Scriptability/Account+Scriptability.swift @@ -58,8 +58,10 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta var container: Container? = nil if let scriptableFolder = scriptableFeed.container as? ScriptableFolder { container = scriptableFolder.folder + } else { + container = account } - account.removeFeed(scriptableFeed.feed, from: container) { result in + account.removeFeed(scriptableFeed.feed, from: container!) { result in } } } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index bb67f67ab..d0a9e354b 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -35,11 +35,11 @@ 51554C25228B71910055115A /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 51554C30228B71A10055115A /* SyncDatabase.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; }; 51554C31228B71A10055115A /* SyncDatabase.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 51554C01228B6EB50055115A /* SyncDatabase.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 515ADE4022E11FAE006B2460 /* SystemMessageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 515ADE3F22E11FAE006B2460 /* SystemMessageViewController.swift */; }; 5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */; }; 5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */; }; 5183CCDD226F1F5C0010922C /* NavigationProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCDC226F1F5C0010922C /* NavigationProgressView.swift */; }; 5183CCDF226F1FCC0010922C /* UINavigationController+Progress.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCDE226F1FCC0010922C /* UINavigationController+Progress.swift */; }; - 5183CCE3226F314C0010922C /* ProgressTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE2226F314C0010922C /* ProgressTableViewController.swift */; }; 5183CCE5226F4DFA0010922C /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; }; 5183CCE6226F4E110010922C /* RefreshInterval.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */; }; 5183CCE8226F68D90010922C /* AccountRefreshTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */; }; @@ -690,11 +690,11 @@ 515436872291D75D005E1CDF /* AddLocalAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddLocalAccountViewController.swift; sourceTree = ""; }; 515436892291FED9005E1CDF /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = ""; }; 51554BFC228B6EB50055115A /* SyncDatabase.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = SyncDatabase.xcodeproj; path = Frameworks/SyncDatabase/SyncDatabase.xcodeproj; sourceTree = SOURCE_ROOT; }; + 515ADE3F22E11FAE006B2460 /* SystemMessageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SystemMessageViewController.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 = ""; }; 5183CCDC226F1F5C0010922C /* NavigationProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationProgressView.swift; sourceTree = ""; }; 5183CCDE226F1FCC0010922C /* UINavigationController+Progress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Progress.swift"; sourceTree = ""; }; - 5183CCE2226F314C0010922C /* ProgressTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressTableViewController.swift; sourceTree = ""; }; 5183CCE4226F4DFA0010922C /* RefreshInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshInterval.swift; sourceTree = ""; }; 5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRefreshTimer.swift; sourceTree = ""; }; 5183CCEC22711DCE0010922C /* Settings.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = ""; }; @@ -1048,7 +1048,6 @@ 5183CCDB226F1EEB0010922C /* Progress */ = { isa = PBXGroup; children = ( - 5183CCE2226F314C0010922C /* ProgressTableViewController.swift */, 5183CCDC226F1F5C0010922C /* NavigationProgressView.swift */, 5183CCDE226F1FCC0010922C /* UINavigationController+Progress.swift */, ); @@ -1154,6 +1153,7 @@ isa = PBXGroup; children = ( 51C4527E2265092C00C03939 /* DetailViewController.swift */, + 515ADE3F22E11FAE006B2460 /* SystemMessageViewController.swift */, ); path = Detail; sourceTree = ""; @@ -1978,12 +1978,12 @@ ORGANIZATIONNAME = "Ranchero Software"; TargetAttributes = { 6581C73220CED60000F4AD34 = { - DevelopmentTeam = M8L2WTLA8W; + DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Manual; }; 840D617B2029031C009BC708 = { CreatedOnToolsVersion = 9.3; - DevelopmentTeam = M8L2WTLA8W; + DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Automatic; SystemCapabilities = { com.apple.BackgroundModes = { @@ -1993,7 +1993,7 @@ }; 849C645F1ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = M8L2WTLA8W; + DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Manual; SystemCapabilities = { com.apple.HardenedRuntime = { @@ -2003,7 +2003,7 @@ }; 849C64701ED37A5D003D8FC0 = { CreatedOnToolsVersion = 8.2.1; - DevelopmentTeam = 9C84TZ7Q6Z; + DevelopmentTeam = SHJK2V3AJG; ProvisioningStyle = Automatic; TestTargetID = 849C645F1ED37A5D003D8FC0; }; @@ -2343,6 +2343,7 @@ 51EF0F79227716380050506E /* ColorHash.swift in Sources */, 5183CCDA226E31A50010922C /* NonIntrinsicImageView.swift in Sources */, 51C4527B2265091600C03939 /* MasterUnreadIndicatorView.swift in Sources */, + 515ADE4022E11FAE006B2460 /* SystemMessageViewController.swift in Sources */, 51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */, 51C45296226509D300C03939 /* OPMLExporter.swift in Sources */, 51C45291226509C800C03939 /* SmartFeed.swift in Sources */, @@ -2359,7 +2360,7 @@ 51C452852265093600C03939 /* AddFeedFolderPickerData.swift in Sources */, 51C4526B226508F600C03939 /* MasterFeedViewController.swift in Sources */, 5126EE97226CB48A00C22AFC /* AppCoordinator.swift in Sources */, - 5126EE97226CB48A00C22AFC /* NavigationStateController.swift in Sources */, + 5126EE97226CB48A00C22AFC /* AppCoordinator.swift in Sources */, 84CAFCB022BC8C35007694F0 /* FetchRequestOperation.swift in Sources */, 51EF0F77227716200050506E /* FaviconGenerator.swift in Sources */, 51C4525A226508D600C03939 /* UIStoryboard-Extensions.swift in Sources */, @@ -2411,7 +2412,6 @@ DF999FF722B5AEFA0064B687 /* SafariView.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, 84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */, - 5183CCE3226F314C0010922C /* ProgressTableViewController.swift in Sources */, 512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */, 51C45268226508F600C03939 /* MasterFeedUnreadCountView.swift in Sources */, 5183CCD0226E1E880010922C /* NonIntrinsicLabel.swift in Sources */, diff --git a/Shared/Commands/DeleteCommand.swift b/Shared/Commands/DeleteCommand.swift index 8ea6584bd..6d1bda0e3 100644 --- a/Shared/Commands/DeleteCommand.swift +++ b/Shared/Commands/DeleteCommand.swift @@ -159,19 +159,28 @@ private struct SidebarItemSpecifier { func delete(completion: @escaping () -> Void) { if let feed = feed { + + guard let container = path.resolveContainer() else { + completion() + return + } + BatchUpdate.shared.start() - account?.removeFeed(feed, from: path.resolveContainer()) { result in + account?.removeFeed(feed, from: container) { result in BatchUpdate.shared.end() completion() self.checkResult(result) } + } else if let folder = folder { + BatchUpdate.shared.start() account?.removeFolder(folder) { result in BatchUpdate.shared.end() completion() self.checkResult(result) } + } } diff --git a/Technotes/HelpBook/5.0/en/adding-feeds.markdown b/Technotes/HelpBook/5.0/en/adding-feeds.markdown new file mode 100644 index 000000000..8e16460b8 --- /dev/null +++ b/Technotes/HelpBook/5.0/en/adding-feeds.markdown @@ -0,0 +1,47 @@ +@title How to add a feed to NetNewsWire + +# How to add a feed to NetNewsWire + +NetNewsWire collects items for you from feeds published on web sites. To do this, NetNewsWire needs to know the address for the feed, e.g. `https://inessential.com/feed.xml`. + +*It’s okay if you don’t know that.* NetNewsWire will look at any web page and try to find it for you. All you need to give is the site’s URL, like `inessential.com`. + +To get started, click the Add Feed button on the toolbar, or use File → New Feed (⌘-N) from the menu bar. + +If you’ve got the address already – great! Type it or paste it into the URL box. Otherwise, just enter the website’s address into the box. + +Before you finish, you can choose an alternative name for the feed and where it will be stored (see below). + +Click ‘Add’ and NetNewsWire will fetch the URL you entered. If you entered an address that’s not a feed, NetNewsWire will search the page and add the feed it finds. + + +## Choosing an alternative name for a feed + +Feeds specify their own name, but you may want to change it to something easier to remember. ‘Brent’ rather than ‘inessential’, for example. You also change this later in the [feed inspector]. + + +## Choosing a feed’s folder + +Before you add a feed, you can choose the account and folder where it will be saved. + +This option is especially important if you’re using multiple accounts. You can choose whether to save the subscription to your [On My Mac](on-my-mac.html) account or [Feedbin] account. + +In either case, if you use folders, you can also choose which one keep the feed in. + + +## What to do when NetNewsWire can’t find a feed + +Sometimes NetNewsWire won’t be able to find a feed for a site. Either the site doesn’t offer a feed, or the feed isn’t advertised in a way that NetNewsWire can find it. + +You may be able to find a feed manually by visiting the site. There, look for a link to an RSS, Atom or JSON feed. If one exists, you can add this direct URL to NetNewsWire using the process above. Right-click on the link and copy the URL to paste into NetNewsWire. + + +## Other ways to add feeds + +### Safari Extension + +You can also add a feed from Safari using the [NetNewsWire Safari Extension](safari-extension.html). + +### Importing an OPML list of feeds + +If you have an existing subscription list in OPML format, you can [import those feeds into NetNewsWire](import-opml.html). \ No newline at end of file diff --git a/Technotes/HelpBook/5.0/en/export-opml.markdown b/Technotes/HelpBook/5.0/en/export-opml.markdown new file mode 100644 index 000000000..24396fbb3 --- /dev/null +++ b/Technotes/HelpBook/5.0/en/export-opml.markdown @@ -0,0 +1,14 @@ +@title How to export OPML + +# How to export OPML + +Your subscription list is portable, meaning you can easily switch to another app or service at any time. NetNewsWire can export an OPML file containing all your subscriptions. This file format is well-established and widely supported for just this purpose. + +1. From the menu bar, select **File → Export Subscriptions…** +2. If you have multiple accounts, select which account’s subscriptions to export +3. Choose the name and location for the OPML file +4. Click **Export OPML** + +Your subscriptions in NetNewsWire are unaffected by this action. Nothing is changed or removed. + +You can now use this file for any app or service which allows you to import OPML-formatted subscription lists. \ No newline at end of file diff --git a/Technotes/HelpBook/5.0/en/import-opml.markdown b/Technotes/HelpBook/5.0/en/import-opml.markdown new file mode 100644 index 000000000..ca495ae98 --- /dev/null +++ b/Technotes/HelpBook/5.0/en/import-opml.markdown @@ -0,0 +1,16 @@ +@title How to import OPML + +# How to import OPML + +You can use an OPML subscription list to import your subscriptions from another app or service into NetNewsWire. + +First you need to get an OPML file. This should be pretty easy – look for export options in the app or the service’s web site. + +Once you’ve got the OPML file, NetNewsWire can make quick work of importing the items within. + +1. From the menu bar, select **File → Import Subscriptions…** +2. If you have multiple accounts, select which account to receive the new subscriptions +3. Navigate to the OPML file’s location +4. Select it and click **Open** + +NetNewsWire won’t replace your current subscription list. The new subscriptions will be added in addition to your current ones. \ No newline at end of file diff --git a/Technotes/HelpBook/5.0/en/index.markdown b/Technotes/HelpBook/5.0/en/index.markdown index c50ccd112..f1ba76010 100644 --- a/Technotes/HelpBook/5.0/en/index.markdown +++ b/Technotes/HelpBook/5.0/en/index.markdown @@ -5,11 +5,11 @@ More topics to do… -How to add a feed +[How to add a feed](adding-feeds.html) -Installing and using the Safari Extension to add feeds +[Installing and using the Safari Extension to add feeds](safari-extension.html) -How to import OPML +[How to import OPML](import-opml.html) How to export OPML @@ -27,7 +27,7 @@ How to get NetNewsWire news (see Help menu command) Keyboard shortcuts (see Help menu command) -About the On My Mac account +[About the On My Mac account](on-my-mac.html) Privacy (including link to privacy policy) diff --git a/Technotes/HelpBook/5.0/en/on-my-mac.markdown b/Technotes/HelpBook/5.0/en/on-my-mac.markdown new file mode 100644 index 000000000..e7b06c034 --- /dev/null +++ b/Technotes/HelpBook/5.0/en/on-my-mac.markdown @@ -0,0 +1,14 @@ +@title About the On My Mac account + +# About the On My Mac account + +The On My Mac account is the simplest way to use NetNewsWire. Using it requires no additional service or software. It’s just you, your subscriptions and NetNewsWire. + +On My Mac subscriptions are wholly managed by NetNewsWire. It keeps your subscription list and is responsible for fetching the feeds and checking for updates. This means it also keeps track of what items you’ve read or not. + +The On My Mac account does not sync this data to any other location. It works best for those people who only read NetNewsWire feeds on one Mac and nowhere else. + + +## Refreshing On My Mac feeds + +The feeds in the On My Mac account will be refreshed automatically whenever you open NetNewsWire. If left open, NetNewsWire will refresh your feeds every hour, or according to the schedule you set in Preferences. You can also disable all automatic refreshing from there. \ No newline at end of file diff --git a/Technotes/HelpBook/5.0/en/safari-extension.markdown b/Technotes/HelpBook/5.0/en/safari-extension.markdown new file mode 100644 index 000000000..71d80ca86 --- /dev/null +++ b/Technotes/HelpBook/5.0/en/safari-extension.markdown @@ -0,0 +1,32 @@ +@title Installing and using the Safari Extension to add feeds + +# Installing and using the Safari Extension to add feeds + +NetNewsWire provides a Safari Extension which adds a ‘Subscribe to Feed’ button to your Safari toolbar. This allows you to quickly add a site’s feed without entering an address manually into NetNewsWire. + + +## Installing the NetNewsWire Safari Extension + +The Safari Extension is installed automatically with NetNewsWire. However, it must be *enabled* before you can use it. + +You will enable the extension in Safari: + +1. Open Safari +2. Click on the **Safari** menu and select **Preferences…** (⌘,) +3. Click the **Extensions** panel +4. From the list, click the checkbox beside **Subscribe to Feed** to enable the extension +5. Close the Preferences window + +Once this is done, the ‘Subscribe to Feed’ button will be added to your Safari toolbar. + + +## Adding a feed using the Safari Extension + +For any site that advertises its feeds, you can use the ‘Subscribe to Feed’ button. Clicking it send the feed’s address to NetNewsWire where you can set options like an alternative feed name, and the account and folder where it will be stored. + + +### What to do if the ‘Subscribe to Feed’ button is greyed out and disabled + +The ‘Subscribe in NetNewsWire’ button will only be enabled for sites that advertise their feeds in their code. If the button is disabled, NetNewsWire wasn’t able to find any feeds automatically. + +You may be able to find a feed manually by visiting the site. There, look for a link to an RSS, Atom or JSON feed. If one exists, you can [add this URL to NetNewsWire directly](adding-feeds.html). Right-click on the link and copy the URL to paste into NetNewsWire. \ No newline at end of file diff --git a/iOS/AppCoordinator.swift b/iOS/AppCoordinator.swift index 958d0a393..1b35fe087 100644 --- a/iOS/AppCoordinator.swift +++ b/iOS/AppCoordinator.swift @@ -21,9 +21,26 @@ public extension Notification.Name { static let ArticleSelectionDidChange = Notification.Name(rawValue: "ArticleSelectionDidChange") } -class AppCoordinator { - - static let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5) +class AppCoordinator: NSObject, UndoableCommandRunner { + + var undoableCommands = [UndoableCommand]() + var undoManager: UndoManager? { + return rootSplitViewController.undoManager + } + + private var rootSplitViewController: UISplitViewController! + private var masterNavigationController: UINavigationController! + private var masterFeedViewController: MasterFeedViewController! + private var masterTimelineViewController: MasterTimelineViewController? + + private var detailViewController: DetailViewController? { + if let detailNavController = targetSplitForDetail().viewControllers.last as? UINavigationController { + return detailNavController.topViewController as? DetailViewController + } + return nil + } + + private let fetchAndMergeArticlesQueue = CoalescingQueue(name: "Fetch and Merge Articles", interval: 0.5) private var articleRowMap = [String: Int]() // articleID: rowIndex @@ -40,10 +57,14 @@ class AppCoordinator { } private let treeControllerDelegate = FeedTreeControllerDelegate() - lazy var treeController: TreeController = { + private(set) lazy var treeController: TreeController = { return TreeController(delegate: treeControllerDelegate) }() + var isThreePanelMode: Bool { + return !rootSplitViewController.isCollapsed && rootSplitViewController.displayMode == .allVisible + } + var rootNode: Node { return treeController.rootNode } @@ -52,7 +73,7 @@ class AppCoordinator { return shadowTable.count } - var currentMasterIndexPath: IndexPath? { + private(set) var currentMasterIndexPath: IndexPath? { didSet { guard let ip = currentMasterIndexPath, let node = nodeFor(ip) else { assertionFailure() @@ -82,9 +103,8 @@ class AppCoordinator { } } - - var showFeedNames = false - var showAvatars = false + private(set) var showFeedNames = false + private(set) var showAvatars = false var isPrevArticleAvailable: Bool { guard let indexPath = currentArticleIndexPath else { @@ -130,7 +150,7 @@ class AppCoordinator { return nil } - var currentArticleIndexPath: IndexPath? { + private(set) var currentArticleIndexPath: IndexPath? { didSet { if currentArticleIndexPath != oldValue { NotificationCenter.default.post(name: .ArticleSelectionDidChange, object: self, userInfo: nil) @@ -138,7 +158,7 @@ class AppCoordinator { } } - var articles = ArticleArray() { + private(set) var articles = ArticleArray() { didSet { if articles == oldValue { return @@ -165,8 +185,9 @@ class AppCoordinator { return appDelegate.unreadCount > 0 } - init() { - + override init() { + super.init() + for section in treeController.rootNode.childNodes { expandedNodes.append(section) shadowTable.append([Node]()) @@ -182,7 +203,23 @@ class AppCoordinator { NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) + } + + func start() -> UIViewController { + rootSplitViewController = UISplitViewController.template() + rootSplitViewController.delegate = self + masterNavigationController = (rootSplitViewController.viewControllers.first as! UINavigationController) + masterNavigationController.delegate = self + masterFeedViewController = UIStoryboard.main.instantiateController(ofType: MasterFeedViewController.self) + masterFeedViewController.coordinator = self + masterNavigationController.pushViewController(masterFeedViewController, animated: false) + + let systemMessageViewController = UIStoryboard.main.instantiateController(ofType: SystemMessageViewController.self) + let controller = addNavControllerIfNecessary(systemMessageViewController, split: rootSplitViewController, showBackButton: true) + rootSplitViewController.showDetailViewController(controller, sender: self) + + return rootSplitViewController } // MARK: Notifications @@ -392,7 +429,6 @@ class AppCoordinator { } func indexesForArticleIDs(_ articleIDs: Set) -> IndexSet { - var indexes = IndexSet() articleIDs.forEach { (articleID) in @@ -407,6 +443,61 @@ class AppCoordinator { return indexes } + func selectFeed(_ indexPath: IndexPath) { + if let _ = navControllerForTimeline().viewControllers.first as? MasterTimelineViewController { + currentMasterIndexPath = indexPath + } else { + masterTimelineViewController = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) + masterTimelineViewController!.coordinator = self + currentMasterIndexPath = indexPath + navControllerForTimeline().pushViewController(masterTimelineViewController!, animated: true) + } + + if isThreePanelMode { + let systemMessageViewController = UIStoryboard.main.instantiateController(ofType: SystemMessageViewController.self) + let targetSplitController = targetSplitForDetail() + let controller = addNavControllerIfNecessary(systemMessageViewController, split: targetSplitController, showBackButton: false) + targetSplitController.showDetailViewController(controller, sender: self) + } + } + + func selectArticle(_ indexPath: IndexPath) { + if detailViewController != nil { + currentArticleIndexPath = indexPath + } else { + let targetSplit = targetSplitForDetail() + + let detailViewController = UIStoryboard.main.instantiateController(ofType: DetailViewController.self) + detailViewController.coordinator = self + + let showBackButton = rootSplitViewController.displayMode != .allVisible + let controller = addNavControllerIfNecessary(detailViewController, split: targetSplit, showBackButton: showBackButton) + currentArticleIndexPath = indexPath + + targetSplit.showDetailViewController(controller, sender: self) + } + + // Automatically hide the overlay + if rootSplitViewController.displayMode == .primaryOverlay { + UIView.animate(withDuration: 0.3) { + self.rootSplitViewController.preferredDisplayMode = .primaryHidden + } + rootSplitViewController.preferredDisplayMode = .automatic + } + } + + func selectPrevArticle() { + if let indexPath = prevArticleIndexPath { + selectArticle(indexPath) + } + } + + func selectNextArticle() { + if let indexPath = nextArticleIndexPath { + selectArticle(indexPath) + } + } + func selectNextUnread() { // This should never happen, but I don't want to risk throwing us @@ -424,6 +515,97 @@ class AppCoordinator { } + func markAllAsRead() { + let accounts = AccountManager.shared.activeAccounts + var articles = Set
() + accounts.forEach { account in + articles.formUnion(account.fetchArticles(.unread)) + } + + guard let undoManager = undoManager, + let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else { + return + } + + runCommand(markReadCommand) + } + + func markAllAsReadInTimeline() { + guard let undoManager = undoManager, + let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else { + return + } + runCommand(markReadCommand) + masterNavigationController.popViewController(animated: true) + } + + func toggleReadForCurrentArticle() { + if let article = currentArticle { + markArticles(Set([article]), statusKey: .read, flag: !article.status.read) + } + } + + func toggleStarForCurrentArticle() { + if let article = currentArticle { + markArticles(Set([article]), statusKey: .starred, flag: !article.status.starred) + } + } + + func showSettings() { + let settingsNavViewController = UIStoryboard.settings.instantiateInitialViewController() as! UINavigationController + settingsNavViewController.modalPresentationStyle = .formSheet + let settingsViewController = settingsNavViewController.topViewController as! SettingsViewController + settingsViewController.presentingParentController = masterFeedViewController + masterFeedViewController.present(settingsNavViewController, animated: true) + + // let settings = UIHostingController(rootView: SettingsView(viewModel: SettingsView.ViewModel())) + // self.present(settings, animated: true) + } + + func showAdd() { + let addViewController = UIStoryboard.add.instantiateInitialViewController()! + addViewController.modalPresentationStyle = .formSheet + addViewController.preferredContentSize = AddContainerViewController.preferredContentSizeForFormSheetDisplay + masterFeedViewController.present(addViewController, animated: true) + } + + func showBrowserForCurrentArticle() { + guard let preferredLink = currentArticle?.preferredLink, let url = URL(string: preferredLink) else { + return + } + UIApplication.shared.open(url, options: [:]) + } + + func showActivityDialogForCurrentArticle() { + guard let detailViewController = detailViewController else { + return + } + guard let preferredLink = currentArticle?.preferredLink, let url = URL(string: preferredLink) else { + return + } + + let itemSource = ArticleActivityItemSource(url: url, subject: currentArticle?.title) + let activityViewController = UIActivityViewController(activityItems: [itemSource], applicationActivities: nil) + + activityViewController.popoverPresentationController?.barButtonItem = detailViewController.actionBarButtonItem + detailViewController.present(activityViewController, animated: true) + } + +} + +// MARK: UINavigationControllerDelegate + +extension AppCoordinator: UINavigationControllerDelegate { + + func navigationController(_ navigationController: UINavigationController, didShow viewController: UIViewController, animated: Bool) { + if rootSplitViewController.isCollapsed != true && navigationController.viewControllers.count == 1 { + let systemMessageViewController = UIStoryboard.main.instantiateController(ofType: SystemMessageViewController.self) + let showBackButton = rootSplitViewController.displayMode != .allVisible + let controller = addNavControllerIfNecessary(systemMessageViewController, split: rootSplitViewController, showBackButton: showBackButton) + rootSplitViewController.showDetailViewController(controller, sender: self) + } + } + } // MARK: UISplitViewControllerDelegate @@ -608,7 +790,7 @@ private extension AppCoordinator { } func queueFetchAndMergeArticles() { - AppCoordinator.fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles)) + fetchAndMergeArticlesQueue.add(self, #selector(fetchAndMergeArticles)) } @objc func fetchAndMergeArticles() { @@ -660,4 +842,52 @@ private extension AppCoordinator { } + // MARK: Double Split + + func addNavControllerIfNecessary(_ controller: UIViewController, split: UISplitViewController, showBackButton: Bool) -> UIViewController { + if split.isCollapsed { + return controller + } else { + let navController = UINavigationController(rootViewController: controller) + navController.isToolbarHidden = false + if showBackButton { + controller.navigationItem.leftBarButtonItem = split.displayModeButtonItem + controller.navigationItem.leftItemsSupplementBackButton = true + } + return navController + } + } + + func ensureDoubleSplit() -> UISplitViewController { + if let subSplit = rootSplitViewController.viewControllers.last as? UISplitViewController { + return subSplit + } + + rootSplitViewController.preferredPrimaryColumnWidthFraction = 0.33 + + let subSplit = UISplitViewController.template() + subSplit.delegate = self + subSplit.preferredDisplayMode = .allVisible + subSplit.preferredPrimaryColumnWidthFraction = 0.5 + rootSplitViewController.showDetailViewController(subSplit, sender: self) + return subSplit + } + + func navControllerForTimeline() -> UINavigationController { + if isThreePanelMode { + let subSplit = ensureDoubleSplit() + return subSplit.viewControllers.first as! UINavigationController + } else { + return masterNavigationController + } + } + + func targetSplitForDetail() -> UISplitViewController { + if isThreePanelMode { + return ensureDoubleSplit() + } else { + return rootSplitViewController + } + } + } diff --git a/iOS/Base.lproj/Main.storyboard b/iOS/Base.lproj/Main.storyboard index b09a0a793..d1120fc2b 100644 --- a/iOS/Base.lproj/Main.storyboard +++ b/iOS/Base.lproj/Main.storyboard @@ -1,32 +1,12 @@ - + - + - - - - - - - - - - - - - - - - - - - - @@ -36,7 +16,7 @@ - + @@ -137,7 +117,39 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -155,9 +167,6 @@ - - - @@ -189,24 +198,10 @@ - - - - - - - - - - - - - - - + @@ -254,26 +249,6 @@ - - - - - - - - - - - - - - - - - - - - @@ -284,7 +259,4 @@ - - - diff --git a/iOS/Detail/DetailViewController.swift b/iOS/Detail/DetailViewController.swift index 936684f15..2f5e85c71 100644 --- a/iOS/Detail/DetailViewController.swift +++ b/iOS/Detail/DetailViewController.swift @@ -23,7 +23,7 @@ class DetailViewController: UIViewController { @IBOutlet weak var browserBarButtonItem: UIBarButtonItem! @IBOutlet weak var webView: WKWebView! - weak var coordinator: AppCoordinator? + weak var coordinator: AppCoordinator! override func viewDidLoad() { @@ -47,14 +47,14 @@ class DetailViewController: UIViewController { } func markAsRead() { - if let article = coordinator?.currentArticle { + if let article = coordinator.currentArticle { markArticles(Set([article]), statusKey: .read, flag: true) } } func updateUI() { - guard let article = coordinator?.currentArticle else { + guard let article = coordinator.currentArticle else { nextUnreadBarButtonItem.isEnabled = false prevArticleBarButtonItem.isEnabled = false nextArticleBarButtonItem.isEnabled = false @@ -65,9 +65,9 @@ class DetailViewController: UIViewController { return } - nextUnreadBarButtonItem.isEnabled = coordinator?.isAnyUnreadAvailable ?? false - prevArticleBarButtonItem.isEnabled = coordinator?.isPrevArticleAvailable ?? false - nextArticleBarButtonItem.isEnabled = coordinator?.isNextArticleAvailable ?? false + nextUnreadBarButtonItem.isEnabled = coordinator.isAnyUnreadAvailable + prevArticleBarButtonItem.isEnabled = coordinator.isPrevArticleAvailable + nextArticleBarButtonItem.isEnabled = coordinator.isNextArticleAvailable readBarButtonItem.isEnabled = true starBarButtonItem.isEnabled = true @@ -80,7 +80,7 @@ class DetailViewController: UIViewController { let starImage = article.status.starred ? AppAssets.starClosedImage : AppAssets.starOpenImage starBarButtonItem.image = starImage - if let timelineName = coordinator?.timelineName { + if let timelineName = coordinator.timelineName { if navigationController?.navigationItem.backBarButtonItem?.title != timelineName { let backItem = UIBarButtonItem(title: timelineName, style: .plain, target: nil, action: nil) navigationController?.navigationItem.backBarButtonItem = backItem @@ -91,7 +91,7 @@ class DetailViewController: UIViewController { func reloadHTML() { - guard let article = coordinator?.currentArticle, let webView = webView else { + guard let article = coordinator.currentArticle, let webView = webView else { return } let style = ArticleStylesManager.shared.currentStyle @@ -110,7 +110,7 @@ class DetailViewController: UIViewController { guard let articles = note.userInfo?[Account.UserInfoKey.articles] as? Set
else { return } - if articles.count == 1 && articles.first?.articleID == coordinator?.currentArticle?.articleID { + if articles.count == 1 && articles.first?.articleID == coordinator.currentArticle?.articleID { updateUI() } } @@ -132,45 +132,31 @@ class DetailViewController: UIViewController { // MARK: Actions @IBAction func nextUnread(_ sender: Any) { - coordinator?.selectNextUnread() + coordinator.selectNextUnread() } @IBAction func prevArticle(_ sender: Any) { - coordinator?.currentArticleIndexPath = coordinator?.prevArticleIndexPath + coordinator.selectPrevArticle() } @IBAction func nextArticle(_ sender: Any) { - coordinator?.currentArticleIndexPath = coordinator?.nextArticleIndexPath + coordinator.selectNextArticle() } @IBAction func toggleRead(_ sender: Any) { - if let article = coordinator?.currentArticle { - markArticles(Set([article]), statusKey: .read, flag: !article.status.read) - } + coordinator.toggleReadForCurrentArticle() } @IBAction func toggleStar(_ sender: Any) { - if let article = coordinator?.currentArticle { - markArticles(Set([article]), statusKey: .starred, flag: !article.status.starred) - } + coordinator.toggleStarForCurrentArticle() } @IBAction func openBrowser(_ sender: Any) { - guard let preferredLink = coordinator?.currentArticle?.preferredLink, let url = URL(string: preferredLink) else { - return - } - UIApplication.shared.open(url, options: [:]) + coordinator.showBrowserForCurrentArticle() } @IBAction func showActivityDialog(_ sender: Any) { - guard let preferredLink = coordinator?.currentArticle?.preferredLink, let url = URL(string: preferredLink) else { - return - } - let itemSource = ArticleActivityItemSource(url: url, subject: coordinator?.currentArticle?.title) - let activityViewController = UIActivityViewController(activityItems: [itemSource], applicationActivities: nil) - activityViewController.popoverPresentationController?.barButtonItem = self.actionBarButtonItem - - present(activityViewController, animated: true) + coordinator.showActivityDialogForCurrentArticle() } } diff --git a/iOS/Detail/SystemMessageViewController.swift b/iOS/Detail/SystemMessageViewController.swift new file mode 100644 index 000000000..cafd2397e --- /dev/null +++ b/iOS/Detail/SystemMessageViewController.swift @@ -0,0 +1,21 @@ +// +// SystemMessageViewController.swift +// NetNewsWire-iOS +// +// Created by Maurice Parker on 7/18/19. +// Copyright © 2019 Ranchero Software. All rights reserved. +// + +import UIKit + +class SystemMessageViewController: UIViewController { + + @IBOutlet weak var messageLabel: UILabel! + var message: String = NSLocalizedString("No Selection", comment: "No Selection") + + override func viewDidLoad() { + super.viewDidLoad() + messageLabel.text = message + } + +} diff --git a/iOS/Extensions/UISplitViewController-Extensions.swift b/iOS/Extensions/UISplitViewController-Extensions.swift index 98fa33c5d..2f3561ea0 100644 --- a/iOS/Extensions/UISplitViewController-Extensions.swift +++ b/iOS/Extensions/UISplitViewController-Extensions.swift @@ -10,11 +10,15 @@ import UIKit extension UISplitViewController { - func toggleMasterView() { - let barButtonItem = self.displayModeButtonItem - if let action = barButtonItem.action { - UIApplication.shared.sendAction(action, to: barButtonItem.target, from: nil, for: nil) - } + static func template() -> UISplitViewController { + let splitViewController = UISplitViewController() + splitViewController.preferredDisplayMode = .automatic + + let navController = UINavigationController() + navController.isToolbarHidden = false + splitViewController.viewControllers = [navController] + + return splitViewController } } diff --git a/iOS/MasterFeed/MasterFeedViewController.swift b/iOS/MasterFeed/MasterFeedViewController.swift index f07b89780..9d1234d63 100644 --- a/iOS/MasterFeed/MasterFeedViewController.swift +++ b/iOS/MasterFeed/MasterFeedViewController.swift @@ -13,7 +13,7 @@ import RSCore import RSTree import SwiftUI -class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunner { +class MasterFeedViewController: UITableViewController, UndoableCommandRunner { @IBOutlet private weak var markAllAsReadButton: UIBarButtonItem! @IBOutlet private weak var addNewItemButton: UIBarButtonItem! @@ -44,7 +44,8 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn NotificationCenter.default.addObserver(self, selector: #selector(userDidAddFeed(_:)), name: .UserDidAddFeed, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .AccountsDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil) - + NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(backingStoresDidRebuild(_:)), name: .BackingStoresDidRebuild, object: coordinator) NotificationCenter.default.addObserver(self, selector: #selector(masterSelectionDidChange(_:)), name: .MasterSelectionDidChange, object: coordinator) NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) @@ -65,6 +66,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) becomeFirstResponder() + navigationController?.updateAccountRefreshProgressIndicator() } override func viewWillDisappear(_ animated: Bool) { @@ -101,7 +103,9 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn return } - tableView.reloadRows(at: [indexPath], with: .automatic) + performBlockAndRestoreSelection { + tableView.reloadRows(at: [indexPath], with: .automatic) + } } @@ -158,6 +162,10 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn updateUI() } + @objc func progressDidChange(_ note: Notification) { + navigationController?.updateAccountRefreshProgressIndicator() + } + @objc func masterSelectionDidChange(_ note: Notification) { if let indexPath = coordinator.currentMasterIndexPath { if tableView.indexPathForSelectedRow != indexPath { @@ -276,12 +284,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - - let timeline = UIStoryboard.main.instantiateController(ofType: MasterTimelineViewController.self) - timeline.coordinator = coordinator - coordinator.currentMasterIndexPath = indexPath - self.navigationController?.pushViewController(timeline, animated: true) - + coordinator.selectFeed(indexPath) } override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { @@ -398,18 +401,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn // MARK: Actions @IBAction func settings(_ sender: UIBarButtonItem) { - - let settingsNavViewController = UIStoryboard.settings.instantiateInitialViewController() as! UINavigationController - settingsNavViewController.modalPresentationStyle = .formSheet - - let settingsViewController = settingsNavViewController.topViewController as! SettingsViewController - settingsViewController.presentingParentController = self - - self.present(settingsNavViewController, animated: true) - -// let settings = UIHostingController(rootView: SettingsView(viewModel: SettingsView.ViewModel())) -// self.present(settings, animated: true) - + coordinator.showSettings() } @IBAction func markAllAsRead(_ sender: Any) { @@ -424,20 +416,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn let markTitle = NSLocalizedString("Mark All Read", comment: "Mark All Read") let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in - - let accounts = AccountManager.shared.activeAccounts - var articles = Set
() - accounts.forEach { account in - articles.formUnion(account.fetchUnreadArticles()) - } - - guard let undoManager = self?.undoManager, - let markReadCommand = MarkStatusCommand(initialArticles: Array(articles), markingRead: true, undoManager: undoManager) else { - return - } - - self?.runCommand(markReadCommand) - + self?.coordinator.markAllAsRead() } alertController.addAction(markAction) @@ -447,12 +426,7 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn } @IBAction func add(_ sender: UIBarButtonItem) { - let addViewController = UIStoryboard.add.instantiateInitialViewController()! - addViewController.modalPresentationStyle = .formSheet - addViewController.preferredContentSize = AddContainerViewController.preferredContentSizeForFormSheetDisplay - addViewController.popoverPresentationController?.barButtonItem = sender - - self.present(addViewController, animated: true) + coordinator.showAdd() } @objc func toggleSectionHeader(_ sender: UITapGestureRecognizer) { @@ -630,10 +604,14 @@ extension MasterFeedViewController: MasterFeedTableViewCellDelegate { private extension MasterFeedViewController { @objc private func refreshAccounts(_ sender: Any) { - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self)) refreshControl?.endRefreshing() + // This is a hack to make sure that an error dialog doesn't interfere with dismissing the refreshControl. + // If the error dialog appears too closely to the call to endRefreshing, then the refreshControl never disappears. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self)) + } } - + func updateUI() { markAllAsReadButton.isEnabled = coordinator.isAnyUnreadAvailable addNewItemButton.isEnabled = !AccountManager.shared.activeAccounts.isEmpty @@ -696,4 +674,12 @@ private extension MasterFeedViewController { } } + func performBlockAndRestoreSelection(_ block: (() -> Void)) { + let indexPaths = tableView.indexPathsForSelectedRows + block() + indexPaths?.forEach { [weak self] indexPath in + self?.tableView.selectRow(at: indexPath, animated: false, scrollPosition: .none) + } + } + } diff --git a/iOS/MasterTimeline/MasterTimelineViewController.swift b/iOS/MasterTimeline/MasterTimelineViewController.swift index 4daa34f76..2cc6bbd52 100644 --- a/iOS/MasterTimeline/MasterTimelineViewController.swift +++ b/iOS/MasterTimeline/MasterTimelineViewController.swift @@ -11,7 +11,7 @@ import RSCore import Account import Articles -class MasterTimelineViewController: ProgressTableViewController, UndoableCommandRunner { +class MasterTimelineViewController: UITableViewController, UndoableCommandRunner { private var numberOfTextLines = 0 @@ -36,6 +36,7 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(articlesReinitialized(_:)), name: .ArticlesReinitialized, object: coordinator) NotificationCenter.default.addObserver(self, selector: #selector(articleDataDidChange(_:)), name: .ArticleDataDidChange, object: coordinator) @@ -56,6 +57,7 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand override func viewDidAppear(_ animated: Bool) { super.viewDidAppear(animated) becomeFirstResponder() + updateProgressIndicatorIfNeeded() } override func viewWillDisappear(_ animated: Bool) { @@ -63,16 +65,6 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand resignFirstResponder() } - override func prepare(for segue: UIStoryboardSegue, sender: Any?) { - if segue.identifier == "showDetail" { - let controller = (segue.destination as! UINavigationController).topViewController as! DetailViewController - controller.coordinator = coordinator - controller.navigationItem.leftBarButtonItem = splitViewController?.displayModeButtonItem - controller.navigationItem.leftItemsSupplementBackButton = true - splitViewController?.toggleMasterView() - } - } - override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { super.traitCollectionDidChange(previousTraitCollection) @@ -100,16 +92,7 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand let markTitle = NSLocalizedString("Mark All Read", comment: "Mark All Read") let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in - - guard let articles = self?.coordinator.articles, - let undoManager = self?.undoManager, - let markReadCommand = MarkStatusCommand(initialArticles: articles, markingRead: true, undoManager: undoManager) else { - return - } - self?.runCommand(markReadCommand) - - self?.navigationController?.popViewController(animated: true) - + self?.coordinator.markAllAsReadInTimeline() } alertController.addAction(markAction) @@ -185,7 +168,7 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand } override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { - coordinator.currentArticleIndexPath = indexPath + coordinator.selectArticle(indexPath) } // MARK: Notifications @@ -293,6 +276,10 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand tableView.reloadData() } + @objc func progressDidChange(_ note: Notification) { + updateProgressIndicatorIfNeeded() + } + // MARK: Reloading @objc func reloadAllVisibleCells() { @@ -358,8 +345,12 @@ class MasterTimelineViewController: ProgressTableViewController, UndoableCommand private extension MasterTimelineViewController { @objc private func refreshAccounts(_ sender: Any) { - AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self)) refreshControl?.endRefreshing() + // This is a hack to make sure that an error dialog doesn't interfere with dismissing the refreshControl. + // If the error dialog appears too closely to the call to endRefreshing, then the refreshControl never disappears. + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + AccountManager.shared.refreshAll(errorHandler: ErrorHandler.present(self)) + } } func resetUI() { @@ -367,6 +358,7 @@ private extension MasterTimelineViewController { title = coordinator.timelineName navigationController?.title = coordinator.timelineName + tableView.selectRow(at: nil, animated: false, scrollPosition: .top) if coordinator.articles.count > 0 { tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) } @@ -380,6 +372,12 @@ private extension MasterTimelineViewController { firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable } + func updateProgressIndicatorIfNeeded() { + if !coordinator.isThreePanelMode { + navigationController?.updateAccountRefreshProgressIndicator() + } + } + func configureTimelineCell(_ cell: MasterTimelineTableViewCell, article: Article) { let avatar = avatarFor(article) diff --git a/iOS/Progress/ProgressTableViewController.swift b/iOS/Progress/ProgressTableViewController.swift deleted file mode 100644 index 216f3f127..000000000 --- a/iOS/Progress/ProgressTableViewController.swift +++ /dev/null @@ -1,27 +0,0 @@ -// -// ProgressTableViewController.swift -// NetNewsWire-iOS -// -// Created by Maurice Parker on 4/23/19. -// Copyright © 2019 Ranchero Software. All rights reserved. -// - -import UIKit - -class ProgressTableViewController: UITableViewController { - - override func viewDidLoad() { - super.viewDidLoad() - NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .AccountRefreshProgressDidChange, object: nil) - } - - override func viewDidAppear(_ animated: Bool) { - super.viewDidAppear(animated) - navigationController?.updateAccountRefreshProgressIndicator() - } - - @objc func progressDidChange(_ note: Notification) { - navigationController?.updateAccountRefreshProgressIndicator() - } - -} diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json index 7ca268e55..fa2bbb429 100644 --- a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -3,7 +3,7 @@ { "size" : "20x20", "idiom" : "iphone", - "filename" : "Icon-20@2x.png", + "filename" : "icon-41.png", "scale" : "2x" }, { @@ -51,13 +51,13 @@ { "size" : "20x20", "idiom" : "ipad", - "filename" : "Icon-20.png", + "filename" : "icon-20.png", "scale" : "1x" }, { "size" : "20x20", "idiom" : "ipad", - "filename" : "Icon-20@2x-1.png", + "filename" : "icon-42.png", "scale" : "2x" }, { @@ -75,7 +75,7 @@ { "size" : "40x40", "idiom" : "ipad", - "filename" : "Icon-40.png", + "filename" : "icon-40.png", "scale" : "1x" }, { diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20.png deleted file mode 100644 index 7b1d6f237..000000000 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20.png and /dev/null differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x-1.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x-1.png deleted file mode 100644 index 6d03cae40..000000000 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x-1.png and /dev/null differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png deleted file mode 100644 index 6d03cae40..000000000 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-20@2x.png and /dev/null differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png deleted file mode 100644 index 6d03cae40..000000000 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/Icon-40.png and /dev/null differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-1024.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-1024.png index d7aeeec4e..70538dd0e 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-1024.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-1024.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-120.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-120.png index 05acd7f68..adc262902 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-120.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-120.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-121.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-121.png index 05acd7f68..adc262902 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-121.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-121.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-152.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-152.png index c23cab84b..55e255eb4 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-152.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-152.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-167.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-167.png index 158e8f12c..0dfe02293 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-167.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-167.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-180.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-180.png index 431c44f6c..3eac6a9b0 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-180.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-180.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-20.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-20.png new file mode 100644 index 000000000..3678fb144 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-20.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-29.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-29.png index 26ac42275..07bdaef80 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-29.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-29.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-40.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-40.png new file mode 100644 index 000000000..2e677e352 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-40.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-41.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-41.png new file mode 100644 index 000000000..2e677e352 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-41.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-42.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-42.png new file mode 100644 index 000000000..2e677e352 Binary files /dev/null and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-42.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-58.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-58.png index f15d7f0ef..d12b28b68 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-58.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-58.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-59.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-59.png index f15d7f0ef..d12b28b68 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-59.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-59.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-60.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-60.png index 106982a4d..f39ae88b0 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-60.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-60.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-76.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-76.png index f61507cf8..c19306559 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-76.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-76.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-80.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-80.png index 2f3a49c16..7c3c3cc4e 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-80.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-80.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-81.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-81.png index 2f3a49c16..7c3c3cc4e 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-81.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-81.png differ diff --git a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-87.png b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-87.png index 4ce4cac57..68e270346 100644 Binary files a/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-87.png and b/iOS/Resources/Assets.xcassets/AppIcon.appiconset/icon-87.png differ diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index c61444e05..084d65d94 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -10,27 +10,17 @@ import UIKit class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? var coordinator = AppCoordinator() - var window: UIWindow? - // UIWindowScene delegate func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + window = UIWindow(windowScene: scene as! UIWindowScene) window!.tintColor = AppAssets.netNewsWireBlueColor - - let splitViewController = UIStoryboard.main.instantiateInitialViewController() as! UISplitViewController - splitViewController.delegate = coordinator - window!.rootViewController = splitViewController - - let masterNavigationController = splitViewController.viewControllers[0] as! UINavigationController - let masterFeedViewController = masterNavigationController.topViewController as! MasterFeedViewController - masterFeedViewController.coordinator = coordinator - - let navigationController = splitViewController.viewControllers[splitViewController.viewControllers.count-1] as! UINavigationController - navigationController.topViewController!.navigationItem.leftBarButtonItem = splitViewController.displayModeButtonItem - + window!.rootViewController = coordinator.start() + window!.makeKeyAndVisible() // if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { // if !configure(window: window, with: userActivity) {