Merge pull request #4 from brentsimmons/master

merge from upstream
This commit is contained in:
Olof Hellman 2019-07-27 19:36:54 -07:00 committed by GitHub
commit 22de040d34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
54 changed files with 716 additions and 385 deletions

View File

@ -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

View File

@ -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, Error>) -> Void) {
public func removeFeed(_ feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> 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) {

View File

@ -37,7 +37,7 @@ protocol AccountDelegate {
func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void)
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
func addFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void)
func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void)
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void)
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void)
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void)

View File

@ -396,8 +396,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
}
func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
retrieveCredentialsIfNecessary(account)
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
if feed.folderRelationship?.count ?? 0 > 1 {
deleteTagging(for: account, with: feed, from: container, completion: completion)
} else {

View File

@ -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, Error>) -> Void) {
container?.removeFeed(feed)
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
container.removeFeed(feed)
completion(.success(()))
}

View File

@ -304,7 +304,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
}
func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> 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)
}
}

View File

@ -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, Error>) -> Void) {

View File

@ -57,7 +57,7 @@ public final class ArticlesDatabase {
}
public func fetchTodayArticles(_ feedIDs: Set<String>) -> Set<Article> {
return articlesTable.fetchTodayArticles(feedIDs)
return articlesTable.fetchArticlesSince(feedIDs, todayCutoffDate())
}
public func fetchStarredArticles(_ feedIDs: Set<String>) -> Set<Article> {
@ -83,7 +83,7 @@ public final class ArticlesDatabase {
}
public func fetchTodayArticlesAsync(_ feedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
articlesTable.fetchTodayArticlesAsync(feedIDs, callback)
articlesTable.fetchArticlesSinceAsync(feedIDs, todayCutoffDate(), callback)
}
public func fetchedStarredArticlesAsync(_ feedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
@ -100,6 +100,10 @@ public final class ArticlesDatabase {
articlesTable.fetchUnreadCounts(feedIDs, callback)
}
public func fetchUnreadCountForToday(for feedIDs: Set<String>, callback: @escaping (Int) -> Void) {
fetchUnreadCount(for: feedIDs, since: todayCutoffDate(), callback: callback)
}
public func fetchUnreadCount(for feedIDs: Set<String>, 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.
}
}

View File

@ -102,23 +102,22 @@ final class ArticlesTable: DatabaseTable {
// MARK: - Fetching Today Articles
func fetchTodayArticles(_ feedIDs: Set<String>) -> Set<Article> {
return fetchArticles{ self.fetchTodayArticles(feedIDs, $0) }
func fetchArticlesSince(_ feedIDs: Set<String>, _ cutoffDate: Date) -> Set<Article> {
return fetchArticles{ self.fetchArticlesSince(feedIDs, cutoffDate, $0) }
}
func fetchTodayArticlesAsync(_ feedIDs: Set<String>, _ callback: @escaping ArticleSetBlock) {
fetchArticlesAsync({ self.fetchTodayArticles(feedIDs, $0) }, callback)
func fetchArticlesSinceAsync(_ feedIDs: Set<String>, _ cutoffDate: Date, _ callback: @escaping ArticleSetBlock) {
fetchArticlesAsync({ self.fetchArticlesSince(feedIDs, cutoffDate, $0) }, callback)
}
private func fetchTodayArticles(_ feedIDs: Set<String>, _ database: FMDatabase) -> Set<Article> {
private func fetchArticlesSince(_ feedIDs: Set<String>, _ cutoffDate: Date, _ database: FMDatabase) -> Set<Article> {
// 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<Article>()
}
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)

View File

@ -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()
}
}

View File

@ -33,14 +33,5 @@ final class TimelineContainerView: NSView {
}
}
}
override var isOpaque: Bool {
return true
}
override func draw(_ dirtyRect: NSRect) {
NSColor.textBackgroundColor.setFill()
dirtyRect.fill()
}
}

View File

@ -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()

View File

@ -71,6 +71,7 @@
<enumerator name="feedbin" code="Fdbn" description="A Feedbin account"/>
<enumerator name="feed wrangler" code="FWrg" description="A Feed Wrangler account"/>
<enumerator name="newsblur" code="NBlr" description="A Newsblur account"/>
<enumerator name="fresh rss" code="Frsh" description="A Fresh RSS account"/>
</enumeration>
<class name="account" code="Acct" plural="accounts" description="An account for subscribing to feeds">

View File

@ -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
}
}
}

View File

