|
@ -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
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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(()))
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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.
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,14 +33,5 @@ final class TimelineContainerView: NSView {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
override var isOpaque: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func draw(_ dirtyRect: NSRect) {
|
||||
NSColor.textBackgroundColor.setFill()
|
||||
dirtyRect.fill()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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">
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 */,
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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).
|
|
@ -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.
|
|
@ -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.
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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.
|
|
@ -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.
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
},
|
||||
{
|
||||
|
|
Before Width: | Height: | Size: 612 B |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 758 KiB After Width: | Height: | Size: 775 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 25 KiB |
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.5 KiB |
After Width: | Height: | Size: 2.5 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.4 KiB |
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 6.1 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 7.5 KiB |
|
@ -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) {
|
||||
|
|