@ -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 = "<group>"; };
515436892291FED9005E1CDF /* FeedbinAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedbinAccountViewController.swift; sourceTree = "<group>"; };
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 = "<group>"; };
5183CCCF226E1E880010922C /* NonIntrinsicLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicLabel.swift; sourceTree = "<group>"; };
5183CCD9226E31A50010922C /* NonIntrinsicImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NonIntrinsicImageView.swift; sourceTree = "<group>"; };
5183CCDC226F1F5C0010922C /* NavigationProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NavigationProgressView.swift; sourceTree = "<group>"; };
5183CCDE226F1FCC0010922C /* UINavigationController+Progress.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UINavigationController+Progress.swift"; sourceTree = "<group>"; };
5183CCE2226F314C0010922C /* ProgressTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProgressTableViewController.swift; sourceTree = "<group>"; };
5183CCE4226F4DFA0010922C /* RefreshInterval.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshInterval.swift; sourceTree = "<group>"; };
5183CCE7226F68D90010922C /* AccountRefreshTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountRefreshTimer.swift; sourceTree = "<group>"; };
5183CCEC22711DCE0010922C /* Settings.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = Settings.storyboard; sourceTree = "<group>"; };
@ -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 = "<group>";
@ -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 */,

View File

@ -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)
}
}
}

View File

@ -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`.
*Its okay if you dont know that.* NetNewsWire will look at any web page and try to find it for you. All you need to give is the sites 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 youve got the address already great! Type it or paste it into the URL box. Otherwise, just enter the websites 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 thats 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 feeds folder
Before you add a feed, you can choose the account and folder where it will be saved.
This option is especially important if youre 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 cant find a feed
Sometimes NetNewsWire wont be able to find a feed for a site. Either the site doesnt offer a feed, or the feed isnt 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).

View File

@ -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 accounts 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.

View File

@ -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 services web site.
Once youve 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 files location
4. Select it and click **Open**
NetNewsWire wont replace your current subscription list. The new subscriptions will be added in addition to your current ones.

View File

@ -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)

View File

@ -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. Its 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 youve 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.

View File

@ -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 sites 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 feeds 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 wasnt 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.

View File

@ -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<String>) -> 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<Article>()
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
}
}
}

View File

@ -1,32 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14810.12" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="H1p-Uh-vWS">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="14845" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
<device id="retina6_1" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14766.15"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="14799.2"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--Feeds-->
<scene sceneID="pY4-Hu-kfo">
<objects>
<navigationController storyboardIdentifier="MasterNavigationViewController" title="Feeds" useStoryboardIdentifierAsRestorationIdentifier="YES" toolbarHidden="NO" id="RMx-3f-FxP" sceneMemberID="viewController">
<navigationBar key="navigationBar" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" largeTitles="YES" id="Pmd-2v-anx">
<rect key="frame" x="0.0" y="44" width="414" height="96"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="cwK-Lq-cXA">
<rect key="frame" x="0.0" y="813" width="414" height="49"/>
<autoresizingMask key="autoresizingMask"/>
</toolbar>
<connections>
<segue destination="7bK-jq-Zjz" kind="relationship" relationship="rootViewController" id="tsl-Nk-0bq"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="8fS-aE-onr" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="154" y="-759"/>
</scene>
<!--Detail-->
<scene sceneID="yUG-lL-AsK">
<objects>
@ -36,7 +16,7 @@
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<wkWebView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="t8d-md-Yhc">
<rect key="frame" x="0.0" y="88" width="414" height="725"/>
<rect key="frame" x="0.0" y="44" width="414" height="769"/>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
<wkWebViewConfiguration key="configuration">
<audiovisualMediaTypes key="mediaTypesRequiringUserActionForPlayback" none="YES"/>
@ -137,7 +117,39 @@
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="FJe-Yq-33r" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="1151" y="-23"/>
<point key="canvasLocation" x="1619" y="-87"/>
</scene>
<!--System Message-->
<scene sceneID="tbo-yR-QVH">
<objects>
<placeholder placeholderIdentifier="IBFirstResponder" id="rgH-br-nLG" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
<viewController storyboardIdentifier="SystemMessageViewController" title="System Message" id="Xld-e9-xoL" customClass="SystemMessageViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="5hz-HK-J2Q">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="System Message" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6C9-Jb-ZZR">
<rect key="frame" x="141.5" y="437.5" width="131" height="21"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<color key="backgroundColor" xcode11CocoaTouchSystemColor="systemBackgroundColor" cocoaTouchSystemColor="whiteColor"/>
<constraints>
<constraint firstItem="6C9-Jb-ZZR" firstAttribute="centerX" secondItem="5hz-HK-J2Q" secondAttribute="centerX" id="9iO-I1-FtK"/>
<constraint firstItem="6C9-Jb-ZZR" firstAttribute="centerY" secondItem="5hz-HK-J2Q" secondAttribute="centerY" id="jXm-OI-Igs"/>
</constraints>
<viewLayoutGuide key="safeArea" id="zOb-OV-HXy"/>
</view>
<navigationItem key="navigationItem" id="2kf-IY-WDY"/>
<simulatedToolbarMetrics key="simulatedBottomBarMetrics"/>
<connections>
<outlet property="messageLabel" destination="6C9-Jb-ZZR" id="HeR-Qv-yfz"/>
</connections>
</viewController>
</objects>
<point key="canvasLocation" x="872" y="-87"/>
</scene>
<!--Timeline-->
<scene sceneID="fag-XH-avP">
@ -155,9 +167,6 @@
<rect key="frame" x="0.0" y="0.0" width="414" height="208"/>
<autoresizingMask key="autoresizingMask"/>
</tableViewCellContentView>
<connections>
<segue destination="vC3-pB-5Vb" kind="showDetail" identifier="showDetail" id="RT3-gH-cyN"/>
</connections>
</tableViewCell>
</prototypes>
<connections>
@ -189,24 +198,10 @@
</objects>
<point key="canvasLocation" x="1620" y="-759"/>
</scene>
<!--Split View Controller-->
<scene sceneID="Nki-YV-4Qg">
<objects>
<splitViewController storyboardIdentifier="SplitViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" id="H1p-Uh-vWS" sceneMemberID="viewController">
<toolbarItems/>
<connections>
<segue destination="RMx-3f-FxP" kind="relationship" relationship="masterViewController" id="BlO-5A-QYV"/>
<segue destination="vC3-pB-5Vb" kind="relationship" relationship="detailViewController" id="FRG-EO-hQw"/>
</connections>
</splitViewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="cZU-Oi-B1e" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="-856" y="-330"/>
</scene>
<!--Feeds-->
<scene sceneID="smW-Zh-WAh">
<objects>
<tableViewController storyboardIdentifier="MasterViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" clearsSelectionOnViewWillAppear="NO" id="7bK-jq-Zjz" customClass="MasterFeedViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<tableViewController storyboardIdentifier="MasterFeedViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" clearsSelectionOnViewWillAppear="NO" id="7bK-jq-Zjz" customClass="MasterFeedViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="prototypes" style="grouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="r7i-6Z-zg0">
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
@ -254,26 +249,6 @@
</objects>
<point key="canvasLocation" x="900" y="-759"/>
</scene>
<!--Navigation Controller-->
<scene sceneID="r7l-gg-dq7">
<objects>
<navigationController storyboardIdentifier="DetailNavigationViewController" useStoryboardIdentifierAsRestorationIdentifier="YES" toolbarHidden="NO" id="vC3-pB-5Vb" sceneMemberID="viewController">
<navigationBar key="navigationBar" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="scaleToFill" id="DjV-YW-jjY">
<rect key="frame" x="0.0" y="44" width="414" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="Skn-vK-czG">
<rect key="frame" x="0.0" y="813" width="414" height="49"/>
<autoresizingMask key="autoresizingMask"/>
</toolbar>
<connections>
<segue destination="JEX-9P-axG" kind="relationship" relationship="rootViewController" id="GKi-kA-LjT"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="SLD-UC-DBI" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="345" y="-23"/>
</scene>
</scenes>
<resources>
<image name="arrow.down" catalog="system" width="58" height="64"/>
@ -284,7 +259,4 @@
<image name="square.and.arrow.up" catalog="system" width="56" height="64"/>
<image name="star" catalog="system" width="64" height="58"/>
</resources>
<inferredMetricsTieBreakers>
<segue reference="RT3-gH-cyN"/>
</inferredMetricsTieBreakers>
</document>

View File

@ -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<Article> 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()
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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<Article>()
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)
}
}
}

View File

@ -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)

View File

@ -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()
}
}

View File

@ -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"
},
{

Binary file not shown.

Before

Width:  |  Height:  |  Size: 612 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 758 KiB

After

Width:  |  Height:  |  Size: 775 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 24 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 6.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

View File

@ -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) {