Rename Feed protocol to SidebarItem. Rename FeedIdentifier to SidebarItemIdentifier. Rename WebFeed to Feed.
This commit is contained in:
parent
0912bfed18
commit
050c47c41d
@ -57,7 +57,7 @@ public enum FetchType {
|
|||||||
case unread(_: Int? = nil)
|
case unread(_: Int? = nil)
|
||||||
case today(_: Int? = nil)
|
case today(_: Int? = nil)
|
||||||
case folder(Folder, Bool)
|
case folder(Folder, Bool)
|
||||||
case webFeed(WebFeed)
|
case webFeed(Feed)
|
||||||
case articleIDs(Set<String>)
|
case articleIDs(Set<String>)
|
||||||
case search(String)
|
case search(String)
|
||||||
case searchWithArticleIDs(String, Set<String>)
|
case searchWithArticleIDs(String, Set<String>)
|
||||||
@ -143,7 +143,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var topLevelWebFeeds = Set<WebFeed>()
|
public var topLevelWebFeeds = Set<Feed>()
|
||||||
public var folders: Set<Folder>? = Set<Folder>()
|
public var folders: Set<Folder>? = Set<Folder>()
|
||||||
|
|
||||||
public var externalID: String? {
|
public var externalID: String? {
|
||||||
@ -163,15 +163,15 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var webFeedDictionariesNeedUpdate = true
|
private var webFeedDictionariesNeedUpdate = true
|
||||||
private var _idToWebFeedDictionary = [String: WebFeed]()
|
private var _idToWebFeedDictionary = [String: Feed]()
|
||||||
var idToWebFeedDictionary: [String: WebFeed] {
|
var idToWebFeedDictionary: [String: Feed] {
|
||||||
if webFeedDictionariesNeedUpdate {
|
if webFeedDictionariesNeedUpdate {
|
||||||
rebuildWebFeedDictionaries()
|
rebuildWebFeedDictionaries()
|
||||||
}
|
}
|
||||||
return _idToWebFeedDictionary
|
return _idToWebFeedDictionary
|
||||||
}
|
}
|
||||||
private var _externalIDToWebFeedDictionary = [String: WebFeed]()
|
private var _externalIDToWebFeedDictionary = [String: Feed]()
|
||||||
var externalIDToWebFeedDictionary: [String: WebFeed] {
|
var externalIDToWebFeedDictionary: [String: Feed] {
|
||||||
if webFeedDictionariesNeedUpdate {
|
if webFeedDictionariesNeedUpdate {
|
||||||
rebuildWebFeedDictionaries()
|
rebuildWebFeedDictionaries()
|
||||||
}
|
}
|
||||||
@ -214,7 +214,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
|
|
||||||
private var unreadCounts = [String: Int]() // [feedID: Int]
|
private var unreadCounts = [String: Int]() // [feedID: Int]
|
||||||
|
|
||||||
private var _flattenedWebFeeds = Set<WebFeed>()
|
private var _flattenedWebFeeds = Set<Feed>()
|
||||||
private var flattenedWebFeedsNeedUpdate = true
|
private var flattenedWebFeedsNeedUpdate = true
|
||||||
|
|
||||||
private lazy var opmlFile = OPMLFile(filename: (dataFolder as NSString).appendingPathComponent("Subscriptions.opml"), account: self)
|
private lazy var opmlFile = OPMLFile(filename: (dataFolder as NSString).appendingPathComponent("Subscriptions.opml"), account: self)
|
||||||
@ -535,7 +535,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
return existingFolder(withExternalID: externalID)
|
return existingFolder(withExternalID: externalID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func existingContainers(withWebFeed webFeed: WebFeed) -> [Container] {
|
func existingContainers(withWebFeed webFeed: Feed) -> [Container] {
|
||||||
var containers = [Container]()
|
var containers = [Container]()
|
||||||
if topLevelWebFeeds.contains(webFeed) {
|
if topLevelWebFeeds.contains(webFeed) {
|
||||||
containers.append(self)
|
containers.append(self)
|
||||||
@ -586,10 +586,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
return folders?.first(where: { $0.externalID == externalID })
|
return folders?.first(where: { $0.externalID == externalID })
|
||||||
}
|
}
|
||||||
|
|
||||||
func newWebFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> WebFeed {
|
func newWebFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> Feed {
|
||||||
let feedURL = opmlFeedSpecifier.feedURL
|
let feedURL = opmlFeedSpecifier.feedURL
|
||||||
let metadata = webFeedMetadata(feedURL: feedURL, webFeedID: feedURL)
|
let metadata = webFeedMetadata(feedURL: feedURL, webFeedID: feedURL)
|
||||||
let feed = WebFeed(account: self, url: opmlFeedSpecifier.feedURL, metadata: metadata)
|
let feed = Feed(account: self, url: opmlFeedSpecifier.feedURL, metadata: metadata)
|
||||||
if let feedTitle = opmlFeedSpecifier.title {
|
if let feedTitle = opmlFeedSpecifier.title {
|
||||||
if feed.name == nil {
|
if feed.name == nil {
|
||||||
feed.name = feedTitle
|
feed.name = feedTitle
|
||||||
@ -598,35 +598,35 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
return feed
|
return feed
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addWebFeed(_ feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
public func addWebFeed(_ feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
delegate.addWebFeed(for: self, with: feed, to: container, completion: completion)
|
delegate.addWebFeed(for: self, with: feed, to: container, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func createWebFeed(url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
public func createWebFeed(url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
delegate.createWebFeed(for: self, url: url, name: name, container: container, validateFeed: validateFeed, completion: completion)
|
delegate.createWebFeed(for: self, url: url, name: name, container: container, validateFeed: validateFeed, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWebFeed(with name: String?, url: String, webFeedID: String, homePageURL: String?) -> WebFeed {
|
func createWebFeed(with name: String?, url: String, webFeedID: String, homePageURL: String?) -> Feed {
|
||||||
let metadata = webFeedMetadata(feedURL: url, webFeedID: webFeedID)
|
let metadata = webFeedMetadata(feedURL: url, webFeedID: webFeedID)
|
||||||
let feed = WebFeed(account: self, url: url, metadata: metadata)
|
let feed = Feed(account: self, url: url, metadata: metadata)
|
||||||
feed.name = name
|
feed.name = name
|
||||||
feed.homePageURL = homePageURL
|
feed.homePageURL = homePageURL
|
||||||
return feed
|
return feed
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeWebFeed(_ feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
public func removeWebFeed(_ feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
delegate.removeWebFeed(for: self, with: feed, from: container, completion: completion)
|
delegate.removeWebFeed(for: self, with: feed, from: container, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func moveWebFeed(_ feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
public func moveWebFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
delegate.moveWebFeed(for: self, with: feed, from: from, to: to, completion: completion)
|
delegate.moveWebFeed(for: self, with: feed, from: from, to: to, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func renameWebFeed(_ feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
public func renameWebFeed(_ feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
delegate.renameWebFeed(for: self, with: feed, to: name, completion: completion)
|
delegate.renameWebFeed(for: self, with: feed, to: name, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func restoreWebFeed(_ feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
public func restoreWebFeed(_ feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
delegate.restoreWebFeed(for: self, feed: feed, container: container, completion: completion)
|
delegate.restoreWebFeed(for: self, feed: feed, container: container, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -646,7 +646,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
delegate.restoreFolder(for: self, folder: folder, completion: completion)
|
delegate.restoreFolder(for: self, folder: folder, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearWebFeedMetadata(_ feed: WebFeed) {
|
func clearWebFeedMetadata(_ feed: Feed) {
|
||||||
webFeedMetadata[feed.url] = nil
|
webFeedMetadata[feed.url] = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -656,7 +656,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
structureDidChange()
|
structureDidChange()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func updateUnreadCounts(for webFeeds: Set<WebFeed>, completion: VoidCompletionBlock? = nil) {
|
public func updateUnreadCounts(for webFeeds: Set<Feed>, completion: VoidCompletionBlock? = nil) {
|
||||||
fetchUnreadCounts(for: webFeeds, completion: completion)
|
fetchUnreadCounts(for: webFeeds, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -735,11 +735,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
database.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion)
|
database.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate(completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func unreadCount(for webFeed: WebFeed) -> Int {
|
public func unreadCount(for webFeed: Feed) -> Int {
|
||||||
return unreadCounts[webFeed.webFeedID] ?? 0
|
return unreadCounts[webFeed.webFeedID] ?? 0
|
||||||
}
|
}
|
||||||
|
|
||||||
public func setUnreadCount(_ unreadCount: Int, for webFeed: WebFeed) {
|
public func setUnreadCount(_ unreadCount: Int, for webFeed: Feed) {
|
||||||
unreadCounts[webFeed.webFeedID] = unreadCount
|
unreadCounts[webFeed.webFeedID] = unreadCount
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -751,7 +751,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
webFeedDictionariesNeedUpdate = true
|
webFeedDictionariesNeedUpdate = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func update(_ webFeed: WebFeed, with parsedFeed: ParsedFeed, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
func update(_ webFeed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping UpdateArticlesCompletionBlock) {
|
||||||
// Used only by an On My Mac or iCloud account.
|
// Used only by an On My Mac or iCloud account.
|
||||||
precondition(Thread.isMainThread)
|
precondition(Thread.isMainThread)
|
||||||
precondition(type == .onMyMac || type == .cloudKit)
|
precondition(type == .onMyMac || type == .cloudKit)
|
||||||
@ -899,7 +899,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
|
|
||||||
// MARK: - Container
|
// MARK: - Container
|
||||||
|
|
||||||
public func flattenedWebFeeds() -> Set<WebFeed> {
|
public func flattenedWebFeeds() -> Set<Feed> {
|
||||||
assert(Thread.isMainThread)
|
assert(Thread.isMainThread)
|
||||||
if flattenedWebFeedsNeedUpdate {
|
if flattenedWebFeedsNeedUpdate {
|
||||||
updateFlattenedWebFeeds()
|
updateFlattenedWebFeeds()
|
||||||
@ -907,13 +907,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
return _flattenedWebFeeds
|
return _flattenedWebFeeds
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeWebFeed(_ webFeed: WebFeed) {
|
public func removeWebFeed(_ webFeed: Feed) {
|
||||||
topLevelWebFeeds.remove(webFeed)
|
topLevelWebFeeds.remove(webFeed)
|
||||||
structureDidChange()
|
structureDidChange()
|
||||||
postChildrenDidChangeNotification()
|
postChildrenDidChangeNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeFeeds(_ webFeeds: Set<WebFeed>) {
|
public func removeFeeds(_ webFeeds: Set<Feed>) {
|
||||||
guard !webFeeds.isEmpty else {
|
guard !webFeeds.isEmpty else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -922,13 +922,13 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
postChildrenDidChangeNotification()
|
postChildrenDidChangeNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addWebFeed(_ webFeed: WebFeed) {
|
public func addWebFeed(_ webFeed: Feed) {
|
||||||
topLevelWebFeeds.insert(webFeed)
|
topLevelWebFeeds.insert(webFeed)
|
||||||
structureDidChange()
|
structureDidChange()
|
||||||
postChildrenDidChangeNotification()
|
postChildrenDidChangeNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
func addFeedIfNotInAnyFolder(_ webFeed: WebFeed) {
|
func addFeedIfNotInAnyFolder(_ webFeed: Feed) {
|
||||||
if !flattenedWebFeeds().contains(webFeed) {
|
if !flattenedWebFeeds().contains(webFeed) {
|
||||||
addWebFeed(webFeed)
|
addWebFeed(webFeed)
|
||||||
}
|
}
|
||||||
@ -970,7 +970,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func unreadCountDidChange(_ note: Notification) {
|
@objc func unreadCountDidChange(_ note: Notification) {
|
||||||
if let feed = note.object as? WebFeed, feed.account === self {
|
if let feed = note.object as? Feed, feed.account === self {
|
||||||
updateUnreadCount()
|
updateUnreadCount()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1078,13 +1078,13 @@ private extension Account {
|
|||||||
fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion)
|
fetchUnreadArticlesAsync(forContainer: folder, limit: nil, completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchArticles(webFeed: WebFeed) throws -> Set<Article> {
|
func fetchArticles(webFeed: Feed) throws -> Set<Article> {
|
||||||
let articles = try database.fetchArticles(webFeed.webFeedID)
|
let articles = try database.fetchArticles(webFeed.webFeedID)
|
||||||
validateUnreadCount(webFeed, articles)
|
validateUnreadCount(webFeed, articles)
|
||||||
return articles
|
return articles
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchArticlesAsync(webFeed: WebFeed, _ completion: @escaping ArticleSetResultBlock) {
|
func fetchArticlesAsync(webFeed: Feed, _ completion: @escaping ArticleSetResultBlock) {
|
||||||
database.fetchArticlesAsync(webFeed.webFeedID) { [weak self] articleSetResult in
|
database.fetchArticlesAsync(webFeed.webFeedID) { [weak self] articleSetResult in
|
||||||
switch articleSetResult {
|
switch articleSetResult {
|
||||||
case .success(let articles):
|
case .success(let articles):
|
||||||
@ -1120,7 +1120,7 @@ private extension Account {
|
|||||||
return database.fetchArticlesAsync(articleIDs: articleIDs, completion)
|
return database.fetchArticlesAsync(articleIDs: articleIDs, completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchUnreadArticles(webFeed: WebFeed) throws -> Set<Article> {
|
func fetchUnreadArticles(webFeed: Feed) throws -> Set<Article> {
|
||||||
let articles = try database.fetchUnreadArticles(Set([webFeed.webFeedID]), nil)
|
let articles = try database.fetchUnreadArticles(Set([webFeed.webFeedID]), nil)
|
||||||
validateUnreadCount(webFeed, articles)
|
validateUnreadCount(webFeed, articles)
|
||||||
return articles
|
return articles
|
||||||
@ -1178,7 +1178,7 @@ private extension Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateUnreadCountsAfterFetchingUnreadArticles(_ webFeeds: Set<WebFeed>, _ articles: Set<Article>) {
|
func validateUnreadCountsAfterFetchingUnreadArticles(_ webFeeds: Set<Feed>, _ articles: Set<Article>) {
|
||||||
// Validate unread counts. This was the site of a performance slowdown:
|
// Validate unread counts. This was the site of a performance slowdown:
|
||||||
// it was calling going through the entire list of articles once per feed:
|
// it was calling going through the entire list of articles once per feed:
|
||||||
// feeds.forEach { validateUnreadCount($0, articles) }
|
// feeds.forEach { validateUnreadCount($0, articles) }
|
||||||
@ -1194,7 +1194,7 @@ private extension Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateUnreadCount(_ webFeed: WebFeed, _ articles: Set<Article>) {
|
func validateUnreadCount(_ webFeed: Feed, _ articles: Set<Article>) {
|
||||||
// articles must contain all the unread articles for the feed.
|
// articles must contain all the unread articles for the feed.
|
||||||
// The unread number should match the feed’s unread count.
|
// The unread number should match the feed’s unread count.
|
||||||
|
|
||||||
@ -1225,7 +1225,7 @@ private extension Account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateFlattenedWebFeeds() {
|
func updateFlattenedWebFeeds() {
|
||||||
var feeds = Set<WebFeed>()
|
var feeds = Set<Feed>()
|
||||||
feeds.formUnion(topLevelWebFeeds)
|
feeds.formUnion(topLevelWebFeeds)
|
||||||
for folder in folders! {
|
for folder in folders! {
|
||||||
feeds.formUnion(folder.flattenedWebFeeds())
|
feeds.formUnion(folder.flattenedWebFeeds())
|
||||||
@ -1236,8 +1236,8 @@ private extension Account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func rebuildWebFeedDictionaries() {
|
func rebuildWebFeedDictionaries() {
|
||||||
var idDictionary = [String: WebFeed]()
|
var idDictionary = [String: Feed]()
|
||||||
var externalIDDictionary = [String: WebFeed]()
|
var externalIDDictionary = [String: Feed]()
|
||||||
|
|
||||||
flattenedWebFeeds().forEach { (feed) in
|
flattenedWebFeeds().forEach { (feed) in
|
||||||
idDictionary[feed.webFeedID] = feed
|
idDictionary[feed.webFeedID] = feed
|
||||||
@ -1287,7 +1287,7 @@ private extension Account {
|
|||||||
/// Fetch unread counts for zero or more feeds.
|
/// Fetch unread counts for zero or more feeds.
|
||||||
///
|
///
|
||||||
/// Uses the most efficient method based on how many feeds were passed in.
|
/// Uses the most efficient method based on how many feeds were passed in.
|
||||||
func fetchUnreadCounts(for feeds: Set<WebFeed>, completion: VoidCompletionBlock?) {
|
func fetchUnreadCounts(for feeds: Set<Feed>, completion: VoidCompletionBlock?) {
|
||||||
if feeds.isEmpty {
|
if feeds.isEmpty {
|
||||||
completion?()
|
completion?()
|
||||||
return
|
return
|
||||||
@ -1303,7 +1303,7 @@ private extension Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchUnreadCount(_ feed: WebFeed, _ completion: VoidCompletionBlock?) {
|
func fetchUnreadCount(_ feed: Feed, _ completion: VoidCompletionBlock?) {
|
||||||
database.fetchUnreadCount(feed.webFeedID) { result in
|
database.fetchUnreadCount(feed.webFeedID) { result in
|
||||||
if let unreadCount = try? result.get() {
|
if let unreadCount = try? result.get() {
|
||||||
feed.unreadCount = unreadCount
|
feed.unreadCount = unreadCount
|
||||||
@ -1312,7 +1312,7 @@ private extension Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchUnreadCounts(_ feeds: Set<WebFeed>, _ completion: VoidCompletionBlock?) {
|
func fetchUnreadCounts(_ feeds: Set<Feed>, _ completion: VoidCompletionBlock?) {
|
||||||
let webFeedIDs = Set(feeds.map { $0.webFeedID })
|
let webFeedIDs = Set(feeds.map { $0.webFeedID })
|
||||||
database.fetchUnreadCounts(for: webFeedIDs) { result in
|
database.fetchUnreadCounts(for: webFeedIDs) { result in
|
||||||
if let unreadCountDictionary = try? result.get() {
|
if let unreadCountDictionary = try? result.get() {
|
||||||
@ -1342,7 +1342,7 @@ private extension Account {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func processUnreadCounts(unreadCountDictionary: UnreadCountDictionary, feeds: Set<WebFeed>) {
|
func processUnreadCounts(unreadCountDictionary: UnreadCountDictionary, feeds: Set<Feed>) {
|
||||||
for feed in feeds {
|
for feed in feeds {
|
||||||
// When the unread count is zero, it won’t appear in unreadCountDictionary.
|
// When the unread count is zero, it won’t appear in unreadCountDictionary.
|
||||||
let unreadCount = unreadCountDictionary[feed.webFeedID] ?? 0
|
let unreadCount = unreadCountDictionary[feed.webFeedID] ?? 0
|
||||||
@ -1351,7 +1351,7 @@ private extension Account {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func sendNotificationAbout(_ articleChanges: ArticleChanges) {
|
func sendNotificationAbout(_ articleChanges: ArticleChanges) {
|
||||||
var webFeeds = Set<WebFeed>()
|
var webFeeds = Set<Feed>()
|
||||||
|
|
||||||
if let newArticles = articleChanges.newArticles {
|
if let newArticles = articleChanges.newArticles {
|
||||||
webFeeds.formUnion(Set(newArticles.compactMap { $0.webFeed }))
|
webFeeds.formUnion(Set(newArticles.compactMap { $0.webFeed }))
|
||||||
@ -1394,11 +1394,11 @@ private extension Account {
|
|||||||
|
|
||||||
extension Account {
|
extension Account {
|
||||||
|
|
||||||
public func existingWebFeed(withWebFeedID webFeedID: String) -> WebFeed? {
|
public func existingWebFeed(withWebFeedID webFeedID: String) -> Feed? {
|
||||||
return idToWebFeedDictionary[webFeedID]
|
return idToWebFeedDictionary[webFeedID]
|
||||||
}
|
}
|
||||||
|
|
||||||
public func existingWebFeed(withExternalID externalID: String) -> WebFeed? {
|
public func existingWebFeed(withExternalID externalID: String) -> Feed? {
|
||||||
return externalIDToWebFeedDictionary[externalID]
|
return externalIDToWebFeedDictionary[externalID]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -36,13 +36,13 @@ protocol AccountDelegate {
|
|||||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
|
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
|
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
|
||||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void)
|
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void)
|
||||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
|
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
func addWebFeed(for account: Account, with: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
func addWebFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
|
||||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
|
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
|
||||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result<Void, Error>) -> Void)
|
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
@ -197,7 +197,7 @@ public final class AccountManager: UnreadCountProvider {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
public func existingFeed(with feedID: FeedIdentifier) -> Feed? {
|
public func existingFeed(with feedID: SidebarItemIdentifier) -> SidebarItem? {
|
||||||
switch feedID {
|
switch feedID {
|
||||||
case .folder(let accountID, let folderName):
|
case .folder(let accountID, let folderName):
|
||||||
if let account = existingAccount(with: accountID) {
|
if let account = existingAccount(with: accountID) {
|
||||||
|
@ -18,7 +18,7 @@ public protocol ArticleFetcher {
|
|||||||
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock)
|
func fetchUnreadArticlesAsync(_ completion: @escaping ArticleSetResultBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WebFeed: ArticleFetcher {
|
extension Feed: ArticleFetcher {
|
||||||
|
|
||||||
public func fetchArticles() throws -> Set<Article> {
|
public func fetchArticles() throws -> Set<Article> {
|
||||||
return try account?.fetchArticles(.webFeed(self)) ?? Set<Article>()
|
return try account?.fetchArticles(.webFeed(self)) ?? Set<Article>()
|
||||||
|
@ -175,7 +175,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
completion(.failure(LocalAccountDelegateError.invalidParameter))
|
completion(.failure(LocalAccountDelegateError.invalidParameter))
|
||||||
return
|
return
|
||||||
@ -186,7 +186,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
createRSSWebFeed(for: account, url: url, editedName: editedName, container: container, validateFeed: validateFeed, completion: completion)
|
createRSSWebFeed(for: account, url: url, editedName: editedName, container: container, validateFeed: validateFeed, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
let editedName = name.isEmpty ? nil : name
|
let editedName = name.isEmpty ? nil : name
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
accountZone.renameWebFeed(feed, editedName: editedName) { result in
|
accountZone.renameWebFeed(feed, editedName: editedName) { result in
|
||||||
@ -202,7 +202,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
removeWebFeedFromCloud(for: account, with: feed, from: container) { result in
|
removeWebFeedFromCloud(for: account, with: feed, from: container) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
@ -222,7 +222,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveWebFeed(for account: Account, with feed: WebFeed, from fromContainer: Container, to toContainer: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func moveWebFeed(for account: Account, with feed: Feed, from fromContainer: Container, to toContainer: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
accountZone.moveWebFeed(feed, from: fromContainer, to: toContainer) { result in
|
accountZone.moveWebFeed(feed, from: fromContainer, to: toContainer) { result in
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
@ -238,7 +238,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
accountZone.addWebFeed(feed, to: container) { result in
|
accountZone.addWebFeed(feed, to: container) { result in
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
@ -253,7 +253,7 @@ final class CloudKitAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
createWebFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
@ -562,7 +562,7 @@ private extension CloudKitAccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func combinedRefresh(_ account: Account, _ webFeeds: Set<WebFeed>, completion: @escaping (Result<Void, Error>) -> Void) {
|
func combinedRefresh(_ account: Account, _ webFeeds: Set<Feed>, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
|
|
||||||
@ -576,7 +576,7 @@ private extension CloudKitAccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
|
|
||||||
func addDeadFeed() {
|
func addDeadFeed() {
|
||||||
let feed = account.createWebFeed(with: editedName, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
|
let feed = account.createWebFeed(with: editedName, url: url.absoluteString, webFeedID: url.absoluteString, homePageURL: nil)
|
||||||
@ -683,7 +683,7 @@ private extension CloudKitAccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendNewArticlesToTheCloud(_ account: Account, _ feed: WebFeed) {
|
func sendNewArticlesToTheCloud(_ account: Account, _ feed: Feed) {
|
||||||
account.fetchArticlesAsync(.webFeed(feed)) { result in
|
account.fetchArticlesAsync(.webFeed(feed)) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let articles):
|
case .success(let articles):
|
||||||
@ -771,7 +771,7 @@ private extension CloudKitAccountDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func removeWebFeedFromCloud(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func removeWebFeedFromCloud(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||||
accountZone.removeWebFeed(feed, from: container) { result in
|
accountZone.removeWebFeed(feed, from: container) { result in
|
||||||
self.refreshProgress.completeTask()
|
self.refreshProgress.completeTask()
|
||||||
@ -798,7 +798,7 @@ private extension CloudKitAccountDelegate {
|
|||||||
|
|
||||||
extension CloudKitAccountDelegate: LocalAccountRefresherDelegate {
|
extension CloudKitAccountDelegate: LocalAccountRefresherDelegate {
|
||||||
|
|
||||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed) {
|
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: Feed) {
|
||||||
refreshProgress.completeTask()
|
refreshProgress.completeTask()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,7 +119,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Rename the given web feed
|
/// Rename the given web feed
|
||||||
func renameWebFeed(_ webFeed: WebFeed, editedName: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
func renameWebFeed(_ webFeed: Feed, editedName: String?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
guard let externalID = webFeed.externalID else {
|
guard let externalID = webFeed.externalID else {
|
||||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||||
return
|
return
|
||||||
@ -140,7 +140,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Removes a web feed from a container and optionally deletes it, calling the completion with true if deleted
|
/// Removes a web feed from a container and optionally deletes it, calling the completion with true if deleted
|
||||||
func removeWebFeed(_ webFeed: WebFeed, from: Container, completion: @escaping (Result<Bool, Error>) -> Void) {
|
func removeWebFeed(_ webFeed: Feed, from: Container, completion: @escaping (Result<Bool, Error>) -> Void) {
|
||||||
guard let fromContainerExternalID = from.externalID else {
|
guard let fromContainerExternalID = from.externalID else {
|
||||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||||
return
|
return
|
||||||
@ -189,7 +189,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveWebFeed(_ webFeed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func moveWebFeed(_ webFeed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else {
|
guard let fromContainerExternalID = from.externalID, let toContainerExternalID = to.externalID else {
|
||||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||||
return
|
return
|
||||||
@ -211,7 +211,7 @@ final class CloudKitAccountZone: CloudKitZone {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addWebFeed(_ webFeed: WebFeed, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func addWebFeed(_ webFeed: Feed, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
guard let toContainerExternalID = to.externalID else {
|
guard let toContainerExternalID = to.externalID else {
|
||||||
completion(.failure(CloudKitZoneError.corruptAccount))
|
completion(.failure(CloudKitZoneError.corruptAccount))
|
||||||
return
|
return
|
||||||
|
@ -17,7 +17,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
|||||||
|
|
||||||
private typealias UnclaimedWebFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String)
|
private typealias UnclaimedWebFeed = (url: URL, name: String?, editedName: String?, homePageURL: String?, webFeedExternalID: String)
|
||||||
private var newUnclaimedWebFeeds = [String: [UnclaimedWebFeed]]()
|
private var newUnclaimedWebFeeds = [String: [UnclaimedWebFeed]]()
|
||||||
private var existingUnclaimedWebFeeds = [String: [WebFeed]]()
|
private var existingUnclaimedWebFeeds = [String: [Feed]]()
|
||||||
|
|
||||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "CloudKit")
|
||||||
|
|
||||||
@ -140,7 +140,7 @@ class CloudKitAcountZoneDelegate: CloudKitZoneDelegate {
|
|||||||
|
|
||||||
private extension CloudKitAcountZoneDelegate {
|
private extension CloudKitAcountZoneDelegate {
|
||||||
|
|
||||||
func updateWebFeed(_ webFeed: WebFeed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
|
func updateWebFeed(_ webFeed: Feed, name: String?, editedName: String?, homePageURL: String?, containerExternalIDs: [String]) {
|
||||||
guard let account = account else { return }
|
guard let account = account else { return }
|
||||||
|
|
||||||
webFeed.name = name
|
webFeed.name = name
|
||||||
@ -192,12 +192,12 @@ private extension CloudKitAcountZoneDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addExistingUnclaimedWebFeed(_ webFeed: WebFeed, containerExternalID: String) {
|
func addExistingUnclaimedWebFeed(_ webFeed: Feed, containerExternalID: String) {
|
||||||
if var unclaimedWebFeeds = self.existingUnclaimedWebFeeds[containerExternalID] {
|
if var unclaimedWebFeeds = self.existingUnclaimedWebFeeds[containerExternalID] {
|
||||||
unclaimedWebFeeds.append(webFeed)
|
unclaimedWebFeeds.append(webFeed)
|
||||||
self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
||||||
} else {
|
} else {
|
||||||
var unclaimedWebFeeds = [WebFeed]()
|
var unclaimedWebFeeds = [Feed]()
|
||||||
unclaimedWebFeeds.append(webFeed)
|
unclaimedWebFeeds.append(webFeed)
|
||||||
self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
self.existingUnclaimedWebFeeds[containerExternalID] = unclaimedWebFeeds
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ extension Notification.Name {
|
|||||||
public protocol Container: AnyObject, ContainerIdentifiable {
|
public protocol Container: AnyObject, ContainerIdentifiable {
|
||||||
|
|
||||||
var account: Account? { get }
|
var account: Account? { get }
|
||||||
var topLevelWebFeeds: Set<WebFeed> { get set }
|
var topLevelWebFeeds: Set<Feed> { get set }
|
||||||
var folders: Set<Folder>? { get set }
|
var folders: Set<Folder>? { get set }
|
||||||
var externalID: String? { get set }
|
var externalID: String? { get set }
|
||||||
|
|
||||||
@ -29,17 +29,17 @@ public protocol Container: AnyObject, ContainerIdentifiable {
|
|||||||
func hasChildFolder(with: String) -> Bool
|
func hasChildFolder(with: String) -> Bool
|
||||||
func childFolder(with: String) -> Folder?
|
func childFolder(with: String) -> Folder?
|
||||||
|
|
||||||
func removeWebFeed(_ webFeed: WebFeed)
|
func removeWebFeed(_ webFeed: Feed)
|
||||||
func addWebFeed(_ webFeed: WebFeed)
|
func addWebFeed(_ webFeed: Feed)
|
||||||
|
|
||||||
//Recursive — checks subfolders
|
//Recursive — checks subfolders
|
||||||
func flattenedWebFeeds() -> Set<WebFeed>
|
func flattenedWebFeeds() -> Set<Feed>
|
||||||
func has(_ webFeed: WebFeed) -> Bool
|
func has(_ webFeed: Feed) -> Bool
|
||||||
func hasWebFeed(with webFeedID: String) -> Bool
|
func hasWebFeed(with webFeedID: String) -> Bool
|
||||||
func hasWebFeed(withURL url: String) -> Bool
|
func hasWebFeed(withURL url: String) -> Bool
|
||||||
func existingWebFeed(withWebFeedID: String) -> WebFeed?
|
func existingWebFeed(withWebFeedID: String) -> Feed?
|
||||||
func existingWebFeed(withURL url: String) -> WebFeed?
|
func existingWebFeed(withURL url: String) -> Feed?
|
||||||
func existingWebFeed(withExternalID externalID: String) -> WebFeed?
|
func existingWebFeed(withExternalID externalID: String) -> Feed?
|
||||||
func existingFolder(with name: String) -> Folder?
|
func existingFolder(with name: String) -> Folder?
|
||||||
func existingFolder(withID: Int) -> Folder?
|
func existingFolder(withID: Int) -> Folder?
|
||||||
|
|
||||||
@ -69,7 +69,7 @@ public extension Container {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func objectIsChild(_ object: AnyObject) -> Bool {
|
func objectIsChild(_ object: AnyObject) -> Bool {
|
||||||
if let feed = object as? WebFeed {
|
if let feed = object as? Feed {
|
||||||
return topLevelWebFeeds.contains(feed)
|
return topLevelWebFeeds.contains(feed)
|
||||||
}
|
}
|
||||||
if let folder = object as? Folder {
|
if let folder = object as? Folder {
|
||||||
@ -78,8 +78,8 @@ public extension Container {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func flattenedWebFeeds() -> Set<WebFeed> {
|
func flattenedWebFeeds() -> Set<Feed> {
|
||||||
var feeds = Set<WebFeed>()
|
var feeds = Set<Feed>()
|
||||||
feeds.formUnion(topLevelWebFeeds)
|
feeds.formUnion(topLevelWebFeeds)
|
||||||
if let folders = folders {
|
if let folders = folders {
|
||||||
for folder in folders {
|
for folder in folders {
|
||||||
@ -97,11 +97,11 @@ public extension Container {
|
|||||||
return existingWebFeed(withURL: url) != nil
|
return existingWebFeed(withURL: url) != nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func has(_ webFeed: WebFeed) -> Bool {
|
func has(_ webFeed: Feed) -> Bool {
|
||||||
return flattenedWebFeeds().contains(webFeed)
|
return flattenedWebFeeds().contains(webFeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func existingWebFeed(withWebFeedID webFeedID: String) -> WebFeed? {
|
func existingWebFeed(withWebFeedID webFeedID: String) -> Feed? {
|
||||||
for feed in flattenedWebFeeds() {
|
for feed in flattenedWebFeeds() {
|
||||||
if feed.webFeedID == webFeedID {
|
if feed.webFeedID == webFeedID {
|
||||||
return feed
|
return feed
|
||||||
@ -110,7 +110,7 @@ public extension Container {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func existingWebFeed(withURL url: String) -> WebFeed? {
|
func existingWebFeed(withURL url: String) -> Feed? {
|
||||||
for feed in flattenedWebFeeds() {
|
for feed in flattenedWebFeeds() {
|
||||||
if feed.url == url {
|
if feed.url == url {
|
||||||
return feed
|
return feed
|
||||||
@ -119,7 +119,7 @@ public extension Container {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func existingWebFeed(withExternalID externalID: String) -> WebFeed? {
|
func existingWebFeed(withExternalID externalID: String) -> Feed? {
|
||||||
for feed in flattenedWebFeeds() {
|
for feed in flattenedWebFeeds() {
|
||||||
if feed.externalID == externalID {
|
if feed.externalID == externalID {
|
||||||
return feed
|
return feed
|
||||||
|
@ -14,7 +14,7 @@ public extension Notification.Name {
|
|||||||
static let WebFeedSettingDidChange = Notification.Name(rawValue: "FeedSettingDidChangeNotification")
|
static let WebFeedSettingDidChange = Notification.Name(rawValue: "FeedSettingDidChangeNotification")
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension WebFeed {
|
public extension Feed {
|
||||||
|
|
||||||
static let WebFeedSettingUserInfoKey = "feedSetting"
|
static let WebFeedSettingUserInfoKey = "feedSetting"
|
||||||
|
|
||||||
@ -30,7 +30,7 @@ public extension WebFeed {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WebFeed {
|
extension Feed {
|
||||||
|
|
||||||
func takeSettings(from parsedFeed: ParsedFeed) {
|
func takeSettings(from parsedFeed: ParsedFeed) {
|
||||||
iconURL = parsedFeed.iconURL
|
iconURL = parsedFeed.iconURL
|
||||||
@ -41,7 +41,7 @@ extension WebFeed {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func postFeedSettingDidChangeNotification(_ codingKey: WebFeedMetadata.CodingKeys) {
|
func postFeedSettingDidChangeNotification(_ codingKey: WebFeedMetadata.CodingKeys) {
|
||||||
let userInfo = [WebFeed.WebFeedSettingUserInfoKey: codingKey.stringValue]
|
let userInfo = [Feed.WebFeedSettingUserInfoKey: codingKey.stringValue]
|
||||||
NotificationCenter.default.post(name: .WebFeedSettingDidChange, object: self, userInfo: userInfo)
|
NotificationCenter.default.post(name: .WebFeedSettingDidChange, object: self, userInfo: userInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -56,7 +56,7 @@ public extension Article {
|
|||||||
return manager.existingAccount(with: accountID)
|
return manager.existingAccount(with: accountID)
|
||||||
}
|
}
|
||||||
|
|
||||||
var webFeed: WebFeed? {
|
var webFeed: Feed? {
|
||||||
return account?.existingWebFeed(withWebFeedID: webFeedID)
|
return account?.existingWebFeed(withWebFeedID: webFeedID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,39 +1,322 @@
|
|||||||
//
|
//
|
||||||
// Feed.swift
|
// WebFeed.swift
|
||||||
// Account
|
// NetNewsWire
|
||||||
//
|
//
|
||||||
// Created by Maurice Parker on 11/15/19.
|
// Created by Brent Simmons on 7/1/17.
|
||||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
import RSCore
|
import RSCore
|
||||||
|
import RSWeb
|
||||||
|
import Articles
|
||||||
|
|
||||||
public enum ReadFilterType {
|
public final class Feed: SidebarItem, Renamable, Hashable {
|
||||||
case read
|
|
||||||
case none
|
public var defaultReadFilterType: ReadFilterType {
|
||||||
case alwaysRead
|
return .none
|
||||||
|
}
|
||||||
|
|
||||||
|
public var sidebarItemID: SidebarItemIdentifier? {
|
||||||
|
guard let accountID = account?.accountID else {
|
||||||
|
assertionFailure("Expected feed.account, but got nil.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return SidebarItemIdentifier.webFeed(accountID, webFeedID)
|
||||||
|
}
|
||||||
|
|
||||||
|
public weak var account: Account?
|
||||||
|
public let url: String
|
||||||
|
|
||||||
|
public var webFeedID: String {
|
||||||
|
get {
|
||||||
|
return metadata.webFeedID
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
metadata.webFeedID = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var homePageURL: String? {
|
||||||
|
get {
|
||||||
|
return metadata.homePageURL
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if let url = newValue, !url.isEmpty {
|
||||||
|
metadata.homePageURL = url.normalizedURL
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
metadata.homePageURL = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: this is available only if the icon URL was available in the feed.
|
||||||
|
// The icon URL is a JSON-Feed-only feature.
|
||||||
|
// Otherwise we find an icon URL via other means, but we don’t store it
|
||||||
|
// as part of feed metadata.
|
||||||
|
public var iconURL: String? {
|
||||||
|
get {
|
||||||
|
return metadata.iconURL
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
metadata.iconURL = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Note: this is available only if the favicon URL was available in the feed.
|
||||||
|
// The favicon URL is a JSON-Feed-only feature.
|
||||||
|
// Otherwise we find a favicon URL via other means, but we don’t store it
|
||||||
|
// as part of feed metadata.
|
||||||
|
public var faviconURL: String? {
|
||||||
|
get {
|
||||||
|
return metadata.faviconURL
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
metadata.faviconURL = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var name: String? {
|
||||||
|
didSet {
|
||||||
|
if name != oldValue {
|
||||||
|
postDisplayNameDidChangeNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var authors: Set<Author>? {
|
||||||
|
get {
|
||||||
|
if let authorsArray = metadata.authors {
|
||||||
|
return Set(authorsArray)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if let authorsSet = newValue {
|
||||||
|
metadata.authors = Array(authorsSet)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
metadata.authors = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var editedName: String? {
|
||||||
|
// Don’t let editedName == ""
|
||||||
|
get {
|
||||||
|
guard let s = metadata.editedName, !s.isEmpty else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if newValue != editedName {
|
||||||
|
if let valueToSet = newValue, !valueToSet.isEmpty {
|
||||||
|
metadata.editedName = valueToSet
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
metadata.editedName = nil
|
||||||
|
}
|
||||||
|
postDisplayNameDidChangeNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var conditionalGetInfo: HTTPConditionalGetInfo? {
|
||||||
|
get {
|
||||||
|
return metadata.conditionalGetInfo
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
metadata.conditionalGetInfo = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var contentHash: String? {
|
||||||
|
get {
|
||||||
|
return metadata.contentHash
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
metadata.contentHash = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isNotifyAboutNewArticles: Bool? {
|
||||||
|
get {
|
||||||
|
return metadata.isNotifyAboutNewArticles
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
metadata.isNotifyAboutNewArticles = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var isArticleExtractorAlwaysOn: Bool? {
|
||||||
|
get {
|
||||||
|
metadata.isArticleExtractorAlwaysOn
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
metadata.isArticleExtractorAlwaysOn = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var sinceToken: String? {
|
||||||
|
get {
|
||||||
|
return metadata.sinceToken
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
metadata.sinceToken = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public var externalID: String? {
|
||||||
|
get {
|
||||||
|
return metadata.externalID
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
metadata.externalID = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Folder Name: Sync Service Relationship ID
|
||||||
|
public var folderRelationship: [String: String]? {
|
||||||
|
get {
|
||||||
|
return metadata.folderRelationship
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
metadata.folderRelationship = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - DisplayNameProvider
|
||||||
|
|
||||||
|
public var nameForDisplay: String {
|
||||||
|
if let s = editedName, !s.isEmpty {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
if let s = name, !s.isEmpty {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return NSLocalizedString("Untitled", comment: "Feed name")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Renamable
|
||||||
|
|
||||||
|
public func rename(to newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
guard let account = account else { return }
|
||||||
|
account.renameWebFeed(self, to: newName, completion: completion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - UnreadCountProvider
|
||||||
|
|
||||||
|
public var unreadCount: Int {
|
||||||
|
get {
|
||||||
|
return account?.unreadCount(for: self) ?? 0
|
||||||
|
}
|
||||||
|
set {
|
||||||
|
if unreadCount == newValue {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
account?.setUnreadCount(newValue, for: self)
|
||||||
|
postUnreadCountDidChangeNotification()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - NotificationDisplayName
|
||||||
|
public var notificationDisplayName: String {
|
||||||
|
#if os(macOS)
|
||||||
|
if self.url.contains("www.reddit.com") {
|
||||||
|
return NSLocalizedString("Show notifications for new posts", comment: "notifyNameDisplay / Reddit")
|
||||||
|
} else {
|
||||||
|
return NSLocalizedString("Show notifications for new articles", comment: "notifyNameDisplay / Default")
|
||||||
|
}
|
||||||
|
#else
|
||||||
|
if self.url.contains("www.reddit.com") {
|
||||||
|
return NSLocalizedString("Notify about new posts", comment: "notifyNameDisplay / Reddit")
|
||||||
|
} else {
|
||||||
|
return NSLocalizedString("Notify about new articles", comment: "notifyNameDisplay / Default")
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
var metadata: WebFeedMetadata
|
||||||
|
|
||||||
|
// MARK: - Private
|
||||||
|
|
||||||
|
private let accountID: String // Used for hashing and equality; account may turn nil
|
||||||
|
|
||||||
|
// MARK: - Init
|
||||||
|
|
||||||
|
init(account: Account, url: String, metadata: WebFeedMetadata) {
|
||||||
|
self.account = account
|
||||||
|
self.accountID = account.accountID
|
||||||
|
self.url = url
|
||||||
|
self.metadata = metadata
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - API
|
||||||
|
|
||||||
|
public func dropConditionalGetInfo() {
|
||||||
|
conditionalGetInfo = nil
|
||||||
|
contentHash = nil
|
||||||
|
sinceToken = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Hashable
|
||||||
|
|
||||||
|
public func hash(into hasher: inout Hasher) {
|
||||||
|
hasher.combine(webFeedID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Equatable
|
||||||
|
|
||||||
|
public class func ==(lhs: Feed, rhs: Feed) -> Bool {
|
||||||
|
return lhs.webFeedID == rhs.webFeedID && lhs.accountID == rhs.accountID
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public protocol Feed: FeedIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider {
|
// MARK: - OPMLRepresentable
|
||||||
|
|
||||||
var account: Account? { get }
|
extension Feed: OPMLRepresentable {
|
||||||
var defaultReadFilterType: ReadFilterType { get }
|
|
||||||
|
|
||||||
|
public func OPMLString(indentLevel: Int, allowCustomAttributes: Bool) -> String {
|
||||||
|
// https://github.com/brentsimmons/NetNewsWire/issues/527
|
||||||
|
// Don’t use nameForDisplay because that can result in a feed name "Untitled" written to disk,
|
||||||
|
// which NetNewsWire may take later to be the actual name.
|
||||||
|
var nameToUse = editedName
|
||||||
|
if nameToUse == nil {
|
||||||
|
nameToUse = name
|
||||||
|
}
|
||||||
|
if nameToUse == nil {
|
||||||
|
nameToUse = ""
|
||||||
|
}
|
||||||
|
let escapedName = nameToUse!.escapingSpecialXMLCharacters
|
||||||
|
|
||||||
|
var escapedHomePageURL = ""
|
||||||
|
if let homePageURL = homePageURL {
|
||||||
|
escapedHomePageURL = homePageURL.escapingSpecialXMLCharacters
|
||||||
|
}
|
||||||
|
let escapedFeedURL = url.escapingSpecialXMLCharacters
|
||||||
|
|
||||||
|
var s = "<outline text=\"\(escapedName)\" title=\"\(escapedName)\" description=\"\" type=\"rss\" version=\"RSS\" htmlUrl=\"\(escapedHomePageURL)\" xmlUrl=\"\(escapedFeedURL)\"/>\n"
|
||||||
|
s = s.prepending(tabCount: indentLevel)
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public extension Feed {
|
extension Set where Element == Feed {
|
||||||
|
|
||||||
func readFiltered(readFilterEnabledTable: [FeedIdentifier: Bool]) -> Bool {
|
func webFeedIDs() -> Set<String> {
|
||||||
guard defaultReadFilterType != .alwaysRead else {
|
return Set<String>(map { $0.webFeedID })
|
||||||
return true
|
}
|
||||||
}
|
|
||||||
if let feedID = feedID, let readFilterEnabled = readFilterEnabledTable[feedID] {
|
|
||||||
return readFilterEnabled
|
|
||||||
} else {
|
|
||||||
return defaultReadFilterType == .read
|
|
||||||
}
|
|
||||||
|
|
||||||
|
func sorted() -> Array<Feed> {
|
||||||
|
return sorted(by: { (webFeed1, webFeed2) -> Bool in
|
||||||
|
if webFeed1.nameForDisplay.localizedStandardCompare(webFeed2.nameForDisplay) == .orderedSame {
|
||||||
|
return webFeed1.url < webFeed2.url
|
||||||
|
}
|
||||||
|
return webFeed1.nameForDisplay.localizedStandardCompare(webFeed2.nameForDisplay) == .orderedAscending
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -169,7 +169,7 @@ final class FeedWranglerAPICaller: NSObject {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func retrieveFeedItems(page: Int = 0, feed: WebFeed? = nil, completion: @escaping (Result<[FeedWranglerFeedItem], Error>) -> Void) {
|
func retrieveFeedItems(page: Int = 0, feed: Feed? = nil, completion: @escaping (Result<[FeedWranglerFeedItem], Error>) -> Void) {
|
||||||
let queryItems = [
|
let queryItems = [
|
||||||
URLQueryItem(name: "read", value: "false"),
|
URLQueryItem(name: "read", value: "false"),
|
||||||
URLQueryItem(name: "offset", value: String(page * FeedWranglerConfig.pageSize)),
|
URLQueryItem(name: "offset", value: String(page * FeedWranglerConfig.pageSize)),
|
||||||
|
@ -315,7 +315,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
|||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||||
|
|
||||||
self.refreshCredentials(for: account) {
|
self.refreshCredentials(for: account) {
|
||||||
@ -336,7 +336,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func addFeedWranglerSubscription(account: Account, subscription sub: FeedWranglerSubscription, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
private func addFeedWranglerSubscription(account: Account, subscription sub: FeedWranglerSubscription, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let feed = account.createWebFeed(with: sub.title, url: sub.feedURL, webFeedID: String(sub.feedID), homePageURL: sub.siteURL)
|
let feed = account.createWebFeed(with: sub.title, url: sub.feedURL, webFeedID: String(sub.feedID), homePageURL: sub.siteURL)
|
||||||
|
|
||||||
@ -364,7 +364,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func initialFeedDownload(account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
private func initialFeedDownload(account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
|
|
||||||
self.caller.retrieveFeedItems(page: 0, feed: feed) { results in
|
self.caller.retrieveFeedItems(page: 0, feed: feed) { results in
|
||||||
switch results {
|
switch results {
|
||||||
@ -383,7 +383,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||||
|
|
||||||
self.refreshCredentials(for: account) {
|
self.refreshCredentials(for: account) {
|
||||||
@ -408,7 +408,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
// just add to account, folders are not supported
|
// just add to account, folders are not supported
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
account.addFeedIfNotInAnyFolder(feed)
|
account.addFeedIfNotInAnyFolder(feed)
|
||||||
@ -416,7 +416,7 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||||
|
|
||||||
self.refreshCredentials(for: account) {
|
self.refreshCredentials(for: account) {
|
||||||
@ -442,11 +442,11 @@ final class FeedWranglerAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
fatalError()
|
fatalError()
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
||||||
account.addWebFeed(existingFeed, to: container) { result in
|
account.addWebFeed(existingFeed, to: container) { result in
|
||||||
switch result {
|
switch result {
|
||||||
|
@ -388,7 +388,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
|
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
caller.createSubscription(url: url) { result in
|
caller.createSubscription(url: url) { result in
|
||||||
@ -420,7 +420,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
// This error should never happen
|
// This error should never happen
|
||||||
guard let subscriptionID = feed.externalID else {
|
guard let subscriptionID = feed.externalID else {
|
||||||
@ -447,7 +447,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
if feed.folderRelationship?.count ?? 0 > 1 {
|
if feed.folderRelationship?.count ?? 0 > 1 {
|
||||||
deleteTagging(for: account, with: feed, from: container, completion: completion)
|
deleteTagging(for: account, with: feed, from: container, completion: completion)
|
||||||
} else {
|
} else {
|
||||||
@ -455,7 +455,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
if from is Account {
|
if from is Account {
|
||||||
addWebFeed(for: account, with: feed, to: to, completion: completion)
|
addWebFeed(for: account, with: feed, to: to, completion: completion)
|
||||||
} else {
|
} else {
|
||||||
@ -470,7 +470,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
if let folder = container as? Folder, let webFeedID = Int(feed.webFeedID) {
|
if let folder = container as? Folder, let webFeedID = Int(feed.webFeedID) {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
@ -502,7 +502,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
||||||
account.addWebFeed(existingFeed, to: container) { result in
|
account.addWebFeed(existingFeed, to: container) { result in
|
||||||
@ -990,14 +990,14 @@ private extension FeedbinAccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearFolderRelationship(for feed: WebFeed, withFolderName folderName: String) {
|
func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) {
|
||||||
if var folderRelationship = feed.folderRelationship {
|
if var folderRelationship = feed.folderRelationship {
|
||||||
folderRelationship[folderName] = nil
|
folderRelationship[folderName] = nil
|
||||||
feed.folderRelationship = folderRelationship
|
feed.folderRelationship = folderRelationship
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveFolderRelationship(for feed: WebFeed, withFolderName folderName: String, id: String) {
|
func saveFolderRelationship(for feed: Feed, withFolderName folderName: String, id: String) {
|
||||||
if var folderRelationship = feed.folderRelationship {
|
if var folderRelationship = feed.folderRelationship {
|
||||||
folderRelationship[folderName] = id
|
folderRelationship[folderName] = id
|
||||||
feed.folderRelationship = folderRelationship
|
feed.folderRelationship = folderRelationship
|
||||||
@ -1006,7 +1006,7 @@ private extension FeedbinAccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [FeedbinSubscriptionChoice], completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [FeedbinSubscriptionChoice], completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
var orderFound = 0
|
var orderFound = 0
|
||||||
|
|
||||||
let feedSpecifiers: [FeedSpecifier] = choices.map { choice in
|
let feedSpecifiers: [FeedSpecifier] = choices.map { choice in
|
||||||
@ -1025,7 +1025,7 @@ private extension FeedbinAccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFeed( account: Account, subscription sub: FeedbinSubscription, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func createFeed( account: Account, subscription sub: FeedbinSubscription, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
@ -1058,7 +1058,7 @@ private extension FeedbinAccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialFeedDownload( account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func initialFeedDownload( account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
|
|
||||||
// refreshArticles is being reused and will clear one of the tasks for us
|
// refreshArticles is being reused and will clear one of the tasks for us
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(4)
|
refreshProgress.addToNumberOfTasksAndRemaining(4)
|
||||||
@ -1371,7 +1371,7 @@ private extension FeedbinAccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteTagging(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
func deleteTagging(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] {
|
if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
@ -1401,7 +1401,7 @@ private extension FeedbinAccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteSubscription(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
func deleteSubscription(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
// This error should never happen
|
// This error should never happen
|
||||||
guard let subscriptionID = feed.externalID else {
|
guard let subscriptionID = feed.externalID else {
|
||||||
|
@ -314,7 +314,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
|
|
||||||
do {
|
do {
|
||||||
guard let credentials = credentials else {
|
guard let credentials = credentials else {
|
||||||
@ -347,7 +347,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
let folderCollectionIds = account.folders?.filter { $0.has(feed) }.compactMap { $0.externalID }
|
let folderCollectionIds = account.folders?.filter { $0.has(feed) }.compactMap { $0.externalID }
|
||||||
guard let collectionIds = folderCollectionIds, let collectionId = collectionIds.first else {
|
guard let collectionIds = folderCollectionIds, let collectionId = collectionIds.first else {
|
||||||
completion(.failure(FeedlyAccountDelegateError.unableToRenameFeed(feed.nameForDisplay, name)))
|
completion(.failure(FeedlyAccountDelegateError.unableToRenameFeed(feed.nameForDisplay, name)))
|
||||||
@ -374,7 +374,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||||||
feed.editedName = name
|
feed.editedName = name
|
||||||
}
|
}
|
||||||
|
|
||||||
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
do {
|
do {
|
||||||
guard let credentials = credentials else {
|
guard let credentials = credentials else {
|
||||||
@ -405,7 +405,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
guard let folder = container as? Folder, let collectionId = folder.externalID else {
|
guard let folder = container as? Folder, let collectionId = folder.externalID else {
|
||||||
return DispatchQueue.main.async {
|
return DispatchQueue.main.async {
|
||||||
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed)))
|
completion(.failure(FeedlyAccountDelegateError.unableToRemoveFeed(feed)))
|
||||||
@ -425,7 +425,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||||||
folder.removeWebFeed(feed)
|
folder.removeWebFeed(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
guard let from = from as? Folder, let to = to as? Folder else {
|
guard let from = from as? Folder, let to = to as? Folder else {
|
||||||
return DispatchQueue.main.async {
|
return DispatchQueue.main.async {
|
||||||
completion(.failure(FeedlyAccountDelegateError.addFeedChooseFolder))
|
completion(.failure(FeedlyAccountDelegateError.addFeedChooseFolder))
|
||||||
@ -458,7 +458,7 @@ final class FeedlyAccountDelegate: AccountDelegate {
|
|||||||
to.addWebFeed(feed)
|
to.addWebFeed(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
||||||
account.addWebFeed(existingFeed, to: container) { result in
|
account.addWebFeed(existingFeed, to: container) { result in
|
||||||
switch result {
|
switch result {
|
||||||
|
@ -14,11 +14,11 @@ enum FeedlyAccountDelegateError: LocalizedError {
|
|||||||
case unableToAddFolder(String)
|
case unableToAddFolder(String)
|
||||||
case unableToRenameFolder(String, String)
|
case unableToRenameFolder(String, String)
|
||||||
case unableToRemoveFolder(String)
|
case unableToRemoveFolder(String)
|
||||||
case unableToMoveFeedBetweenFolders(WebFeed, Folder, Folder)
|
case unableToMoveFeedBetweenFolders(Feed, Folder, Folder)
|
||||||
case addFeedChooseFolder
|
case addFeedChooseFolder
|
||||||
case addFeedInvalidFolder(Folder)
|
case addFeedInvalidFolder(Folder)
|
||||||
case unableToRenameFeed(String, String)
|
case unableToRenameFeed(String, String)
|
||||||
case unableToRemoveFeed(WebFeed)
|
case unableToRemoveFeed(Feed)
|
||||||
|
|
||||||
var errorDescription: String? {
|
var errorDescription: String? {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -28,7 +28,7 @@ class FeedlyAddNewFeedOperation: FeedlyOperation, FeedlyOperationDelegate, Feedl
|
|||||||
private let getStreamContentsService: FeedlyGetStreamContentsService
|
private let getStreamContentsService: FeedlyGetStreamContentsService
|
||||||
private let log: OSLog
|
private let log: OSLog
|
||||||
private var feedResourceId: FeedlyFeedResourceId?
|
private var feedResourceId: FeedlyFeedResourceId?
|
||||||
var addCompletionHandler: ((Result<WebFeed, Error>) -> ())?
|
var addCompletionHandler: ((Result<Feed, Error>) -> ())?
|
||||||
|
|
||||||
init(account: Account, credentials: Credentials, url: String, feedName: String?, searchService: FeedlySearchService, addToCollectionService: FeedlyAddFeedToCollectionService, syncUnreadIdsService: FeedlyGetStreamIdsService, getStreamContentsService: FeedlyGetStreamContentsService, database: SyncDatabase, container: Container, progress: DownloadProgress, log: OSLog) throws {
|
init(account: Account, credentials: Credentials, url: String, feedName: String?, searchService: FeedlySearchService, addToCollectionService: FeedlyAddFeedToCollectionService, syncUnreadIdsService: FeedlyGetStreamIdsService, getStreamContentsService: FeedlyGetStreamContentsService, database: SyncDatabase, container: Container, progress: DownloadProgress, log: OSLog) throws {
|
||||||
|
|
||||||
|
@ -46,7 +46,7 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pair each Feed with its Folder.
|
// Pair each Feed with its Folder.
|
||||||
var feedsAdded = Set<WebFeed>()
|
var feedsAdded = Set<Feed>()
|
||||||
|
|
||||||
let feedsAndFolders = pairs
|
let feedsAndFolders = pairs
|
||||||
.map({ (collectionFeeds, folder) -> [(FeedlyFeed, Folder)] in
|
.map({ (collectionFeeds, folder) -> [(FeedlyFeed, Folder)] in
|
||||||
@ -55,7 +55,7 @@ final class FeedlyCreateFeedsForCollectionFoldersOperation: FeedlyOperation {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.flatMap { $0 }
|
.flatMap { $0 }
|
||||||
.compactMap { (collectionFeed, folder) -> (WebFeed, Folder) in
|
.compactMap { (collectionFeed, folder) -> (Feed, Folder) in
|
||||||
|
|
||||||
// find an existing feed previously added to the account
|
// find an existing feed previously added to the account
|
||||||
if let feed = account.existingWebFeed(withWebFeedID: collectionFeed.id) {
|
if let feed = account.existingWebFeed(withWebFeedID: collectionFeed.id) {
|
||||||
|
@ -10,7 +10,7 @@ import Foundation
|
|||||||
import Articles
|
import Articles
|
||||||
import RSCore
|
import RSCore
|
||||||
|
|
||||||
public final class Folder: Feed, Renamable, Container, Hashable {
|
public final class Folder: SidebarItem, Renamable, Container, Hashable {
|
||||||
|
|
||||||
public var defaultReadFilterType: ReadFilterType {
|
public var defaultReadFilterType: ReadFilterType {
|
||||||
return .read
|
return .read
|
||||||
@ -24,16 +24,16 @@ public final class Folder: Feed, Renamable, Container, Hashable {
|
|||||||
return ContainerIdentifier.folder(accountID, nameForDisplay)
|
return ContainerIdentifier.folder(accountID, nameForDisplay)
|
||||||
}
|
}
|
||||||
|
|
||||||
public var feedID: FeedIdentifier? {
|
public var sidebarItemID: SidebarItemIdentifier? {
|
||||||
guard let accountID = account?.accountID else {
|
guard let accountID = account?.accountID else {
|
||||||
assertionFailure("Expected feed.account, but got nil.")
|
assertionFailure("Expected feed.account, but got nil.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return FeedIdentifier.folder(accountID, nameForDisplay)
|
return SidebarItemIdentifier.folder(accountID, nameForDisplay)
|
||||||
}
|
}
|
||||||
|
|
||||||
public weak var account: Account?
|
public weak var account: Account?
|
||||||
public var topLevelWebFeeds: Set<WebFeed> = Set<WebFeed>()
|
public var topLevelWebFeeds: Set<Feed> = Set<Feed>()
|
||||||
public var folders: Set<Folder>? = nil // subfolders are not supported, so this is always nil
|
public var folders: Set<Folder>? = nil // subfolders are not supported, so this is always nil
|
||||||
|
|
||||||
public var name: String? {
|
public var name: String? {
|
||||||
@ -100,25 +100,25 @@ public final class Folder: Feed, Renamable, Container, Hashable {
|
|||||||
|
|
||||||
// MARK: Container
|
// MARK: Container
|
||||||
|
|
||||||
public func flattenedWebFeeds() -> Set<WebFeed> {
|
public func flattenedWebFeeds() -> Set<Feed> {
|
||||||
// Since sub-folders are not supported, it’s always the top-level feeds.
|
// Since sub-folders are not supported, it’s always the top-level feeds.
|
||||||
return topLevelWebFeeds
|
return topLevelWebFeeds
|
||||||
}
|
}
|
||||||
|
|
||||||
public func objectIsChild(_ object: AnyObject) -> Bool {
|
public func objectIsChild(_ object: AnyObject) -> Bool {
|
||||||
// Folders contain Feed objects only, at least for now.
|
// Folders contain Feed objects only, at least for now.
|
||||||
guard let feed = object as? WebFeed else {
|
guard let feed = object as? Feed else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
return topLevelWebFeeds.contains(feed)
|
return topLevelWebFeeds.contains(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addWebFeed(_ feed: WebFeed) {
|
public func addWebFeed(_ feed: Feed) {
|
||||||
topLevelWebFeeds.insert(feed)
|
topLevelWebFeeds.insert(feed)
|
||||||
postChildrenDidChangeNotification()
|
postChildrenDidChangeNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addFeeds(_ feeds: Set<WebFeed>) {
|
public func addFeeds(_ feeds: Set<Feed>) {
|
||||||
guard !feeds.isEmpty else {
|
guard !feeds.isEmpty else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -126,12 +126,12 @@ public final class Folder: Feed, Renamable, Container, Hashable {
|
|||||||
postChildrenDidChangeNotification()
|
postChildrenDidChangeNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeWebFeed(_ feed: WebFeed) {
|
public func removeWebFeed(_ feed: Feed) {
|
||||||
topLevelWebFeeds.remove(feed)
|
topLevelWebFeeds.remove(feed)
|
||||||
postChildrenDidChangeNotification()
|
postChildrenDidChangeNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeFeeds(_ feeds: Set<WebFeed>) {
|
public func removeFeeds(_ feeds: Set<Feed>) {
|
||||||
guard !feeds.isEmpty else {
|
guard !feeds.isEmpty else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -164,7 +164,7 @@ private extension Folder {
|
|||||||
unreadCount = updatedUnreadCount
|
unreadCount = updatedUnreadCount
|
||||||
}
|
}
|
||||||
|
|
||||||
func childrenContain(_ feed: WebFeed) -> Bool {
|
func childrenContain(_ feed: Feed) -> Bool {
|
||||||
return topLevelWebFeeds.contains(feed)
|
return topLevelWebFeeds.contains(feed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -122,7 +122,7 @@ final class LocalAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func createWebFeed(for account: Account, url urlString: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
completion(.failure(LocalAccountDelegateError.invalidParameter))
|
completion(.failure(LocalAccountDelegateError.invalidParameter))
|
||||||
return
|
return
|
||||||
@ -131,28 +131,28 @@ final class LocalAccountDelegate: AccountDelegate {
|
|||||||
createRSSWebFeed(for: account, url: url, editedName: name, container: container, completion: completion)
|
createRSSWebFeed(for: account, url: url, editedName: name, container: container, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
feed.editedName = name
|
feed.editedName = name
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
container.removeWebFeed(feed)
|
container.removeWebFeed(feed)
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
from.removeWebFeed(feed)
|
from.removeWebFeed(feed)
|
||||||
to.addWebFeed(feed)
|
to.addWebFeed(feed)
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
container.addWebFeed(feed)
|
container.addWebFeed(feed)
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
container.addWebFeed(feed)
|
container.addWebFeed(feed)
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
@ -219,7 +219,7 @@ final class LocalAccountDelegate: AccountDelegate {
|
|||||||
extension LocalAccountDelegate: LocalAccountRefresherDelegate {
|
extension LocalAccountDelegate: LocalAccountRefresherDelegate {
|
||||||
|
|
||||||
|
|
||||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed) {
|
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: Feed) {
|
||||||
refreshProgress.completeTask()
|
refreshProgress.completeTask()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -231,7 +231,7 @@ extension LocalAccountDelegate: LocalAccountRefresherDelegate {
|
|||||||
|
|
||||||
private extension LocalAccountDelegate {
|
private extension LocalAccountDelegate {
|
||||||
|
|
||||||
func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func createRSSWebFeed(for account: Account, url: URL, editedName: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
|
|
||||||
// We need to use a batch update here because we need to assign add the feed to the
|
// We need to use a batch update here because we need to assign add the feed to the
|
||||||
// container before the name has been downloaded. This will put it in the sidebar
|
// container before the name has been downloaded. This will put it in the sidebar
|
||||||
|
@ -14,7 +14,7 @@ import Articles
|
|||||||
import ArticlesDatabase
|
import ArticlesDatabase
|
||||||
|
|
||||||
protocol LocalAccountRefresherDelegate {
|
protocol LocalAccountRefresherDelegate {
|
||||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: WebFeed)
|
func localAccountRefresher(_ refresher: LocalAccountRefresher, requestCompletedFor: Feed)
|
||||||
func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges, completion: @escaping () -> Void)
|
func localAccountRefresher(_ refresher: LocalAccountRefresher, articleChanges: ArticleChanges, completion: @escaping () -> Void)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,7 +28,7 @@ final class LocalAccountRefresher {
|
|||||||
return DownloadSession(delegate: self)
|
return DownloadSession(delegate: self)
|
||||||
}()
|
}()
|
||||||
|
|
||||||
public func refreshFeeds(_ feeds: Set<WebFeed>, completion: (() -> Void)? = nil) {
|
public func refreshFeeds(_ feeds: Set<Feed>, completion: (() -> Void)? = nil) {
|
||||||
guard !feeds.isEmpty else {
|
guard !feeds.isEmpty else {
|
||||||
completion?()
|
completion?()
|
||||||
return
|
return
|
||||||
@ -53,7 +53,7 @@ final class LocalAccountRefresher {
|
|||||||
extension LocalAccountRefresher: DownloadSessionDelegate {
|
extension LocalAccountRefresher: DownloadSessionDelegate {
|
||||||
|
|
||||||
func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? {
|
func downloadSession(_ downloadSession: DownloadSession, requestForRepresentedObject representedObject: AnyObject) -> URLRequest? {
|
||||||
guard let feed = representedObject as? WebFeed else {
|
guard let feed = representedObject as? Feed else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
guard let url = URL(string: feed.url) else {
|
guard let url = URL(string: feed.url) else {
|
||||||
@ -69,7 +69,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) {
|
func downloadSession(_ downloadSession: DownloadSession, downloadDidCompleteForRepresentedObject representedObject: AnyObject, response: URLResponse?, data: Data, error: NSError?, completion: @escaping () -> Void) {
|
||||||
let feed = representedObject as! WebFeed
|
let feed = representedObject as! Feed
|
||||||
|
|
||||||
guard !data.isEmpty, !isSuspended else {
|
guard !data.isEmpty, !isSuspended else {
|
||||||
completion()
|
completion()
|
||||||
@ -120,7 +120,7 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData data: Data, representedObject: AnyObject) -> Bool {
|
func downloadSession(_ downloadSession: DownloadSession, shouldContinueAfterReceivingData data: Data, representedObject: AnyObject) -> Bool {
|
||||||
let feed = representedObject as! WebFeed
|
let feed = representedObject as! Feed
|
||||||
guard !isSuspended else {
|
guard !isSuspended else {
|
||||||
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
return false
|
return false
|
||||||
@ -139,17 +139,17 @@ extension LocalAccountRefresher: DownloadSessionDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downloadSession(_ downloadSession: DownloadSession, didReceiveUnexpectedResponse response: URLResponse, representedObject: AnyObject) {
|
func downloadSession(_ downloadSession: DownloadSession, didReceiveUnexpectedResponse response: URLResponse, representedObject: AnyObject) {
|
||||||
let feed = representedObject as! WebFeed
|
let feed = representedObject as! Feed
|
||||||
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadSession(_ downloadSession: DownloadSession, didReceiveNotModifiedResponse: URLResponse, representedObject: AnyObject) {
|
func downloadSession(_ downloadSession: DownloadSession, didReceiveNotModifiedResponse: URLResponse, representedObject: AnyObject) {
|
||||||
let feed = representedObject as! WebFeed
|
let feed = representedObject as! Feed
|
||||||
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadSession(_ downloadSession: DownloadSession, didDiscardDuplicateRepresentedObject representedObject: AnyObject) {
|
func downloadSession(_ downloadSession: DownloadSession, didDiscardDuplicateRepresentedObject representedObject: AnyObject) {
|
||||||
let feed = representedObject as! WebFeed
|
let feed = representedObject as! Feed
|
||||||
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
delegate?.localAccountRefresher(self, requestCompletedFor: feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,14 +195,14 @@ extension NewsBlurAccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearFolderRelationship(for feed: WebFeed, withFolderName folderName: String) {
|
func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) {
|
||||||
if var folderRelationship = feed.folderRelationship {
|
if var folderRelationship = feed.folderRelationship {
|
||||||
folderRelationship[folderName] = nil
|
folderRelationship[folderName] = nil
|
||||||
feed.folderRelationship = folderRelationship
|
feed.folderRelationship = folderRelationship
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveFolderRelationship(for feed: WebFeed, withFolderName folderName: String, id: String) {
|
func saveFolderRelationship(for feed: Feed, withFolderName folderName: String, id: String) {
|
||||||
if var folderRelationship = feed.folderRelationship {
|
if var folderRelationship = feed.folderRelationship {
|
||||||
folderRelationship[folderName] = id
|
folderRelationship[folderName] = id
|
||||||
feed.folderRelationship = folderRelationship
|
feed.folderRelationship = folderRelationship
|
||||||
@ -412,7 +412,7 @@ extension NewsBlurAccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFeed(account: Account, feed: NewsBlurFeed?, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func createFeed(account: Account, feed: NewsBlurFeed?, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
guard let feed = feed else {
|
guard let feed = feed else {
|
||||||
completion(.failure(NewsBlurError.invalidParameter))
|
completion(.failure(NewsBlurError.invalidParameter))
|
||||||
return
|
return
|
||||||
@ -445,7 +445,7 @@ extension NewsBlurAccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func downloadFeed(account: Account, feed: WebFeed, page: Int, completion: @escaping (Result<Void, Error>) -> Void) {
|
func downloadFeed(account: Account, feed: Feed, page: Int, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
|
|
||||||
caller.retrieveStories(feedID: feed.webFeedID, page: page) { result in
|
caller.retrieveStories(feedID: feed.webFeedID, page: page) { result in
|
||||||
@ -484,7 +484,7 @@ extension NewsBlurAccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialFeedDownload(account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func initialFeedDownload(account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
|
|
||||||
// Download the initial articles
|
// Download the initial articles
|
||||||
@ -513,7 +513,7 @@ extension NewsBlurAccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteFeed(for account: Account, with feed: WebFeed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
func deleteFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
// This error should never happen
|
// This error should never happen
|
||||||
guard let feedID = feed.externalID else {
|
guard let feedID = feed.externalID else {
|
||||||
completion(.failure(NewsBlurError.invalidParameter))
|
completion(.failure(NewsBlurError.invalidParameter))
|
||||||
|
@ -423,7 +423,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> ()) {
|
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> ()) {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
|
|
||||||
let folderName = (container as? Folder)?.name
|
let folderName = (container as? Folder)?.name
|
||||||
@ -442,7 +442,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
|
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||||
guard let feedID = feed.externalID else {
|
guard let feedID = feed.externalID else {
|
||||||
completion(.failure(NewsBlurError.invalidParameter))
|
completion(.failure(NewsBlurError.invalidParameter))
|
||||||
return
|
return
|
||||||
@ -469,7 +469,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||||
guard let folder = container as? Folder else {
|
guard let folder = container as? Folder else {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
if let account = container as? Account {
|
if let account = container as? Account {
|
||||||
@ -488,11 +488,11 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||||
deleteFeed(for: account, with: feed, from: container, completion: completion)
|
deleteFeed(for: account, with: feed, from: container, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||||
guard let feedID = feed.externalID else {
|
guard let feedID = feed.externalID else {
|
||||||
completion(.failure(NewsBlurError.invalidParameter))
|
completion(.failure(NewsBlurError.invalidParameter))
|
||||||
return
|
return
|
||||||
@ -519,7 +519,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> ()) {
|
||||||
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
||||||
account.addWebFeed(existingFeed, to: container) { result in
|
account.addWebFeed(existingFeed, to: container) { result in
|
||||||
switch result {
|
switch result {
|
||||||
@ -547,7 +547,7 @@ final class NewsBlurAccountDelegate: AccountDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var feedsToRestore: [WebFeed] = []
|
var feedsToRestore: [Feed] = []
|
||||||
for feed in folder.topLevelWebFeeds {
|
for feed in folder.topLevelWebFeeds {
|
||||||
feedsToRestore.append(feed)
|
feedsToRestore.append(feed)
|
||||||
folder.topLevelWebFeeds.remove(feed)
|
folder.topLevelWebFeeds.remove(feed)
|
||||||
|
@ -390,7 +390,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func createWebFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
guard let url = URL(string: url) else {
|
guard let url = URL(string: url) else {
|
||||||
completion(.failure(ReaderAPIAccountDelegateError.invalidParameter))
|
completion(.failure(ReaderAPIAccountDelegateError.invalidParameter))
|
||||||
return
|
return
|
||||||
@ -439,7 +439,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func renameWebFeed(for account: Account, with feed: WebFeed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
func renameWebFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
// This error should never happen
|
// This error should never happen
|
||||||
guard let subscriptionID = feed.externalID else {
|
guard let subscriptionID = feed.externalID else {
|
||||||
@ -466,7 +466,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeWebFeed(for account: Account, with feed: WebFeed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func removeWebFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
guard let subscriptionID = feed.externalID else {
|
guard let subscriptionID = feed.externalID else {
|
||||||
completion(.failure(ReaderAPIAccountDelegateError.invalidParameter))
|
completion(.failure(ReaderAPIAccountDelegateError.invalidParameter))
|
||||||
return
|
return
|
||||||
@ -496,7 +496,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveWebFeed(for account: Account, with feed: WebFeed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func moveWebFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
if from is Account {
|
if from is Account {
|
||||||
addWebFeed(for: account, with: feed, to: to, completion: completion)
|
addWebFeed(for: account, with: feed, to: to, completion: completion)
|
||||||
} else {
|
} else {
|
||||||
@ -524,7 +524,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addWebFeed(for account: Account, with feed: WebFeed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func addWebFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
if let folder = container as? Folder, let feedExternalID = feed.externalID {
|
if let folder = container as? Folder, let feedExternalID = feed.externalID {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
refreshProgress.addToNumberOfTasksAndRemaining(1)
|
||||||
caller.createTagging(subscriptionID: feedExternalID, tagName: folder.name ?? "") { result in
|
caller.createTagging(subscriptionID: feedExternalID, tagName: folder.name ?? "") { result in
|
||||||
@ -554,7 +554,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreWebFeed(for account: Account, feed: WebFeed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
func restoreWebFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
if let existingFeed = account.existingWebFeed(withURL: feed.url) {
|
||||||
account.addWebFeed(existingFeed, to: container) { result in
|
account.addWebFeed(existingFeed, to: container) { result in
|
||||||
@ -901,13 +901,13 @@ private extension ReaderAPIAccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func clearFolderRelationship(for feed: WebFeed, folderExternalID: String?) {
|
func clearFolderRelationship(for feed: Feed, folderExternalID: String?) {
|
||||||
guard var folderRelationship = feed.folderRelationship, let folderExternalID = folderExternalID else { return }
|
guard var folderRelationship = feed.folderRelationship, let folderExternalID = folderExternalID else { return }
|
||||||
folderRelationship[folderExternalID] = nil
|
folderRelationship[folderExternalID] = nil
|
||||||
feed.folderRelationship = folderRelationship
|
feed.folderRelationship = folderRelationship
|
||||||
}
|
}
|
||||||
|
|
||||||
func saveFolderRelationship(for feed: WebFeed, folderExternalID: String?, feedExternalID: String) {
|
func saveFolderRelationship(for feed: Feed, folderExternalID: String?, feedExternalID: String) {
|
||||||
guard let folderExternalID = folderExternalID else { return }
|
guard let folderExternalID = folderExternalID else { return }
|
||||||
if var folderRelationship = feed.folderRelationship {
|
if var folderRelationship = feed.folderRelationship {
|
||||||
folderRelationship[folderExternalID] = feedExternalID
|
folderRelationship[folderExternalID] = feedExternalID
|
||||||
@ -917,7 +917,7 @@ private extension ReaderAPIAccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFeed( account: Account, subscription sub: ReaderAPISubscription, name: String?, container: Container, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func createFeed( account: Account, subscription sub: ReaderAPISubscription, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
@ -948,7 +948,7 @@ private extension ReaderAPIAccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initialFeedDownload( account: Account, feed: WebFeed, completion: @escaping (Result<WebFeed, Error>) -> Void) {
|
func initialFeedDownload( account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
refreshProgress.addToNumberOfTasksAndRemaining(5)
|
refreshProgress.addToNumberOfTasksAndRemaining(5)
|
||||||
|
|
||||||
// Download the initial articles
|
// Download the initial articles
|
||||||
|
39
Account/Sources/Account/SidebarItem.swift
Normal file
39
Account/Sources/Account/SidebarItem.swift
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
//
|
||||||
|
// SidebarItem.swift
|
||||||
|
// Account
|
||||||
|
//
|
||||||
|
// Created by Maurice Parker on 11/15/19.
|
||||||
|
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import RSCore
|
||||||
|
|
||||||
|
public enum ReadFilterType {
|
||||||
|
case read
|
||||||
|
case none
|
||||||
|
case alwaysRead
|
||||||
|
}
|
||||||
|
|
||||||
|
public protocol SidebarItem: SidebarItemIdentifiable, ArticleFetcher, DisplayNameProvider, UnreadCountProvider {
|
||||||
|
|
||||||
|
var account: Account? { get }
|
||||||
|
var defaultReadFilterType: ReadFilterType { get }
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension SidebarItem {
|
||||||
|
|
||||||
|
func readFiltered(readFilterEnabledTable: [SidebarItemIdentifier: Bool]) -> Bool {
|
||||||
|
guard defaultReadFilterType != .alwaysRead else {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if let feedID = sidebarItemID, let readFilterEnabled = readFilterEnabledTable[feedID] {
|
||||||
|
return readFilterEnabled
|
||||||
|
} else {
|
||||||
|
return defaultReadFilterType == .read
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -8,11 +8,11 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public protocol FeedIdentifiable {
|
public protocol SidebarItemIdentifiable {
|
||||||
var feedID: FeedIdentifier? { get }
|
var sidebarItemID: SidebarItemIdentifier? { get }
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum FeedIdentifier: CustomStringConvertible, Hashable, Equatable {
|
public enum SidebarItemIdentifier: CustomStringConvertible, Hashable, Equatable {
|
||||||
|
|
||||||
case smartFeed(String) // String is a unique identifier
|
case smartFeed(String) // String is a unique identifier
|
||||||
case script(String) // String is a unique identifier
|
case script(String) // String is a unique identifier
|
||||||
@ -65,16 +65,16 @@ public enum FeedIdentifier: CustomStringConvertible, Hashable, Equatable {
|
|||||||
switch type {
|
switch type {
|
||||||
case "smartFeed":
|
case "smartFeed":
|
||||||
guard let id = userInfo["id"] as? String else { return nil }
|
guard let id = userInfo["id"] as? String else { return nil }
|
||||||
self = FeedIdentifier.smartFeed(id)
|
self = SidebarItemIdentifier.smartFeed(id)
|
||||||
case "script":
|
case "script":
|
||||||
guard let id = userInfo["id"] as? String else { return nil }
|
guard let id = userInfo["id"] as? String else { return nil }
|
||||||
self = FeedIdentifier.script(id)
|
self = SidebarItemIdentifier.script(id)
|
||||||
case "feed":
|
case "feed":
|
||||||
guard let accountID = userInfo["accountID"] as? String, let webFeedID = userInfo["webFeedID"] as? String else { return nil }
|
guard let accountID = userInfo["accountID"] as? String, let webFeedID = userInfo["webFeedID"] as? String else { return nil }
|
||||||
self = FeedIdentifier.webFeed(accountID, webFeedID)
|
self = SidebarItemIdentifier.webFeed(accountID, webFeedID)
|
||||||
case "folder":
|
case "folder":
|
||||||
guard let accountID = userInfo["accountID"] as? String, let folderName = userInfo["folderName"] as? String else { return nil }
|
guard let accountID = userInfo["accountID"] as? String, let folderName = userInfo["folderName"] as? String else { return nil }
|
||||||
self = FeedIdentifier.folder(accountID, folderName)
|
self = SidebarItemIdentifier.folder(accountID, folderName)
|
||||||
default:
|
default:
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
@ -1,322 +0,0 @@
|
|||||||
//
|
|
||||||
// WebFeed.swift
|
|
||||||
// NetNewsWire
|
|
||||||
//
|
|
||||||
// Created by Brent Simmons on 7/1/17.
|
|
||||||
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import RSCore
|
|
||||||
import RSWeb
|
|
||||||
import Articles
|
|
||||||
|
|
||||||
public final class WebFeed: Feed, Renamable, Hashable {
|
|
||||||
|
|
||||||
public var defaultReadFilterType: ReadFilterType {
|
|
||||||
return .none
|
|
||||||
}
|
|
||||||
|
|
||||||
public var feedID: FeedIdentifier? {
|
|
||||||
guard let accountID = account?.accountID else {
|
|
||||||
assertionFailure("Expected feed.account, but got nil.")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return FeedIdentifier.webFeed(accountID, webFeedID)
|
|
||||||
}
|
|
||||||
|
|
||||||
public weak var account: Account?
|
|
||||||
public let url: String
|
|
||||||
|
|
||||||
public var webFeedID: String {
|
|
||||||
get {
|
|
||||||
return metadata.webFeedID
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
metadata.webFeedID = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var homePageURL: String? {
|
|
||||||
get {
|
|
||||||
return metadata.homePageURL
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
if let url = newValue, !url.isEmpty {
|
|
||||||
metadata.homePageURL = url.normalizedURL
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
metadata.homePageURL = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: this is available only if the icon URL was available in the feed.
|
|
||||||
// The icon URL is a JSON-Feed-only feature.
|
|
||||||
// Otherwise we find an icon URL via other means, but we don’t store it
|
|
||||||
// as part of feed metadata.
|
|
||||||
public var iconURL: String? {
|
|
||||||
get {
|
|
||||||
return metadata.iconURL
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
metadata.iconURL = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Note: this is available only if the favicon URL was available in the feed.
|
|
||||||
// The favicon URL is a JSON-Feed-only feature.
|
|
||||||
// Otherwise we find a favicon URL via other means, but we don’t store it
|
|
||||||
// as part of feed metadata.
|
|
||||||
public var faviconURL: String? {
|
|
||||||
get {
|
|
||||||
return metadata.faviconURL
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
metadata.faviconURL = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var name: String? {
|
|
||||||
didSet {
|
|
||||||
if name != oldValue {
|
|
||||||
postDisplayNameDidChangeNotification()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var authors: Set<Author>? {
|
|
||||||
get {
|
|
||||||
if let authorsArray = metadata.authors {
|
|
||||||
return Set(authorsArray)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
if let authorsSet = newValue {
|
|
||||||
metadata.authors = Array(authorsSet)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
metadata.authors = nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var editedName: String? {
|
|
||||||
// Don’t let editedName == ""
|
|
||||||
get {
|
|
||||||
guard let s = metadata.editedName, !s.isEmpty else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
if newValue != editedName {
|
|
||||||
if let valueToSet = newValue, !valueToSet.isEmpty {
|
|
||||||
metadata.editedName = valueToSet
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
metadata.editedName = nil
|
|
||||||
}
|
|
||||||
postDisplayNameDidChangeNotification()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var conditionalGetInfo: HTTPConditionalGetInfo? {
|
|
||||||
get {
|
|
||||||
return metadata.conditionalGetInfo
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
metadata.conditionalGetInfo = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var contentHash: String? {
|
|
||||||
get {
|
|
||||||
return metadata.contentHash
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
metadata.contentHash = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var isNotifyAboutNewArticles: Bool? {
|
|
||||||
get {
|
|
||||||
return metadata.isNotifyAboutNewArticles
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
metadata.isNotifyAboutNewArticles = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var isArticleExtractorAlwaysOn: Bool? {
|
|
||||||
get {
|
|
||||||
metadata.isArticleExtractorAlwaysOn
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
metadata.isArticleExtractorAlwaysOn = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var sinceToken: String? {
|
|
||||||
get {
|
|
||||||
return metadata.sinceToken
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
metadata.sinceToken = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var externalID: String? {
|
|
||||||
get {
|
|
||||||
return metadata.externalID
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
metadata.externalID = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Folder Name: Sync Service Relationship ID
|
|
||||||
public var folderRelationship: [String: String]? {
|
|
||||||
get {
|
|
||||||
return metadata.folderRelationship
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
metadata.folderRelationship = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - DisplayNameProvider
|
|
||||||
|
|
||||||
public var nameForDisplay: String {
|
|
||||||
if let s = editedName, !s.isEmpty {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
if let s = name, !s.isEmpty {
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
return NSLocalizedString("Untitled", comment: "Feed name")
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Renamable
|
|
||||||
|
|
||||||
public func rename(to newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
guard let account = account else { return }
|
|
||||||
account.renameWebFeed(self, to: newName, completion: completion)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - UnreadCountProvider
|
|
||||||
|
|
||||||
public var unreadCount: Int {
|
|
||||||
get {
|
|
||||||
return account?.unreadCount(for: self) ?? 0
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
if unreadCount == newValue {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
account?.setUnreadCount(newValue, for: self)
|
|
||||||
postUnreadCountDidChangeNotification()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - NotificationDisplayName
|
|
||||||
public var notificationDisplayName: String {
|
|
||||||
#if os(macOS)
|
|
||||||
if self.url.contains("www.reddit.com") {
|
|
||||||
return NSLocalizedString("Show notifications for new posts", comment: "notifyNameDisplay / Reddit")
|
|
||||||
} else {
|
|
||||||
return NSLocalizedString("Show notifications for new articles", comment: "notifyNameDisplay / Default")
|
|
||||||
}
|
|
||||||
#else
|
|
||||||
if self.url.contains("www.reddit.com") {
|
|
||||||
return NSLocalizedString("Notify about new posts", comment: "notifyNameDisplay / Reddit")
|
|
||||||
} else {
|
|
||||||
return NSLocalizedString("Notify about new articles", comment: "notifyNameDisplay / Default")
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
var metadata: WebFeedMetadata
|
|
||||||
|
|
||||||
// MARK: - Private
|
|
||||||
|
|
||||||
private let accountID: String // Used for hashing and equality; account may turn nil
|
|
||||||
|
|
||||||
// MARK: - Init
|
|
||||||
|
|
||||||
init(account: Account, url: String, metadata: WebFeedMetadata) {
|
|
||||||
self.account = account
|
|
||||||
self.accountID = account.accountID
|
|
||||||
self.url = url
|
|
||||||
self.metadata = metadata
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - API
|
|
||||||
|
|
||||||
public func dropConditionalGetInfo() {
|
|
||||||
conditionalGetInfo = nil
|
|
||||||
contentHash = nil
|
|
||||||
sinceToken = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Hashable
|
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
|
||||||
hasher.combine(webFeedID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - Equatable
|
|
||||||
|
|
||||||
public class func ==(lhs: WebFeed, rhs: WebFeed) -> Bool {
|
|
||||||
return lhs.webFeedID == rhs.webFeedID && lhs.accountID == rhs.accountID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: - OPMLRepresentable
|
|
||||||
|
|
||||||
extension WebFeed: OPMLRepresentable {
|
|
||||||
|
|
||||||
public func OPMLString(indentLevel: Int, allowCustomAttributes: Bool) -> String {
|
|
||||||
// https://github.com/brentsimmons/NetNewsWire/issues/527
|
|
||||||
// Don’t use nameForDisplay because that can result in a feed name "Untitled" written to disk,
|
|
||||||
// which NetNewsWire may take later to be the actual name.
|
|
||||||
var nameToUse = editedName
|
|
||||||
if nameToUse == nil {
|
|
||||||
nameToUse = name
|
|
||||||
}
|
|
||||||
if nameToUse == nil {
|
|
||||||
nameToUse = ""
|
|
||||||
}
|
|
||||||
let escapedName = nameToUse!.escapingSpecialXMLCharacters
|
|
||||||
|
|
||||||
var escapedHomePageURL = ""
|
|
||||||
if let homePageURL = homePageURL {
|
|
||||||
escapedHomePageURL = homePageURL.escapingSpecialXMLCharacters
|
|
||||||
}
|
|
||||||
let escapedFeedURL = url.escapingSpecialXMLCharacters
|
|
||||||
|
|
||||||
var s = "<outline text=\"\(escapedName)\" title=\"\(escapedName)\" description=\"\" type=\"rss\" version=\"RSS\" htmlUrl=\"\(escapedHomePageURL)\" xmlUrl=\"\(escapedFeedURL)\"/>\n"
|
|
||||||
s = s.prepending(tabCount: indentLevel)
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Set where Element == WebFeed {
|
|
||||||
|
|
||||||
func webFeedIDs() -> Set<String> {
|
|
||||||
return Set<String>(map { $0.webFeedID })
|
|
||||||
}
|
|
||||||
|
|
||||||
func sorted() -> Array<WebFeed> {
|
|
||||||
return sorted(by: { (webFeed1, webFeed2) -> Bool in
|
|
||||||
if webFeed1.nameForDisplay.localizedStandardCompare(webFeed2.nameForDisplay) == .orderedSame {
|
|
||||||
return webFeed1.url < webFeed2.url
|
|
||||||
}
|
|
||||||
return webFeed1.nameForDisplay.localizedStandardCompare(webFeed2.nameForDisplay) == .orderedAscending
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -350,10 +350,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func webFeedSettingDidChange(_ note: Notification) {
|
@objc func webFeedSettingDidChange(_ note: Notification) {
|
||||||
guard let feed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else {
|
guard let feed = note.object as? Feed, let key = note.userInfo?[Feed.WebFeedSettingUserInfoKey] as? String else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if key == WebFeed.WebFeedSettingKey.homePageURL || key == WebFeed.WebFeedSettingKey.faviconURL {
|
if key == Feed.WebFeedSettingKey.homePageURL || key == Feed.WebFeedSettingKey.faviconURL {
|
||||||
let _ = faviconDownloader.favicon(for: feed)
|
let _ = faviconDownloader.favicon(for: feed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,7 @@ final class WebFeedInspectorViewController: NSViewController, Inspector {
|
|||||||
@IBOutlet weak var isNotifyAboutNewArticlesCheckBox: NSButton!
|
@IBOutlet weak var isNotifyAboutNewArticlesCheckBox: NSButton!
|
||||||
@IBOutlet weak var isReaderViewAlwaysOnCheckBox: NSButton?
|
@IBOutlet weak var isReaderViewAlwaysOnCheckBox: NSButton?
|
||||||
|
|
||||||
private var feed: WebFeed? {
|
private var feed: Feed? {
|
||||||
didSet {
|
didSet {
|
||||||
if feed != oldValue {
|
if feed != oldValue {
|
||||||
updateUI()
|
updateUI()
|
||||||
@ -42,7 +42,7 @@ final class WebFeedInspectorViewController: NSViewController, Inspector {
|
|||||||
var windowTitle: String = NSLocalizedString("Feed Inspector", comment: "Feed Inspector window title")
|
var windowTitle: String = NSLocalizedString("Feed Inspector", comment: "Feed Inspector window title")
|
||||||
|
|
||||||
func canInspect(_ objects: [Any]) -> Bool {
|
func canInspect(_ objects: [Any]) -> Bool {
|
||||||
return objects.count == 1 && objects.first is WebFeed
|
return objects.count == 1 && objects.first is Feed
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: NSViewController
|
// MARK: NSViewController
|
||||||
@ -123,7 +123,7 @@ extension WebFeedInspectorViewController: NSTextFieldDelegate {
|
|||||||
private extension WebFeedInspectorViewController {
|
private extension WebFeedInspectorViewController {
|
||||||
|
|
||||||
func updateFeed() {
|
func updateFeed() {
|
||||||
guard let objects = objects, objects.count == 1, let singleFeed = objects.first as? WebFeed else {
|
guard let objects = objects, objects.count == 1, let singleFeed = objects.first as? Feed else {
|
||||||
feed = nil
|
feed = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -176,7 +176,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let feed = currentFeedOrFolder as? WebFeed, let noteObject = noteObject as? WebFeed {
|
if let feed = currentFeedOrFolder as? Feed, let noteObject = noteObject as? Feed {
|
||||||
if feed == noteObject {
|
if feed == noteObject {
|
||||||
updateWindowTitle()
|
updateWindowTitle()
|
||||||
return
|
return
|
||||||
@ -633,7 +633,7 @@ extension MainWindowController: TimelineContainerViewControllerDelegate {
|
|||||||
detailViewController?.setState(detailState, mode: mode)
|
detailViewController?.setState(detailState, mode: mode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func timelineRequestedWebFeedSelection(_: TimelineContainerViewController, webFeed: WebFeed) {
|
func timelineRequestedWebFeedSelection(_: TimelineContainerViewController, webFeed: Feed) {
|
||||||
sidebarViewController?.selectFeed(webFeed)
|
sidebarViewController?.selectFeed(webFeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -146,7 +146,7 @@ struct PasteboardWebFeed: Hashable {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WebFeed: @retroactive PasteboardWriterOwner {
|
extension Feed: @retroactive PasteboardWriterOwner {
|
||||||
|
|
||||||
public var pasteboardWriter: NSPasteboardWriting {
|
public var pasteboardWriter: NSPasteboardWriting {
|
||||||
return WebFeedPasteboardWriter(webFeed: self)
|
return WebFeedPasteboardWriter(webFeed: self)
|
||||||
@ -155,14 +155,14 @@ extension WebFeed: @retroactive PasteboardWriterOwner {
|
|||||||
|
|
||||||
@objc final class WebFeedPasteboardWriter: NSObject, NSPasteboardWriting {
|
@objc final class WebFeedPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||||
|
|
||||||
private let webFeed: WebFeed
|
private let webFeed: Feed
|
||||||
static let webFeedUTI = "com.ranchero.webFeed"
|
static let webFeedUTI = "com.ranchero.webFeed"
|
||||||
static let webFeedUTIType = NSPasteboard.PasteboardType(rawValue: webFeedUTI)
|
static let webFeedUTIType = NSPasteboard.PasteboardType(rawValue: webFeedUTI)
|
||||||
static let webFeedUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.webFeed"
|
static let webFeedUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.webFeed"
|
||||||
static let webFeedUTIInternalType = NSPasteboard.PasteboardType(rawValue: webFeedUTIInternal)
|
static let webFeedUTIInternalType = NSPasteboard.PasteboardType(rawValue: webFeedUTIInternal)
|
||||||
|
|
||||||
|
|
||||||
init(webFeed: WebFeed) {
|
init(webFeed: Feed) {
|
||||||
self.webFeed = webFeed
|
self.webFeed = webFeed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ enum SidebarDeleteItemsAlert {
|
|||||||
alert.messageText = NSLocalizedString("Delete Folder", comment: "Delete Folder")
|
alert.messageText = NSLocalizedString("Delete Folder", comment: "Delete Folder")
|
||||||
let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” folder?", comment: "Folder delete text")
|
let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” folder?", comment: "Folder delete text")
|
||||||
alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, folder.nameForDisplay) as String
|
alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, folder.nameForDisplay) as String
|
||||||
} else if let feed = nodes.first?.representedObject as? Feed {
|
} else if let feed = nodes.first?.representedObject as? SidebarItem {
|
||||||
alert.messageText = NSLocalizedString("Delete Feed", comment: "Delete Feed")
|
alert.messageText = NSLocalizedString("Delete Feed", comment: "Delete Feed")
|
||||||
let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” feed?", comment: "Feed delete text")
|
let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” feed?", comment: "Feed delete text")
|
||||||
alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, feed.nameForDisplay) as String
|
alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, feed.nameForDisplay) as String
|
||||||
|
@ -136,7 +136,7 @@ private extension SidebarOutlineDataSource {
|
|||||||
// Don’t allow PseudoFeed to be dragged.
|
// Don’t allow PseudoFeed to be dragged.
|
||||||
// This will have to be revisited later. For instance,
|
// This will have to be revisited later. For instance,
|
||||||
// user-created smart feeds should be draggable, maybe.
|
// user-created smart feeds should be draggable, maybe.
|
||||||
return node.representedObject is Folder || node.representedObject is WebFeed
|
return node.representedObject is Folder || node.representedObject is Feed
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Drag and Drop
|
// MARK: - Drag and Drop
|
||||||
@ -249,7 +249,7 @@ private extension SidebarOutlineDataSource {
|
|||||||
if let folder = node.representedObject as? Folder {
|
if let folder = node.representedObject as? Folder {
|
||||||
return folder.account
|
return folder.account
|
||||||
}
|
}
|
||||||
if let feed = node.representedObject as? WebFeed {
|
if let feed = node.representedObject as? Feed {
|
||||||
return feed.account
|
return feed.account
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -309,7 +309,7 @@ private extension SidebarOutlineDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyWebFeedInAccount(node: Node, to parentNode: Node) {
|
func copyWebFeedInAccount(node: Node, to parentNode: Node) {
|
||||||
guard let feed = node.representedObject as? WebFeed, let destination = parentNode.representedObject as? Container else {
|
guard let feed = node.representedObject as? Feed, let destination = parentNode.representedObject as? Container else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,7 +324,7 @@ private extension SidebarOutlineDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func moveWebFeedInAccount(node: Node, to parentNode: Node) {
|
func moveWebFeedInAccount(node: Node, to parentNode: Node) {
|
||||||
guard let feed = node.representedObject as? WebFeed,
|
guard let feed = node.representedObject as? Feed,
|
||||||
let source = node.parent?.representedObject as? Container,
|
let source = node.parent?.representedObject as? Container,
|
||||||
let destination = parentNode.representedObject as? Container else {
|
let destination = parentNode.representedObject as? Container else {
|
||||||
return
|
return
|
||||||
@ -343,7 +343,7 @@ private extension SidebarOutlineDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyWebFeedBetweenAccounts(node: Node, to parentNode: Node) {
|
func copyWebFeedBetweenAccounts(node: Node, to parentNode: Node) {
|
||||||
guard let feed = node.representedObject as? WebFeed,
|
guard let feed = node.representedObject as? Feed,
|
||||||
let destinationAccount = nodeAccount(parentNode),
|
let destinationAccount = nodeAccount(parentNode),
|
||||||
let destinationContainer = parentNode.representedObject as? Container else {
|
let destinationContainer = parentNode.representedObject as? Container else {
|
||||||
return
|
return
|
||||||
@ -495,7 +495,7 @@ private extension SidebarOutlineDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func nodeRepresentsAnyDraggedFeed(_ node: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
|
func nodeRepresentsAnyDraggedFeed(_ node: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
|
||||||
guard let feed = node.representedObject as? WebFeed else {
|
guard let feed = node.representedObject as? Feed else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for draggedFeed in draggedFeeds {
|
for draggedFeed in draggedFeeds {
|
||||||
@ -520,7 +520,7 @@ private extension SidebarOutlineDataSource {
|
|||||||
return account
|
return account
|
||||||
} else if let folder = node.representedObject as? Folder {
|
} else if let folder = node.representedObject as? Folder {
|
||||||
return folder.account
|
return folder.account
|
||||||
} else if let webFeed = node.representedObject as? WebFeed {
|
} else if let webFeed = node.representedObject as? Feed {
|
||||||
return webFeed.account
|
return webFeed.account
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
|
@ -31,8 +31,8 @@ extension SidebarViewController {
|
|||||||
let object = objects.first!
|
let object = objects.first!
|
||||||
|
|
||||||
switch object {
|
switch object {
|
||||||
case is WebFeed:
|
case is Feed:
|
||||||
return menuForWebFeed(object as! WebFeed)
|
return menuForWebFeed(object as! Feed)
|
||||||
case is Folder:
|
case is Folder:
|
||||||
return menuForFolder(object as! Folder)
|
return menuForFolder(object as! Folder)
|
||||||
case is PseudoFeed:
|
case is PseudoFeed:
|
||||||
@ -93,7 +93,7 @@ extension SidebarViewController {
|
|||||||
|
|
||||||
@objc func renameFromContextualMenu(_ sender: Any?) {
|
@objc func renameFromContextualMenu(_ sender: Any?) {
|
||||||
|
|
||||||
guard let window = view.window, let menuItem = sender as? NSMenuItem, let object = menuItem.representedObject as? DisplayNameProvider, object is WebFeed || object is Folder else {
|
guard let window = view.window, let menuItem = sender as? NSMenuItem, let object = menuItem.representedObject as? DisplayNameProvider, object is Feed || object is Folder else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,7 +106,7 @@ extension SidebarViewController {
|
|||||||
|
|
||||||
@objc func toggleNotificationsFromContextMenu(_ sender: Any?) {
|
@objc func toggleNotificationsFromContextMenu(_ sender: Any?) {
|
||||||
guard let item = sender as? NSMenuItem,
|
guard let item = sender as? NSMenuItem,
|
||||||
let feed = item.representedObject as? WebFeed else {
|
let feed = item.representedObject as? Feed else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
|
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
|
||||||
@ -137,7 +137,7 @@ extension SidebarViewController {
|
|||||||
|
|
||||||
@objc func toggleArticleExtractorFromContextMenu(_ sender: Any?) {
|
@objc func toggleArticleExtractorFromContextMenu(_ sender: Any?) {
|
||||||
guard let item = sender as? NSMenuItem,
|
guard let item = sender as? NSMenuItem,
|
||||||
let feed = item.representedObject as? WebFeed else {
|
let feed = item.representedObject as? Feed else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if feed.isArticleExtractorAlwaysOn == nil { feed.isArticleExtractorAlwaysOn = false }
|
if feed.isArticleExtractorAlwaysOn == nil { feed.isArticleExtractorAlwaysOn = false }
|
||||||
@ -170,7 +170,7 @@ extension SidebarViewController: RenameWindowControllerDelegate {
|
|||||||
|
|
||||||
func renameWindowController(_ windowController: RenameWindowController, didRenameObject object: Any, withNewName name: String) {
|
func renameWindowController(_ windowController: RenameWindowController, didRenameObject object: Any, withNewName name: String) {
|
||||||
|
|
||||||
if let feed = object as? WebFeed {
|
if let feed = object as? Feed {
|
||||||
feed.rename(to: name) { result in
|
feed.rename(to: name) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
@ -206,7 +206,7 @@ private extension SidebarViewController {
|
|||||||
return menu
|
return menu
|
||||||
}
|
}
|
||||||
|
|
||||||
func menuForWebFeed(_ webFeed: WebFeed) -> NSMenu? {
|
func menuForWebFeed(_ webFeed: Feed) -> NSMenu? {
|
||||||
|
|
||||||
let menu = NSMenu(title: "")
|
let menu = NSMenu(title: "")
|
||||||
|
|
||||||
@ -338,7 +338,7 @@ private extension SidebarViewController {
|
|||||||
|
|
||||||
func objectIsFeedOrFolder(_ object: Any) -> Bool {
|
func objectIsFeedOrFolder(_ object: Any) -> Bool {
|
||||||
|
|
||||||
return object is WebFeed || object is Folder
|
return object is Feed || object is Folder
|
||||||
}
|
}
|
||||||
|
|
||||||
func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem {
|
func menuItem(_ title: String, _ action: Selector, _ representedObject: Any) -> NSMenuItem {
|
||||||
|
@ -97,7 +97,7 @@ protocol SidebarDelegate: AnyObject {
|
|||||||
func saveState(to state: inout [AnyHashable : Any]) {
|
func saveState(to state: inout [AnyHashable : Any]) {
|
||||||
state[UserInfoKey.readFeedsFilterState] = isReadFiltered
|
state[UserInfoKey.readFeedsFilterState] = isReadFiltered
|
||||||
state[UserInfoKey.containerExpandedWindowState] = expandedTable.map { $0.userInfo }
|
state[UserInfoKey.containerExpandedWindowState] = expandedTable.map { $0.userInfo }
|
||||||
state[UserInfoKey.selectedFeedsState] = selectedFeeds.compactMap { $0.feedID?.userInfo }
|
state[UserInfoKey.selectedFeedsState] = selectedFeeds.compactMap { $0.sidebarItemID?.userInfo }
|
||||||
}
|
}
|
||||||
|
|
||||||
func restoreState(from state: [AnyHashable : Any]) {
|
func restoreState(from state: [AnyHashable : Any]) {
|
||||||
@ -111,7 +111,7 @@ protocol SidebarDelegate: AnyObject {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedFeedIdentifers = Set(selectedFeedsState.compactMap( { FeedIdentifier(userInfo: $0) }))
|
let selectedFeedIdentifers = Set(selectedFeedsState.compactMap( { SidebarItemIdentifier(userInfo: $0) }))
|
||||||
selectedFeedIdentifers.forEach { treeControllerDelegate.addFilterException($0) }
|
selectedFeedIdentifers.forEach { treeControllerDelegate.addFilterException($0) }
|
||||||
|
|
||||||
rebuildTreeAndReloadDataIfNeeded()
|
rebuildTreeAndReloadDataIfNeeded()
|
||||||
@ -119,7 +119,7 @@ protocol SidebarDelegate: AnyObject {
|
|||||||
var selectIndexes = IndexSet()
|
var selectIndexes = IndexSet()
|
||||||
|
|
||||||
func selectFeedsVisitor(node: Node) {
|
func selectFeedsVisitor(node: Node) {
|
||||||
if let feedID = (node.representedObject as? FeedIdentifiable)?.feedID {
|
if let feedID = (node.representedObject as? SidebarItemIdentifiable)?.sidebarItemID {
|
||||||
if selectedFeedIdentifers.contains(feedID) {
|
if selectedFeedIdentifers.contains(feedID) {
|
||||||
selectIndexes.insert(outlineView.row(forItem: node) )
|
selectIndexes.insert(outlineView.row(forItem: node) )
|
||||||
}
|
}
|
||||||
@ -194,15 +194,15 @@ protocol SidebarDelegate: AnyObject {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||||
guard let webFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else { return }
|
guard let webFeed = note.userInfo?[UserInfoKey.webFeed] as? Feed else { return }
|
||||||
configureCellsForRepresentedObject(webFeed)
|
configureCellsForRepresentedObject(webFeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func webFeedSettingDidChange(_ note: Notification) {
|
@objc func webFeedSettingDidChange(_ note: Notification) {
|
||||||
guard let webFeed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else {
|
guard let webFeed = note.object as? Feed, let key = note.userInfo?[Feed.WebFeedSettingUserInfoKey] as? String else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if key == WebFeed.WebFeedSettingKey.homePageURL || key == WebFeed.WebFeedSettingKey.faviconURL {
|
if key == Feed.WebFeedSettingKey.homePageURL || key == Feed.WebFeedSettingKey.faviconURL {
|
||||||
configureCellsForRepresentedObject(webFeed)
|
configureCellsForRepresentedObject(webFeed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -444,13 +444,13 @@ protocol SidebarDelegate: AnyObject {
|
|||||||
|
|
||||||
// MARK: - API
|
// MARK: - API
|
||||||
|
|
||||||
func selectFeed(_ feed: Feed) {
|
func selectFeed(_ feed: SidebarItem) {
|
||||||
if isReadFiltered, let feedID = feed.feedID {
|
if isReadFiltered, let feedID = feed.sidebarItemID {
|
||||||
self.treeControllerDelegate.addFilterException(feedID)
|
self.treeControllerDelegate.addFilterException(feedID)
|
||||||
|
|
||||||
if let webFeed = feed as? WebFeed, let account = webFeed.account {
|
if let webFeed = feed as? Feed, let account = webFeed.account {
|
||||||
let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(webFeed) })
|
let parentFolder = account.sortedFolders?.first(where: { $0.objectIsChild(webFeed) })
|
||||||
if let parentFolderFeedID = parentFolder?.feedID {
|
if let parentFolderFeedID = parentFolder?.sidebarItemID {
|
||||||
self.treeControllerDelegate.addFilterException(parentFolderFeedID)
|
self.treeControllerDelegate.addFilterException(parentFolderFeedID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -465,7 +465,7 @@ protocol SidebarDelegate: AnyObject {
|
|||||||
func deepLinkRevealAndSelect(for userInfo: [AnyHashable : Any]) {
|
func deepLinkRevealAndSelect(for userInfo: [AnyHashable : Any]) {
|
||||||
guard let accountNode = findAccountNode(userInfo),
|
guard let accountNode = findAccountNode(userInfo),
|
||||||
let feedNode = findFeedNode(userInfo, beginningAt: accountNode),
|
let feedNode = findFeedNode(userInfo, beginningAt: accountNode),
|
||||||
let feed = feedNode.representedObject as? Feed else {
|
let feed = feedNode.representedObject as? SidebarItem else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
selectFeed(feed)
|
selectFeed(feed)
|
||||||
@ -510,8 +510,8 @@ private extension SidebarViewController {
|
|||||||
return [Node]()
|
return [Node]()
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectedFeeds: [Feed] {
|
var selectedFeeds: [SidebarItem] {
|
||||||
selectedNodes.compactMap { $0.representedObject as? Feed }
|
selectedNodes.compactMap { $0.representedObject as? SidebarItem }
|
||||||
}
|
}
|
||||||
|
|
||||||
var singleSelectedNode: Node? {
|
var singleSelectedNode: Node? {
|
||||||
@ -521,26 +521,26 @@ private extension SidebarViewController {
|
|||||||
return selectedNodes.first!
|
return selectedNodes.first!
|
||||||
}
|
}
|
||||||
|
|
||||||
var singleSelectedWebFeed: WebFeed? {
|
var singleSelectedWebFeed: Feed? {
|
||||||
guard let node = singleSelectedNode else {
|
guard let node = singleSelectedNode else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
return node.representedObject as? WebFeed
|
return node.representedObject as? Feed
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAllSelectedToFilterExceptions() {
|
func addAllSelectedToFilterExceptions() {
|
||||||
selectedFeeds.forEach { addToFilterExeptionsIfNecessary($0) }
|
selectedFeeds.forEach { addToFilterExeptionsIfNecessary($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func addToFilterExeptionsIfNecessary(_ feed: Feed?) {
|
func addToFilterExeptionsIfNecessary(_ feed: SidebarItem?) {
|
||||||
if isReadFiltered, let feedID = feed?.feedID {
|
if isReadFiltered, let feedID = feed?.sidebarItemID {
|
||||||
if feed is PseudoFeed {
|
if feed is PseudoFeed {
|
||||||
treeControllerDelegate.addFilterException(feedID)
|
treeControllerDelegate.addFilterException(feedID)
|
||||||
} else if let folderFeed = feed as? Folder {
|
} else if let folderFeed = feed as? Folder {
|
||||||
if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil {
|
if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil {
|
||||||
treeControllerDelegate.addFilterException(feedID)
|
treeControllerDelegate.addFilterException(feedID)
|
||||||
}
|
}
|
||||||
} else if let webFeed = feed as? WebFeed {
|
} else if let webFeed = feed as? Feed {
|
||||||
if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil {
|
if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil {
|
||||||
treeControllerDelegate.addFilterException(feedID)
|
treeControllerDelegate.addFilterException(feedID)
|
||||||
addParentFolderToFilterExceptions(webFeed)
|
addParentFolderToFilterExceptions(webFeed)
|
||||||
@ -549,10 +549,10 @@ private extension SidebarViewController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addParentFolderToFilterExceptions(_ feed: Feed) {
|
func addParentFolderToFilterExceptions(_ feed: SidebarItem) {
|
||||||
guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject),
|
guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject),
|
||||||
let folder = node.parent?.representedObject as? Folder,
|
let folder = node.parent?.representedObject as? Folder,
|
||||||
let folderFeedID = folder.feedID else {
|
let folderFeedID = folder.sidebarItemID else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -610,7 +610,7 @@ private extension SidebarViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func addTreeControllerToFilterExceptionsVisitor(node: Node) {
|
func addTreeControllerToFilterExceptionsVisitor(node: Node) {
|
||||||
if let feed = node.representedObject as? Feed, let feedID = feed.feedID {
|
if let feed = node.representedObject as? SidebarItem, let feedID = feed.sidebarItemID {
|
||||||
treeControllerDelegate.addFilterException(feedID)
|
treeControllerDelegate.addFilterException(feedID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -741,7 +741,7 @@ private extension SidebarViewController {
|
|||||||
guard let webFeedID = userInfo?[ArticlePathKey.webFeedID] as? String else {
|
guard let webFeedID = userInfo?[ArticlePathKey.webFeedID] as? String else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if let node = startingNode.descendantNode(where: { ($0.representedObject as? WebFeed)?.webFeedID == webFeedID }) {
|
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.webFeedID == webFeedID }) {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -768,7 +768,7 @@ private extension SidebarViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func imageFor(_ node: Node) -> IconImage? {
|
func imageFor(_ node: Node) -> IconImage? {
|
||||||
if let feed = node.representedObject as? WebFeed, let feedIcon = IconImageCache.shared.imageForFeed(feed) {
|
if let feed = node.representedObject as? Feed, let feedIcon = IconImageCache.shared.imageForFeed(feed) {
|
||||||
return feedIcon
|
return feedIcon
|
||||||
}
|
}
|
||||||
if let smallIconProvider = node.representedObject as? SmallIconProvider {
|
if let smallIconProvider = node.representedObject as? SmallIconProvider {
|
||||||
@ -858,7 +858,7 @@ private extension Node {
|
|||||||
if representedObject === object {
|
if representedObject === object {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if let feed1 = object as? WebFeed, let feed2 = representedObject as? WebFeed {
|
if let feed1 = object as? Feed, let feed2 = representedObject as? Feed {
|
||||||
return feed1 == feed2
|
return feed1 == feed2
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
|
@ -12,7 +12,7 @@ import Articles
|
|||||||
|
|
||||||
protocol TimelineContainerViewControllerDelegate: AnyObject {
|
protocol TimelineContainerViewControllerDelegate: AnyObject {
|
||||||
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode)
|
func timelineSelectionDidChange(_: TimelineContainerViewController, articles: [Article]?, mode: TimelineSourceMode)
|
||||||
func timelineRequestedWebFeedSelection(_: TimelineContainerViewController, webFeed: WebFeed)
|
func timelineRequestedWebFeedSelection(_: TimelineContainerViewController, webFeed: Feed)
|
||||||
func timelineInvalidatedRestorationState(_: TimelineContainerViewController)
|
func timelineInvalidatedRestorationState(_: TimelineContainerViewController)
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -141,7 +141,7 @@ extension TimelineContainerViewController: TimelineDelegate {
|
|||||||
delegate?.timelineSelectionDidChange(self, articles: selectedArticles, mode: mode(for: timelineViewController))
|
delegate?.timelineSelectionDidChange(self, articles: selectedArticles, mode: mode(for: timelineViewController))
|
||||||
}
|
}
|
||||||
|
|
||||||
func timelineRequestedWebFeedSelection(_: TimelineViewController, webFeed: WebFeed) {
|
func timelineRequestedWebFeedSelection(_: TimelineViewController, webFeed: Feed) {
|
||||||
delegate?.timelineRequestedWebFeedSelection(self, webFeed: webFeed)
|
delegate?.timelineRequestedWebFeedSelection(self, webFeed: webFeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -65,7 +65,7 @@ extension TimelineViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func selectFeedInSidebarFromContextualMenu(_ sender: Any?) {
|
@objc func selectFeedInSidebarFromContextualMenu(_ sender: Any?) {
|
||||||
guard let menuItem = sender as? NSMenuItem, let webFeed = menuItem.representedObject as? WebFeed else {
|
guard let menuItem = sender as? NSMenuItem, let webFeed = menuItem.representedObject as? Feed else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
delegate?.timelineRequestedWebFeedSelection(self, webFeed: webFeed)
|
delegate?.timelineRequestedWebFeedSelection(self, webFeed: webFeed)
|
||||||
@ -164,7 +164,7 @@ private extension TimelineViewController {
|
|||||||
menu.addSeparatorIfNeeded()
|
menu.addSeparatorIfNeeded()
|
||||||
|
|
||||||
if articles.count == 1, let feed = articles.first!.webFeed {
|
if articles.count == 1, let feed = articles.first!.webFeed {
|
||||||
if !(representedObjects?.contains(where: { $0 as? WebFeed == feed }) ?? false) {
|
if !(representedObjects?.contains(where: { $0 as? Feed == feed }) ?? false) {
|
||||||
menu.addItem(selectFeedInSidebarMenuItem(feed))
|
menu.addItem(selectFeedInSidebarMenuItem(feed))
|
||||||
}
|
}
|
||||||
if let markAllMenuItem = markAllAsReadMenuItem(feed) {
|
if let markAllMenuItem = markAllAsReadMenuItem(feed) {
|
||||||
@ -248,13 +248,13 @@ private extension TimelineViewController {
|
|||||||
return menuItem(NSLocalizedString("Mark Below as Read", comment: "Command"), #selector(markBelowArticlesReadFromContextualMenu(_:)), articles)
|
return menuItem(NSLocalizedString("Mark Below as Read", comment: "Command"), #selector(markBelowArticlesReadFromContextualMenu(_:)), articles)
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectFeedInSidebarMenuItem(_ feed: WebFeed) -> NSMenuItem {
|
func selectFeedInSidebarMenuItem(_ feed: Feed) -> NSMenuItem {
|
||||||
let localizedMenuText = NSLocalizedString("Select “%@” in Sidebar", comment: "Command")
|
let localizedMenuText = NSLocalizedString("Select “%@” in Sidebar", comment: "Command")
|
||||||
let formattedMenuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay)
|
let formattedMenuText = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay)
|
||||||
return menuItem(formattedMenuText as String, #selector(selectFeedInSidebarFromContextualMenu(_:)), feed)
|
return menuItem(formattedMenuText as String, #selector(selectFeedInSidebarFromContextualMenu(_:)), feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func markAllAsReadMenuItem(_ feed: WebFeed) -> NSMenuItem? {
|
func markAllAsReadMenuItem(_ feed: Feed) -> NSMenuItem? {
|
||||||
guard let articlesSet = try? feed.fetchArticles() else {
|
guard let articlesSet = try? feed.fetchArticles() else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import os.log
|
|||||||
|
|
||||||
protocol TimelineDelegate: AnyObject {
|
protocol TimelineDelegate: AnyObject {
|
||||||
func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
|
func timelineSelectionDidChange(_: TimelineViewController, selectedArticles: [Article]?)
|
||||||
func timelineRequestedWebFeedSelection(_: TimelineViewController, webFeed: WebFeed)
|
func timelineRequestedWebFeedSelection(_: TimelineViewController, webFeed: Feed)
|
||||||
func timelineInvalidatedRestorationState(_: TimelineViewController)
|
func timelineInvalidatedRestorationState(_: TimelineViewController)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,15 +28,15 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
|
|
||||||
@IBOutlet var tableView: TimelineTableView!
|
@IBOutlet var tableView: TimelineTableView!
|
||||||
|
|
||||||
private var readFilterEnabledTable = [FeedIdentifier: Bool]()
|
private var readFilterEnabledTable = [SidebarItemIdentifier: Bool]()
|
||||||
var isReadFiltered: Bool? {
|
var isReadFiltered: Bool? {
|
||||||
guard representedObjects?.count == 1, let timelineFeed = representedObjects?.first as? Feed else {
|
guard representedObjects?.count == 1, let timelineFeed = representedObjects?.first as? SidebarItem else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
guard timelineFeed.defaultReadFilterType != .alwaysRead else {
|
guard timelineFeed.defaultReadFilterType != .alwaysRead else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if let feedID = timelineFeed.feedID, let readFilterEnabled = readFilterEnabledTable[feedID] {
|
if let feedID = timelineFeed.sidebarItemID, let readFilterEnabled = readFilterEnabledTable[feedID] {
|
||||||
return readFilterEnabled
|
return readFilterEnabled
|
||||||
} else {
|
} else {
|
||||||
return timelineFeed.defaultReadFilterType == .read
|
return timelineFeed.defaultReadFilterType == .read
|
||||||
@ -46,7 +46,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
var isCleanUpAvailable: Bool {
|
var isCleanUpAvailable: Bool {
|
||||||
let isEligibleForCleanUp: Bool?
|
let isEligibleForCleanUp: Bool?
|
||||||
|
|
||||||
if representedObjects?.count == 1, let timelineFeed = representedObjects?.first as? Feed, timelineFeed.defaultReadFilterType == .alwaysRead {
|
if representedObjects?.count == 1, let timelineFeed = representedObjects?.first as? SidebarItem, timelineFeed.defaultReadFilterType == .alwaysRead {
|
||||||
isEligibleForCleanUp = true
|
isEligibleForCleanUp = true
|
||||||
} else {
|
} else {
|
||||||
isEligibleForCleanUp = isReadFiltered
|
isEligibleForCleanUp = isReadFiltered
|
||||||
@ -111,7 +111,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let representedObjects = representedObjects, representedObjects.count == 1 && representedObjects.first is WebFeed {
|
if let representedObjects = representedObjects, representedObjects.count == 1 && representedObjects.first is Feed {
|
||||||
showFeedNames = {
|
showFeedNames = {
|
||||||
for article in articles {
|
for article in articles {
|
||||||
if !article.byline().isEmpty {
|
if !article.byline().isEmpty {
|
||||||
@ -263,7 +263,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func toggleReadFilter() {
|
func toggleReadFilter() {
|
||||||
guard let filter = isReadFiltered, let feedID = (representedObjects?.first as? Feed)?.feedID else { return }
|
guard let filter = isReadFiltered, let feedID = (representedObjects?.first as? SidebarItem)?.sidebarItemID else { return }
|
||||||
readFilterEnabledTable[feedID] = !filter
|
readFilterEnabledTable[feedID] = !filter
|
||||||
delegate?.timelineInvalidatedRestorationState(self)
|
delegate?.timelineInvalidatedRestorationState(self)
|
||||||
fetchAndReplacePreservingSelection()
|
fetchAndReplacePreservingSelection()
|
||||||
@ -287,7 +287,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
}
|
}
|
||||||
|
|
||||||
for i in 0..<readArticlesFilterStateKeys.count {
|
for i in 0..<readArticlesFilterStateKeys.count {
|
||||||
if let feedIdentifier = FeedIdentifier(userInfo: readArticlesFilterStateKeys[i]) {
|
if let feedIdentifier = SidebarItemIdentifier(userInfo: readArticlesFilterStateKeys[i]) {
|
||||||
readFilterEnabledTable[feedIdentifier] = readArticlesFilterStateValues[i]
|
readFilterEnabledTable[feedIdentifier] = readArticlesFilterStateValues[i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -594,7 +594,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||||
guard showIcons, let feed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else {
|
guard showIcons, let feed = note.userInfo?[UserInfoKey.webFeed] as? Feed else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let indexesToReload = tableView.indexesOfAvailableRowsPassingTest { (row) -> Bool in
|
let indexesToReload = tableView.indexesOfAvailableRowsPassingTest { (row) -> Bool in
|
||||||
@ -636,7 +636,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner, Unr
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func accountDidDownloadArticles(_ note: Notification) {
|
@objc func accountDidDownloadArticles(_ note: Notification) {
|
||||||
guard let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<WebFeed> else {
|
guard let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<Feed> else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1151,7 +1151,7 @@ private extension TimelineViewController {
|
|||||||
|
|
||||||
var fetchedArticles = Set<Article>()
|
var fetchedArticles = Set<Article>()
|
||||||
for fetchers in fetchers {
|
for fetchers in fetchers {
|
||||||
if (fetchers as? Feed)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true {
|
if (fetchers as? SidebarItem)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true {
|
||||||
if let articles = try? fetchers.fetchUnreadArticles() {
|
if let articles = try? fetchers.fetchUnreadArticles() {
|
||||||
fetchedArticles.formUnion(articles)
|
fetchedArticles.formUnion(articles)
|
||||||
}
|
}
|
||||||
@ -1226,14 +1226,14 @@ private extension TimelineViewController {
|
|||||||
return representedObjects?.contains(where: { $0 is Folder }) ?? false
|
return representedObjects?.contains(where: { $0 is Folder }) ?? false
|
||||||
}
|
}
|
||||||
|
|
||||||
func representedObjectsContainsAnyWebFeed(_ webFeeds: Set<WebFeed>) -> Bool {
|
func representedObjectsContainsAnyWebFeed(_ webFeeds: Set<Feed>) -> Bool {
|
||||||
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
||||||
|
|
||||||
guard let representedObjects = representedObjects else {
|
guard let representedObjects = representedObjects else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for representedObject in representedObjects {
|
for representedObject in representedObjects {
|
||||||
if let feed = representedObject as? WebFeed {
|
if let feed = representedObject as? Feed {
|
||||||
for oneFeed in webFeeds {
|
for oneFeed in webFeeds {
|
||||||
if feed.webFeedID == oneFeed.webFeedID || feed.url == oneFeed.url {
|
if feed.webFeedID == oneFeed.webFeedID || feed.url == oneFeed.url {
|
||||||
return true
|
return true
|
||||||
|
@ -73,10 +73,10 @@ extension NSApplication : ScriptingObjectContainer {
|
|||||||
for 'articles of feed "The Shape of Everything" of account "On My Mac"'
|
for 'articles of feed "The Shape of Everything" of account "On My Mac"'
|
||||||
*/
|
*/
|
||||||
|
|
||||||
func allWebFeeds() -> [WebFeed] {
|
func allWebFeeds() -> [Feed] {
|
||||||
let accounts = AccountManager.shared.activeAccounts
|
let accounts = AccountManager.shared.activeAccounts
|
||||||
let emptyFeeds:[WebFeed] = []
|
let emptyFeeds:[Feed] = []
|
||||||
return accounts.reduce(emptyFeeds) { (result, nthAccount) -> [WebFeed] in
|
return accounts.reduce(emptyFeeds) { (result, nthAccount) -> [Feed] in
|
||||||
let accountFeeds = Array(nthAccount.topLevelWebFeeds)
|
let accountFeeds = Array(nthAccount.topLevelWebFeeds)
|
||||||
return result + accountFeeds
|
return result + accountFeeds
|
||||||
}
|
}
|
||||||
|
@ -14,10 +14,10 @@ import Articles
|
|||||||
@objc(ScriptableWebFeed)
|
@objc(ScriptableWebFeed)
|
||||||
class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
|
||||||
|
|
||||||
let webFeed:WebFeed
|
let webFeed:Feed
|
||||||
let container:ScriptingObjectContainer
|
let container:ScriptingObjectContainer
|
||||||
|
|
||||||
init (_ webFeed:WebFeed, container:ScriptingObjectContainer) {
|
init (_ webFeed:Feed, container:ScriptingObjectContainer) {
|
||||||
self.webFeed = webFeed
|
self.webFeed = webFeed
|
||||||
self.container = container
|
self.container = container
|
||||||
}
|
}
|
||||||
@ -71,7 +71,7 @@ class ScriptableWebFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
|
|
||||||
class func scriptableFeed(_ feed:WebFeed, account:Account, folder:Folder?) -> ScriptableWebFeed {
|
class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableWebFeed {
|
||||||
let scriptableAccount = ScriptableAccount(account)
|
let scriptableAccount = ScriptableAccount(account)
|
||||||
if let folder = folder {
|
if let folder = folder {
|
||||||
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)
|
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)
|
||||||
|
@ -48,12 +48,12 @@ class ActivityManager {
|
|||||||
invalidateNextUnread()
|
invalidateNextUnread()
|
||||||
}
|
}
|
||||||
|
|
||||||
func selecting(feed: Feed) {
|
func selecting(feed: SidebarItem) {
|
||||||
invalidateCurrentActivities()
|
invalidateCurrentActivities()
|
||||||
|
|
||||||
selectingActivity = makeSelectFeedActivity(feed: feed)
|
selectingActivity = makeSelectFeedActivity(feed: feed)
|
||||||
|
|
||||||
if let webFeed = feed as? WebFeed {
|
if let webFeed = feed as? Feed {
|
||||||
updateSelectingActivityFeedSearchAttributes(with: webFeed)
|
updateSelectingActivityFeedSearchAttributes(with: webFeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -86,7 +86,7 @@ class ActivityManager {
|
|||||||
nextUnreadActivity = nil
|
nextUnreadActivity = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func reading(feed: Feed?, article: Article?) {
|
func reading(feed: SidebarItem?, article: Article?) {
|
||||||
invalidateReading()
|
invalidateReading()
|
||||||
invalidateNextUnread()
|
invalidateNextUnread()
|
||||||
|
|
||||||
@ -134,13 +134,13 @@ class ActivityManager {
|
|||||||
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
|
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: ids)
|
||||||
}
|
}
|
||||||
|
|
||||||
static func cleanUp(_ webFeed: WebFeed) {
|
static func cleanUp(_ webFeed: Feed) {
|
||||||
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: identifiers(for: webFeed))
|
CSSearchableIndex.default().deleteSearchableItems(withIdentifiers: identifiers(for: webFeed))
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||||
guard let webFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed, let activityFeedId = selectingActivity?.userInfo?[ArticlePathKey.webFeedID] as? String else {
|
guard let webFeed = note.userInfo?[UserInfoKey.webFeed] as? Feed, let activityFeedId = selectingActivity?.userInfo?[ArticlePathKey.webFeedID] as? String else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -161,7 +161,7 @@ class ActivityManager {
|
|||||||
|
|
||||||
private extension ActivityManager {
|
private extension ActivityManager {
|
||||||
|
|
||||||
func makeSelectFeedActivity(feed: Feed) -> NSUserActivity {
|
func makeSelectFeedActivity(feed: SidebarItem) -> NSUserActivity {
|
||||||
let activity = NSUserActivity(activityType: ActivityType.selectFeed.rawValue)
|
let activity = NSUserActivity(activityType: ActivityType.selectFeed.rawValue)
|
||||||
|
|
||||||
let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Folder")
|
let localizedText = NSLocalizedString("See articles in “%@”", comment: "See articles in Folder")
|
||||||
@ -171,27 +171,27 @@ private extension ActivityManager {
|
|||||||
activity.keywords = Set(makeKeywords(title))
|
activity.keywords = Set(makeKeywords(title))
|
||||||
activity.isEligibleForSearch = true
|
activity.isEligibleForSearch = true
|
||||||
|
|
||||||
let articleFetcherIdentifierUserInfo = feed.feedID?.userInfo ?? [AnyHashable: Any]()
|
let articleFetcherIdentifierUserInfo = feed.sidebarItemID?.userInfo ?? [AnyHashable: Any]()
|
||||||
activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo]
|
activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo]
|
||||||
activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String })
|
activity.requiredUserInfoKeys = Set(activity.userInfo!.keys.map { $0 as! String })
|
||||||
|
|
||||||
activity.persistentIdentifier = feed.feedID?.description ?? ""
|
activity.persistentIdentifier = feed.sidebarItemID?.description ?? ""
|
||||||
|
|
||||||
#if os(iOS)
|
#if os(iOS)
|
||||||
activity.suggestedInvocationPhrase = title
|
activity.suggestedInvocationPhrase = title
|
||||||
activity.isEligibleForPrediction = true
|
activity.isEligibleForPrediction = true
|
||||||
activity.contentAttributeSet?.relatedUniqueIdentifier = feed.feedID?.description ?? ""
|
activity.contentAttributeSet?.relatedUniqueIdentifier = feed.sidebarItemID?.description ?? ""
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
return activity
|
return activity
|
||||||
}
|
}
|
||||||
|
|
||||||
func makeReadArticleActivity(feed: Feed?, article: Article) -> NSUserActivity {
|
func makeReadArticleActivity(feed: SidebarItem?, article: Article) -> NSUserActivity {
|
||||||
let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue)
|
let activity = NSUserActivity(activityType: ActivityType.readArticle.rawValue)
|
||||||
activity.title = ArticleStringFormatter.truncatedTitle(article)
|
activity.title = ArticleStringFormatter.truncatedTitle(article)
|
||||||
|
|
||||||
if let feed = feed {
|
if let feed = feed {
|
||||||
let articleFetcherIdentifierUserInfo = feed.feedID?.userInfo ?? [AnyHashable: Any]()
|
let articleFetcherIdentifierUserInfo = feed.sidebarItemID?.userInfo ?? [AnyHashable: Any]()
|
||||||
let articlePathUserInfo = article.pathUserInfo
|
let articlePathUserInfo = article.pathUserInfo
|
||||||
activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo, UserInfoKey.articlePath: articlePathUserInfo]
|
activity.userInfo = [UserInfoKey.feedIdentifier: articleFetcherIdentifierUserInfo, UserInfoKey.articlePath: articlePathUserInfo]
|
||||||
} else {
|
} else {
|
||||||
@ -244,7 +244,7 @@ private extension ActivityManager {
|
|||||||
return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? []
|
return value?.components(separatedBy: " ").filter { $0.count > 2 } ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
func updateSelectingActivityFeedSearchAttributes(with feed: WebFeed) {
|
func updateSelectingActivityFeedSearchAttributes(with feed: Feed) {
|
||||||
|
|
||||||
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
|
let attributeSet = CSSearchableItemAttributeSet(itemContentType: kUTTypeItem as String)
|
||||||
attributeSet.title = feed.nameForDisplay
|
attributeSet.title = feed.nameForDisplay
|
||||||
@ -277,7 +277,7 @@ private extension ActivityManager {
|
|||||||
return "account_\(folder.account!.accountID)_folder_\(folder.nameForDisplay)"
|
return "account_\(folder.account!.accountID)_folder_\(folder.nameForDisplay)"
|
||||||
}
|
}
|
||||||
|
|
||||||
static func identifier(for feed: WebFeed) -> String {
|
static func identifier(for feed: Feed) -> String {
|
||||||
return "account_\(feed.account!.accountID)_feed_\(feed.webFeedID)"
|
return "account_\(feed.account!.accountID)_feed_\(feed.webFeedID)"
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -285,7 +285,7 @@ private extension ActivityManager {
|
|||||||
return "account_\(article.accountID)_feed_\(article.webFeedID)_article_\(article.articleID)"
|
return "account_\(article.accountID)_feed_\(article.webFeedID)_article_\(article.articleID)"
|
||||||
}
|
}
|
||||||
|
|
||||||
static func identifiers(for feed: WebFeed) -> [String] {
|
static func identifiers(for feed: Feed) -> [String] {
|
||||||
var ids = [String]()
|
var ids = [String]()
|
||||||
ids.append(identifier(for: feed))
|
ids.append(identifier(for: feed))
|
||||||
if let articles = try? feed.fetchArticles() {
|
if let articles = try? feed.fetchArticles() {
|
||||||
|
@ -77,7 +77,7 @@ final class DeleteCommand: UndoableCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
if let _ = node.representedObject as? WebFeed {
|
if let _ = node.representedObject as? Feed {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if let _ = node.representedObject as? Folder {
|
if let _ = node.representedObject as? Folder {
|
||||||
@ -98,7 +98,7 @@ private struct SidebarItemSpecifier {
|
|||||||
private weak var account: Account?
|
private weak var account: Account?
|
||||||
private let parentFolder: Folder?
|
private let parentFolder: Folder?
|
||||||
private let folder: Folder?
|
private let folder: Folder?
|
||||||
private let webFeed: WebFeed?
|
private let webFeed: Feed?
|
||||||
private let path: ContainerPath
|
private let path: ContainerPath
|
||||||
private let errorHandler: (Error) -> ()
|
private let errorHandler: (Error) -> ()
|
||||||
|
|
||||||
@ -118,7 +118,7 @@ private struct SidebarItemSpecifier {
|
|||||||
|
|
||||||
self.parentFolder = node.parentFolder()
|
self.parentFolder = node.parentFolder()
|
||||||
|
|
||||||
if let webFeed = node.representedObject as? WebFeed {
|
if let webFeed = node.representedObject as? Feed {
|
||||||
self.webFeed = webFeed
|
self.webFeed = webFeed
|
||||||
self.folder = nil
|
self.folder = nil
|
||||||
account = webFeed.account
|
account = webFeed.account
|
||||||
@ -271,7 +271,7 @@ private struct DeleteActionName {
|
|||||||
var numberOfFolders = 0
|
var numberOfFolders = 0
|
||||||
|
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
if let _ = node.representedObject as? WebFeed {
|
if let _ = node.representedObject as? Feed {
|
||||||
numberOfFeeds += 1
|
numberOfFeeds += 1
|
||||||
}
|
}
|
||||||
else if let _ = node.representedObject as? Folder {
|
else if let _ = node.representedObject as? Folder {
|
||||||
|
@ -42,7 +42,7 @@ private func accountAndArticlesDictionary(_ articles: Set<Article>) -> [String:
|
|||||||
|
|
||||||
extension Article {
|
extension Article {
|
||||||
|
|
||||||
var webFeed: WebFeed? {
|
var webFeed: Feed? {
|
||||||
return account?.existingWebFeed(withWebFeedID: webFeedID)
|
return account?.existingWebFeed(withWebFeedID: webFeedID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -121,7 +121,7 @@ extension Article {
|
|||||||
return IconImageCache.shared.imageForArticle(self)
|
return IconImageCache.shared.imageForArticle(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
func iconImageUrl(webFeed: WebFeed) -> URL? {
|
func iconImageUrl(webFeed: Feed) -> URL? {
|
||||||
if let image = iconImage() {
|
if let image = iconImage() {
|
||||||
let fm = FileManager.default
|
let fm = FileManager.default
|
||||||
var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
var path = fm.urls(for: .cachesDirectory, in: .userDomainMask)[0]
|
||||||
|
@ -25,7 +25,7 @@ extension Account: SmallIconProvider {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension WebFeed: SmallIconProvider {
|
extension Feed: SmallIconProvider {
|
||||||
|
|
||||||
var smallIcon: IconImage? {
|
var smallIcon: IconImage? {
|
||||||
if let iconImage = appDelegate.faviconDownloader.favicon(for: self) {
|
if let iconImage = appDelegate.faviconDownloader.favicon(for: self) {
|
||||||
|
@ -44,7 +44,7 @@ final class FaviconDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private let queue: DispatchQueue
|
private let queue: DispatchQueue
|
||||||
private var cache = [WebFeed: IconImage]() // faviconURL: RSImage
|
private var cache = [Feed: IconImage]() // faviconURL: RSImage
|
||||||
|
|
||||||
struct UserInfoKey {
|
struct UserInfoKey {
|
||||||
static let faviconURL = "faviconURL"
|
static let faviconURL = "faviconURL"
|
||||||
@ -69,10 +69,10 @@ final class FaviconDownloader {
|
|||||||
// MARK: - API
|
// MARK: - API
|
||||||
|
|
||||||
func resetCache() {
|
func resetCache() {
|
||||||
cache = [WebFeed: IconImage]()
|
cache = [Feed: IconImage]()
|
||||||
}
|
}
|
||||||
|
|
||||||
func favicon(for webFeed: WebFeed) -> IconImage? {
|
func favicon(for webFeed: Feed) -> IconImage? {
|
||||||
|
|
||||||
assert(Thread.isMainThread)
|
assert(Thread.isMainThread)
|
||||||
|
|
||||||
@ -94,7 +94,7 @@ final class FaviconDownloader {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func faviconAsIcon(for webFeed: WebFeed) -> IconImage? {
|
func faviconAsIcon(for webFeed: Feed) -> IconImage? {
|
||||||
|
|
||||||
if let image = cache[webFeed] {
|
if let image = cache[webFeed] {
|
||||||
return image
|
return image
|
||||||
|
@ -14,7 +14,7 @@ final class FaviconGenerator {
|
|||||||
|
|
||||||
private static var faviconGeneratorCache = [String: IconImage]() // feedURL: RSImage
|
private static var faviconGeneratorCache = [String: IconImage]() // feedURL: RSImage
|
||||||
|
|
||||||
static func favicon(_ webFeed: WebFeed) -> IconImage {
|
static func favicon(_ webFeed: Feed) -> IconImage {
|
||||||
|
|
||||||
if let favicon = FaviconGenerator.faviconGeneratorCache[webFeed.url] {
|
if let favicon = FaviconGenerator.faviconGeneratorCache[webFeed.url] {
|
||||||
return favicon
|
return favicon
|
||||||
|
@ -14,13 +14,13 @@ class IconImageCache {
|
|||||||
|
|
||||||
static var shared = IconImageCache()
|
static var shared = IconImageCache()
|
||||||
|
|
||||||
private var smartFeedIconImageCache = [FeedIdentifier: IconImage]()
|
private var smartFeedIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||||
private var webFeedIconImageCache = [FeedIdentifier: IconImage]()
|
private var webFeedIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||||
private var faviconImageCache = [FeedIdentifier: IconImage]()
|
private var faviconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||||
private var smallIconImageCache = [FeedIdentifier: IconImage]()
|
private var smallIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||||
private var authorIconImageCache = [Author: IconImage]()
|
private var authorIconImageCache = [Author: IconImage]()
|
||||||
|
|
||||||
func imageFor(_ feedID: FeedIdentifier) -> IconImage? {
|
func imageFor(_ feedID: SidebarItemIdentifier) -> IconImage? {
|
||||||
if let smartFeed = SmartFeedsController.shared.find(by: feedID) {
|
if let smartFeed = SmartFeedsController.shared.find(by: feedID) {
|
||||||
return imageForFeed(smartFeed)
|
return imageForFeed(smartFeed)
|
||||||
}
|
}
|
||||||
@ -30,15 +30,15 @@ class IconImageCache {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageForFeed(_ feed: Feed) -> IconImage? {
|
func imageForFeed(_ feed: SidebarItem) -> IconImage? {
|
||||||
guard let feedID = feed.feedID else {
|
guard let feedID = feed.sidebarItemID else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if let smartFeed = feed as? PseudoFeed {
|
if let smartFeed = feed as? PseudoFeed {
|
||||||
return imageForSmartFeed(smartFeed, feedID)
|
return imageForSmartFeed(smartFeed, feedID)
|
||||||
}
|
}
|
||||||
if let webFeed = feed as? WebFeed, let iconImage = imageForWebFeed(webFeed, feedID) {
|
if let webFeed = feed as? Feed, let iconImage = imageForWebFeed(webFeed, feedID) {
|
||||||
return iconImage
|
return iconImage
|
||||||
}
|
}
|
||||||
if let smallIconProvider = feed as? SmallIconProvider {
|
if let smallIconProvider = feed as? SmallIconProvider {
|
||||||
@ -59,17 +59,17 @@ class IconImageCache {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func emptyCache() {
|
func emptyCache() {
|
||||||
smartFeedIconImageCache = [FeedIdentifier: IconImage]()
|
smartFeedIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||||
webFeedIconImageCache = [FeedIdentifier: IconImage]()
|
webFeedIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||||
faviconImageCache = [FeedIdentifier: IconImage]()
|
faviconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||||
smallIconImageCache = [FeedIdentifier: IconImage]()
|
smallIconImageCache = [SidebarItemIdentifier: IconImage]()
|
||||||
authorIconImageCache = [Author: IconImage]()
|
authorIconImageCache = [Author: IconImage]()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension IconImageCache {
|
private extension IconImageCache {
|
||||||
|
|
||||||
func imageForSmartFeed(_ smartFeed: PseudoFeed, _ feedID: FeedIdentifier) -> IconImage? {
|
func imageForSmartFeed(_ smartFeed: PseudoFeed, _ feedID: SidebarItemIdentifier) -> IconImage? {
|
||||||
if let iconImage = smartFeedIconImageCache[feedID] {
|
if let iconImage = smartFeedIconImageCache[feedID] {
|
||||||
return iconImage
|
return iconImage
|
||||||
}
|
}
|
||||||
@ -80,7 +80,7 @@ private extension IconImageCache {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageForWebFeed(_ webFeed: WebFeed, _ feedID: FeedIdentifier) -> IconImage? {
|
func imageForWebFeed(_ webFeed: Feed, _ feedID: SidebarItemIdentifier) -> IconImage? {
|
||||||
if let iconImage = webFeedIconImageCache[feedID] {
|
if let iconImage = webFeedIconImageCache[feedID] {
|
||||||
return iconImage
|
return iconImage
|
||||||
}
|
}
|
||||||
@ -98,7 +98,7 @@ private extension IconImageCache {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageForSmallIconProvider(_ provider: SmallIconProvider, _ feedID: FeedIdentifier) -> IconImage? {
|
func imageForSmallIconProvider(_ provider: SmallIconProvider, _ feedID: SidebarItemIdentifier) -> IconImage? {
|
||||||
if let iconImage = smallIconImageCache[feedID] {
|
if let iconImage = smallIconImageCache[feedID] {
|
||||||
return iconImage
|
return iconImage
|
||||||
}
|
}
|
||||||
|
@ -53,8 +53,8 @@ public final class WebFeedIconDownloader {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
private var urlsInProgress = Set<String>()
|
private var urlsInProgress = Set<String>()
|
||||||
private var cache = [WebFeed: IconImage]()
|
private var cache = [Feed: IconImage]()
|
||||||
private var waitingForFeedURLs = [String: WebFeed]()
|
private var waitingForFeedURLs = [String: Feed]()
|
||||||
|
|
||||||
init(imageDownloader: ImageDownloader, folder: String) {
|
init(imageDownloader: ImageDownloader, folder: String) {
|
||||||
self.imageDownloader = imageDownloader
|
self.imageDownloader = imageDownloader
|
||||||
@ -68,10 +68,10 @@ public final class WebFeedIconDownloader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func resetCache() {
|
func resetCache() {
|
||||||
cache = [WebFeed: IconImage]()
|
cache = [Feed: IconImage]()
|
||||||
}
|
}
|
||||||
|
|
||||||
func icon(for feed: WebFeed) -> IconImage? {
|
func icon(for feed: Feed) -> IconImage? {
|
||||||
|
|
||||||
if let cachedImage = cache[feed] {
|
if let cachedImage = cache[feed] {
|
||||||
return cachedImage
|
return cachedImage
|
||||||
@ -153,7 +153,7 @@ public final class WebFeedIconDownloader {
|
|||||||
|
|
||||||
private extension WebFeedIconDownloader {
|
private extension WebFeedIconDownloader {
|
||||||
|
|
||||||
func icon(forHomePageURL homePageURL: String, feed: WebFeed, _ imageResultBlock: @escaping (RSImage?) -> Void) {
|
func icon(forHomePageURL homePageURL: String, feed: Feed, _ imageResultBlock: @escaping (RSImage?) -> Void) {
|
||||||
|
|
||||||
if homePagesWithNoIconURLCache.contains(homePageURL) || homePagesWithUglyIcons.contains(homePageURL) {
|
if homePagesWithNoIconURLCache.contains(homePageURL) || homePagesWithUglyIcons.contains(homePageURL) {
|
||||||
imageResultBlock(nil)
|
imageResultBlock(nil)
|
||||||
@ -168,7 +168,7 @@ private extension WebFeedIconDownloader {
|
|||||||
findIconURLForHomePageURL(homePageURL, feed: feed)
|
findIconURLForHomePageURL(homePageURL, feed: feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func icon(forURL url: String, feed: WebFeed, _ imageResultBlock: @escaping (RSImage?) -> Void) {
|
func icon(forURL url: String, feed: Feed, _ imageResultBlock: @escaping (RSImage?) -> Void) {
|
||||||
waitingForFeedURLs[url] = feed
|
waitingForFeedURLs[url] = feed
|
||||||
guard let imageData = imageDownloader.image(for: url) else {
|
guard let imageData = imageDownloader.image(for: url) else {
|
||||||
imageResultBlock(nil)
|
imageResultBlock(nil)
|
||||||
@ -177,7 +177,7 @@ private extension WebFeedIconDownloader {
|
|||||||
RSImage.scaledForIcon(imageData, imageResultBlock: imageResultBlock)
|
RSImage.scaledForIcon(imageData, imageResultBlock: imageResultBlock)
|
||||||
}
|
}
|
||||||
|
|
||||||
func postFeedIconDidBecomeAvailableNotification(_ feed: WebFeed) {
|
func postFeedIconDidBecomeAvailableNotification(_ feed: Feed) {
|
||||||
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
let userInfo: [AnyHashable: Any] = [UserInfoKey.webFeed: feed]
|
let userInfo: [AnyHashable: Any] = [UserInfoKey.webFeed: feed]
|
||||||
@ -197,7 +197,7 @@ private extension WebFeedIconDownloader {
|
|||||||
homePageToIconURLCacheDirty = true
|
homePageToIconURLCacheDirty = true
|
||||||
}
|
}
|
||||||
|
|
||||||
func findIconURLForHomePageURL(_ homePageURL: String, feed: WebFeed) {
|
func findIconURLForHomePageURL(_ homePageURL: String, feed: Feed) {
|
||||||
|
|
||||||
guard !urlsInProgress.contains(homePageURL) else {
|
guard !urlsInProgress.contains(homePageURL) else {
|
||||||
return
|
return
|
||||||
@ -214,7 +214,7 @@ private extension WebFeedIconDownloader {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pullIconURL(from metadata: RSHTMLMetadata, homePageURL: String, feed: WebFeed) {
|
func pullIconURL(from metadata: RSHTMLMetadata, homePageURL: String, feed: Feed) {
|
||||||
|
|
||||||
if let url = metadata.bestWebsiteIconURL() {
|
if let url = metadata.bestWebsiteIconURL() {
|
||||||
cacheIconURL(for: homePageURL, url)
|
cacheIconURL(for: homePageURL, url)
|
||||||
|
@ -13,7 +13,7 @@ import Articles
|
|||||||
import Account
|
import Account
|
||||||
import RSCore
|
import RSCore
|
||||||
|
|
||||||
protocol PseudoFeed: AnyObject, Feed, SmallIconProvider, PasteboardWriterOwner {
|
protocol PseudoFeed: AnyObject, SidebarItem, SmallIconProvider, PasteboardWriterOwner {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -24,7 +24,7 @@ import Articles
|
|||||||
import Account
|
import Account
|
||||||
import RSCore
|
import RSCore
|
||||||
|
|
||||||
protocol PseudoFeed: AnyObject, Feed, SmallIconProvider {
|
protocol PseudoFeed: AnyObject, SidebarItem, SmallIconProvider {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -14,8 +14,8 @@ import ArticlesDatabase
|
|||||||
|
|
||||||
struct SearchFeedDelegate: SmartFeedDelegate {
|
struct SearchFeedDelegate: SmartFeedDelegate {
|
||||||
|
|
||||||
var feedID: FeedIdentifier? {
|
var sidebarItemID: SidebarItemIdentifier? {
|
||||||
return FeedIdentifier.smartFeed(String(describing: SearchFeedDelegate.self))
|
return SidebarItemIdentifier.smartFeed(String(describing: SearchFeedDelegate.self))
|
||||||
}
|
}
|
||||||
|
|
||||||
var nameForDisplay: String {
|
var nameForDisplay: String {
|
||||||
|
@ -14,8 +14,8 @@ import ArticlesDatabase
|
|||||||
|
|
||||||
struct SearchTimelineFeedDelegate: SmartFeedDelegate {
|
struct SearchTimelineFeedDelegate: SmartFeedDelegate {
|
||||||
|
|
||||||
var feedID: FeedIdentifier? {
|
var sidebarItemID: SidebarItemIdentifier? {
|
||||||
return FeedIdentifier.smartFeed(String(describing: SearchTimelineFeedDelegate.self))
|
return SidebarItemIdentifier.smartFeed(String(describing: SearchTimelineFeedDelegate.self))
|
||||||
}
|
}
|
||||||
|
|
||||||
var nameForDisplay: String {
|
var nameForDisplay: String {
|
||||||
|
@ -20,8 +20,8 @@ final class SmartFeed: PseudoFeed {
|
|||||||
return .none
|
return .none
|
||||||
}
|
}
|
||||||
|
|
||||||
var feedID: FeedIdentifier? {
|
var sidebarItemID: SidebarItemIdentifier? {
|
||||||
delegate.feedID
|
delegate.sidebarItemID
|
||||||
}
|
}
|
||||||
|
|
||||||
var nameForDisplay: String {
|
var nameForDisplay: String {
|
||||||
|
@ -12,7 +12,7 @@ import Articles
|
|||||||
import ArticlesDatabase
|
import ArticlesDatabase
|
||||||
import RSCore
|
import RSCore
|
||||||
|
|
||||||
protocol SmartFeedDelegate: FeedIdentifiable, DisplayNameProvider, ArticleFetcher, SmallIconProvider {
|
protocol SmartFeedDelegate: SidebarItemIdentifiable, DisplayNameProvider, ArticleFetcher, SmallIconProvider {
|
||||||
var fetchType: FetchType { get }
|
var fetchType: FetchType { get }
|
||||||
func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock)
|
func fetchUnreadCount(for: Account, completion: @escaping SingleUnreadCountCompletionBlock)
|
||||||
}
|
}
|
||||||
|
@ -19,7 +19,7 @@ final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable {
|
|||||||
public static let shared = SmartFeedsController()
|
public static let shared = SmartFeedsController()
|
||||||
let nameForDisplay = NSLocalizedString("Smart Feeds", comment: "Smart Feeds group title")
|
let nameForDisplay = NSLocalizedString("Smart Feeds", comment: "Smart Feeds group title")
|
||||||
|
|
||||||
var smartFeeds = [Feed]()
|
var smartFeeds = [SidebarItem]()
|
||||||
let todayFeed = SmartFeed(delegate: TodayFeedDelegate())
|
let todayFeed = SmartFeed(delegate: TodayFeedDelegate())
|
||||||
let unreadFeed = UnreadFeed()
|
let unreadFeed = UnreadFeed()
|
||||||
let starredFeed = SmartFeed(delegate: StarredFeedDelegate())
|
let starredFeed = SmartFeed(delegate: StarredFeedDelegate())
|
||||||
@ -28,7 +28,7 @@ final class SmartFeedsController: DisplayNameProvider, ContainerIdentifiable {
|
|||||||
self.smartFeeds = [todayFeed, unreadFeed, starredFeed]
|
self.smartFeeds = [todayFeed, unreadFeed, starredFeed]
|
||||||
}
|
}
|
||||||
|
|
||||||
func find(by identifier: FeedIdentifier) -> PseudoFeed? {
|
func find(by identifier: SidebarItemIdentifier) -> PseudoFeed? {
|
||||||
switch identifier {
|
switch identifier {
|
||||||
case .smartFeed(let stringIdentifer):
|
case .smartFeed(let stringIdentifer):
|
||||||
switch stringIdentifer {
|
switch stringIdentifer {
|
||||||
|
@ -16,8 +16,8 @@ import Account
|
|||||||
|
|
||||||
struct StarredFeedDelegate: SmartFeedDelegate {
|
struct StarredFeedDelegate: SmartFeedDelegate {
|
||||||
|
|
||||||
var feedID: FeedIdentifier? {
|
var sidebarItemID: SidebarItemIdentifier? {
|
||||||
return FeedIdentifier.smartFeed(String(describing: StarredFeedDelegate.self))
|
return SidebarItemIdentifier.smartFeed(String(describing: StarredFeedDelegate.self))
|
||||||
}
|
}
|
||||||
|
|
||||||
let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title")
|
let nameForDisplay = NSLocalizedString("Starred", comment: "Starred pseudo-feed title")
|
||||||
|
@ -14,8 +14,8 @@ import Account
|
|||||||
|
|
||||||
struct TodayFeedDelegate: SmartFeedDelegate {
|
struct TodayFeedDelegate: SmartFeedDelegate {
|
||||||
|
|
||||||
var feedID: FeedIdentifier? {
|
var sidebarItemID: SidebarItemIdentifier? {
|
||||||
return FeedIdentifier.smartFeed(String(describing: TodayFeedDelegate.self))
|
return SidebarItemIdentifier.smartFeed(String(describing: TodayFeedDelegate.self))
|
||||||
}
|
}
|
||||||
|
|
||||||
let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title")
|
let nameForDisplay = NSLocalizedString("Today", comment: "Today pseudo-feed title")
|
||||||
|
@ -26,8 +26,8 @@ final class UnreadFeed: PseudoFeed {
|
|||||||
return .alwaysRead
|
return .alwaysRead
|
||||||
}
|
}
|
||||||
|
|
||||||
var feedID: FeedIdentifier? {
|
var sidebarItemID: SidebarItemIdentifier? {
|
||||||
return FeedIdentifier.smartFeed(String(describing: UnreadFeed.self))
|
return SidebarItemIdentifier.smartFeed(String(describing: UnreadFeed.self))
|
||||||
}
|
}
|
||||||
|
|
||||||
let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title")
|
let nameForDisplay = NSLocalizedString("All Unread", comment: "All Unread pseudo-feed title")
|
||||||
|
@ -19,13 +19,13 @@ typealias FetchRequestOperationResultBlock = (Set<Article>, FetchRequestOperatio
|
|||||||
final class FetchRequestOperation {
|
final class FetchRequestOperation {
|
||||||
|
|
||||||
let id: Int
|
let id: Int
|
||||||
let readFilterEnabledTable: [FeedIdentifier: Bool]
|
let readFilterEnabledTable: [SidebarItemIdentifier: Bool]
|
||||||
let resultBlock: FetchRequestOperationResultBlock
|
let resultBlock: FetchRequestOperationResultBlock
|
||||||
var isCanceled = false
|
var isCanceled = false
|
||||||
var isFinished = false
|
var isFinished = false
|
||||||
private let fetchers: [ArticleFetcher]
|
private let fetchers: [ArticleFetcher]
|
||||||
|
|
||||||
init(id: Int, readFilterEnabledTable: [FeedIdentifier: Bool], fetchers: [ArticleFetcher], resultBlock: @escaping FetchRequestOperationResultBlock) {
|
init(id: Int, readFilterEnabledTable: [SidebarItemIdentifier: Bool], fetchers: [ArticleFetcher], resultBlock: @escaping FetchRequestOperationResultBlock) {
|
||||||
precondition(Thread.isMainThread)
|
precondition(Thread.isMainThread)
|
||||||
self.id = id
|
self.id = id
|
||||||
self.readFilterEnabledTable = readFilterEnabledTable
|
self.readFilterEnabledTable = readFilterEnabledTable
|
||||||
@ -81,7 +81,7 @@ final class FetchRequestOperation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for fetcher in fetchers {
|
for fetcher in fetchers {
|
||||||
if (fetcher as? Feed)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true {
|
if (fetcher as? SidebarItem)?.readFiltered(readFilterEnabledTable: readFilterEnabledTable) ?? true {
|
||||||
fetcher.fetchUnreadArticlesAsync { articleSetResult in
|
fetcher.fetchUnreadArticlesAsync { articleSetResult in
|
||||||
let articles = (try? articleSetResult.get()) ?? Set<Article>()
|
let articles = (try? articleSetResult.get()) ?? Set<Article>()
|
||||||
process(articles)
|
process(articles)
|
||||||
|
@ -13,15 +13,15 @@ import Account
|
|||||||
|
|
||||||
final class WebFeedTreeControllerDelegate: TreeControllerDelegate {
|
final class WebFeedTreeControllerDelegate: TreeControllerDelegate {
|
||||||
|
|
||||||
private var filterExceptions = Set<FeedIdentifier>()
|
private var filterExceptions = Set<SidebarItemIdentifier>()
|
||||||
var isReadFiltered = false
|
var isReadFiltered = false
|
||||||
|
|
||||||
func addFilterException(_ feedID: FeedIdentifier) {
|
func addFilterException(_ feedID: SidebarItemIdentifier) {
|
||||||
filterExceptions.insert(feedID)
|
filterExceptions.insert(feedID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetFilterExceptions() {
|
func resetFilterExceptions() {
|
||||||
filterExceptions = Set<FeedIdentifier>()
|
filterExceptions = Set<SidebarItemIdentifier>()
|
||||||
}
|
}
|
||||||
|
|
||||||
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
|
func treeController(treeController: TreeController, childNodesFor node: Node) -> [Node]? {
|
||||||
@ -67,14 +67,14 @@ private extension WebFeedTreeControllerDelegate {
|
|||||||
var children = [AnyObject]()
|
var children = [AnyObject]()
|
||||||
|
|
||||||
for webFeed in container.topLevelWebFeeds {
|
for webFeed in container.topLevelWebFeeds {
|
||||||
if let feedID = webFeed.feedID, !(!filterExceptions.contains(feedID) && isReadFiltered && webFeed.unreadCount == 0) {
|
if let feedID = webFeed.sidebarItemID, !(!filterExceptions.contains(feedID) && isReadFiltered && webFeed.unreadCount == 0) {
|
||||||
children.append(webFeed)
|
children.append(webFeed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let folders = container.folders {
|
if let folders = container.folders {
|
||||||
for folder in folders {
|
for folder in folders {
|
||||||
if let feedID = folder.feedID, !(!filterExceptions.contains(feedID) && isReadFiltered && folder.unreadCount == 0) {
|
if let feedID = folder.sidebarItemID, !(!filterExceptions.contains(feedID) && isReadFiltered && folder.unreadCount == 0) {
|
||||||
children.append(folder)
|
children.append(folder)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -100,7 +100,7 @@ private extension WebFeedTreeControllerDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func createNode(representedObject: Any, parent: Node) -> Node? {
|
func createNode(representedObject: Any, parent: Node) -> Node? {
|
||||||
if let webFeed = representedObject as? WebFeed {
|
if let webFeed = representedObject as? Feed {
|
||||||
return createNode(webFeed: webFeed, parent: parent)
|
return createNode(webFeed: webFeed, parent: parent)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,7 +115,7 @@ private extension WebFeedTreeControllerDelegate {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func createNode(webFeed: WebFeed, parent: Node) -> Node {
|
func createNode(webFeed: Feed, parent: Node) -> Node {
|
||||||
return parent.createChildNode(webFeed)
|
return parent.createChildNode(webFeed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ final class UserNotificationManager: NSObject {
|
|||||||
|
|
||||||
private extension UserNotificationManager {
|
private extension UserNotificationManager {
|
||||||
|
|
||||||
func sendNotification(webFeed: WebFeed, article: Article) {
|
func sendNotification(webFeed: Feed, article: Article) {
|
||||||
let content = UNMutableNotificationContent()
|
let content = UNMutableNotificationContent()
|
||||||
|
|
||||||
content.title = webFeed.nameForDisplay
|
content.title = webFeed.nameForDisplay
|
||||||
@ -81,7 +81,7 @@ private extension UserNotificationManager {
|
|||||||
/// - webFeed: `WebFeed`
|
/// - webFeed: `WebFeed`
|
||||||
/// - Returns: A `UNNotifcationAttachment` if an icon is available. Otherwise nil.
|
/// - Returns: A `UNNotifcationAttachment` if an icon is available. Otherwise nil.
|
||||||
/// - Warning: In certain scenarios, this will return the `faviconTemplateImage`.
|
/// - Warning: In certain scenarios, this will return the `faviconTemplateImage`.
|
||||||
func thumbnailAttachment(for article: Article, webFeed: WebFeed) -> UNNotificationAttachment? {
|
func thumbnailAttachment(for article: Article, webFeed: Feed) -> UNNotificationAttachment? {
|
||||||
if let imageURL = article.iconImageUrl(webFeed: webFeed) {
|
if let imageURL = article.iconImageUrl(webFeed: webFeed) {
|
||||||
let thumbnail = try? UNNotificationAttachment(identifier: webFeed.webFeedID, url: imageURL, options: nil)
|
let thumbnail = try? UNNotificationAttachment(identifier: webFeed.webFeedID, url: imageURL, options: nil)
|
||||||
return thumbnail
|
return thumbnail
|
||||||
|
@ -15,7 +15,7 @@ class WebFeedInspectorViewController: UITableViewController {
|
|||||||
|
|
||||||
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0)
|
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0)
|
||||||
|
|
||||||
var webFeed: WebFeed!
|
var webFeed: Feed!
|
||||||
@IBOutlet weak var nameTextField: UITextField!
|
@IBOutlet weak var nameTextField: UITextField!
|
||||||
@IBOutlet weak var notifyAboutNewArticlesSwitch: UISwitch!
|
@IBOutlet weak var notifyAboutNewArticlesSwitch: UISwitch!
|
||||||
@IBOutlet weak var alwaysShowReaderViewSwitch: UISwitch!
|
@IBOutlet weak var alwaysShowReaderViewSwitch: UISwitch!
|
||||||
|
@ -13,7 +13,7 @@ import Account
|
|||||||
extension MasterFeedViewController: UITableViewDragDelegate {
|
extension MasterFeedViewController: UITableViewDragDelegate {
|
||||||
|
|
||||||
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||||
guard let node = coordinator.nodeFor(indexPath), let webFeed = node.representedObject as? WebFeed else {
|
guard let node = coordinator.nodeFor(indexPath), let webFeed = node.representedObject as? Feed else {
|
||||||
return [UIDragItem]()
|
return [UIDragItem]()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
|||||||
return UITableViewDropProposal(operation: .forbidden)
|
return UITableViewDropProposal(operation: .forbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? Feed,
|
guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? SidebarItem,
|
||||||
let destAccount = destFeed.account,
|
let destAccount = destFeed.account,
|
||||||
let destCell = tableView.cellForRow(at: destIndexPath) else {
|
let destCell = tableView.cellForRow(at: destIndexPath) else {
|
||||||
return UITableViewDropProposal(operation: .forbidden)
|
return UITableViewDropProposal(operation: .forbidden)
|
||||||
@ -31,7 +31,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
|||||||
// Validate account specific behaviors...
|
// Validate account specific behaviors...
|
||||||
if destAccount.behaviors.contains(.disallowFeedInMultipleFolders),
|
if destAccount.behaviors.contains(.disallowFeedInMultipleFolders),
|
||||||
let sourceNode = session.localDragSession?.items.first?.localObject as? Node,
|
let sourceNode = session.localDragSession?.items.first?.localObject as? Node,
|
||||||
let sourceWebFeed = sourceNode.representedObject as? WebFeed,
|
let sourceWebFeed = sourceNode.representedObject as? Feed,
|
||||||
sourceWebFeed.account?.accountID != destAccount.accountID && destAccount.hasWebFeed(withURL: sourceWebFeed.url) {
|
sourceWebFeed.account?.accountID != destAccount.accountID && destAccount.hasWebFeed(withURL: sourceWebFeed.url) {
|
||||||
return UITableViewDropProposal(operation: .forbidden)
|
return UITableViewDropProposal(operation: .forbidden)
|
||||||
}
|
}
|
||||||
@ -91,7 +91,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
guard let destination = destinationContainer, let webFeed = dragNode.representedObject as? WebFeed else { return }
|
guard let destination = destinationContainer, let webFeed = dragNode.representedObject as? Feed else { return }
|
||||||
|
|
||||||
if source.account == destination.account {
|
if source.account == destination.account {
|
||||||
moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination)
|
moveWebFeedInAccount(feed: webFeed, sourceContainer: source, destinationContainer: destination)
|
||||||
@ -100,7 +100,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveWebFeedInAccount(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
|
func moveWebFeedInAccount(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
|
||||||
guard sourceContainer !== destinationContainer else { return }
|
guard sourceContainer !== destinationContainer else { return }
|
||||||
|
|
||||||
BatchUpdate.shared.start()
|
BatchUpdate.shared.start()
|
||||||
@ -115,7 +115,7 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func moveWebFeedBetweenAccounts(feed: WebFeed, sourceContainer: Container, destinationContainer: Container) {
|
func moveWebFeedBetweenAccounts(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
|
||||||
|
|
||||||
if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) {
|
if let existingFeed = destinationContainer.account?.existingWebFeed(withURL: feed.url) {
|
||||||
|
|
||||||
|
@ -130,17 +130,17 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
@objc func webFeedIconDidBecomeAvailable(_ note: Notification) {
|
||||||
guard let webFeed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else {
|
guard let webFeed = note.userInfo?[UserInfoKey.webFeed] as? Feed else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
applyToCellsForRepresentedObject(webFeed, configureIcon(_:_:))
|
applyToCellsForRepresentedObject(webFeed, configureIcon(_:_:))
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func webFeedSettingDidChange(_ note: Notification) {
|
@objc func webFeedSettingDidChange(_ note: Notification) {
|
||||||
guard let webFeed = note.object as? WebFeed, let key = note.userInfo?[WebFeed.WebFeedSettingUserInfoKey] as? String else {
|
guard let webFeed = note.object as? Feed, let key = note.userInfo?[Feed.WebFeedSettingUserInfoKey] as? String else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if key == WebFeed.WebFeedSettingKey.homePageURL || key == WebFeed.WebFeedSettingKey.faviconURL {
|
if key == Feed.WebFeedSettingKey.homePageURL || key == Feed.WebFeedSettingKey.faviconURL {
|
||||||
configureCellsForRepresentedObject(webFeed)
|
configureCellsForRepresentedObject(webFeed)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -268,7 +268,7 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
|||||||
renameAction.backgroundColor = UIColor.systemOrange
|
renameAction.backgroundColor = UIColor.systemOrange
|
||||||
actions.append(renameAction)
|
actions.append(renameAction)
|
||||||
|
|
||||||
if let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed {
|
if let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed {
|
||||||
let moreTitle = NSLocalizedString("More", comment: "More")
|
let moreTitle = NSLocalizedString("More", comment: "More")
|
||||||
let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in
|
let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in
|
||||||
|
|
||||||
@ -320,10 +320,10 @@ class MasterFeedViewController: UITableViewController, UndoableCommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else {
|
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if feed is WebFeed {
|
if feed is Feed {
|
||||||
return makeWebFeedContextMenu(indexPath: indexPath, includeDeleteRename: true)
|
return makeWebFeedContextMenu(indexPath: indexPath, includeDeleteRename: true)
|
||||||
} else if feed is Folder {
|
} else if feed is Folder {
|
||||||
return makeFolderContextMenu(indexPath: indexPath)
|
return makeFolderContextMenu(indexPath: indexPath)
|
||||||
@ -795,7 +795,7 @@ private extension MasterFeedViewController {
|
|||||||
cell.isDisclosureAvailable = false
|
cell.isDisclosureAvailable = false
|
||||||
}
|
}
|
||||||
|
|
||||||
if let feed = node.representedObject as? Feed {
|
if let feed = node.representedObject as? SidebarItem {
|
||||||
cell.name = feed.nameForDisplay
|
cell.name = feed.nameForDisplay
|
||||||
cell.unreadCount = feed.unreadCount
|
cell.unreadCount = feed.unreadCount
|
||||||
}
|
}
|
||||||
@ -812,7 +812,7 @@ private extension MasterFeedViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func configureIcon(_ cell: MasterFeedTableViewCell, _ indexPath: IndexPath) {
|
func configureIcon(_ cell: MasterFeedTableViewCell, _ indexPath: IndexPath) {
|
||||||
guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? Feed, let feedID = feed.feedID else {
|
guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? SidebarItem, let feedID = feed.sidebarItemID else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cell.iconImage = IconImageCache.shared.imageFor(feedID)
|
cell.iconImage = IconImageCache.shared.imageFor(feedID)
|
||||||
@ -832,9 +832,9 @@ private extension MasterFeedViewController {
|
|||||||
func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ completion: (MasterFeedTableViewCell, IndexPath) -> Void) {
|
func applyToCellsForRepresentedObject(_ representedObject: AnyObject, _ completion: (MasterFeedTableViewCell, IndexPath) -> Void) {
|
||||||
applyToAvailableCells { (cell, indexPath) in
|
applyToAvailableCells { (cell, indexPath) in
|
||||||
if let node = coordinator.nodeFor(indexPath),
|
if let node = coordinator.nodeFor(indexPath),
|
||||||
let representedFeed = representedObject as? Feed,
|
let representedFeed = representedObject as? SidebarItem,
|
||||||
let candidate = node.representedObject as? Feed,
|
let candidate = node.representedObject as? SidebarItem,
|
||||||
representedFeed.feedID == candidate.feedID {
|
representedFeed.sidebarItemID == candidate.sidebarItemID {
|
||||||
completion(cell, indexPath)
|
completion(cell, indexPath)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -862,7 +862,7 @@ private extension MasterFeedViewController {
|
|||||||
if let folder = node.representedObject as? Folder {
|
if let folder = node.representedObject as? Folder {
|
||||||
return folder.account
|
return folder.account
|
||||||
}
|
}
|
||||||
if let feed = node.representedObject as? WebFeed {
|
if let feed = node.representedObject as? Feed {
|
||||||
return feed.account
|
return feed.account
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -1000,7 +1000,7 @@ private extension MasterFeedViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyFeedPageAction(indexPath: IndexPath) -> UIAction? {
|
func copyFeedPageAction(indexPath: IndexPath) -> UIAction? {
|
||||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||||
let url = URL(string: webFeed.url) else {
|
let url = URL(string: webFeed.url) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -1013,7 +1013,7 @@ private extension MasterFeedViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyFeedPageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
func copyFeedPageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||||
let url = URL(string: webFeed.url) else {
|
let url = URL(string: webFeed.url) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -1027,7 +1027,7 @@ private extension MasterFeedViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyHomePageAction(indexPath: IndexPath) -> UIAction? {
|
func copyHomePageAction(indexPath: IndexPath) -> UIAction? {
|
||||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||||
let homePageURL = webFeed.homePageURL,
|
let homePageURL = webFeed.homePageURL,
|
||||||
let url = URL(string: homePageURL) else {
|
let url = URL(string: homePageURL) else {
|
||||||
return nil
|
return nil
|
||||||
@ -1041,7 +1041,7 @@ private extension MasterFeedViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func copyHomePageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
func copyHomePageAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||||
let homePageURL = webFeed.homePageURL,
|
let homePageURL = webFeed.homePageURL,
|
||||||
let url = URL(string: homePageURL) else {
|
let url = URL(string: homePageURL) else {
|
||||||
return nil
|
return nil
|
||||||
@ -1056,7 +1056,7 @@ private extension MasterFeedViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func markAllAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
func markAllAsReadAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed,
|
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
||||||
webFeed.unreadCount > 0,
|
webFeed.unreadCount > 0,
|
||||||
let articles = try? webFeed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
let articles = try? webFeed.fetchArticles(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
||||||
return nil
|
return nil
|
||||||
@ -1096,7 +1096,7 @@ private extension MasterFeedViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getInfoAction(indexPath: IndexPath) -> UIAction? {
|
func getInfoAction(indexPath: IndexPath) -> UIAction? {
|
||||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed else {
|
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1124,7 +1124,7 @@ private extension MasterFeedViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getInfoAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
func getInfoAlertAction(indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||||
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? WebFeed else {
|
guard let webFeed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1137,7 +1137,7 @@ private extension MasterFeedViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func markAllAsReadAction(indexPath: IndexPath) -> UIAction? {
|
func markAllAsReadAction(indexPath: IndexPath) -> UIAction? {
|
||||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed,
|
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem,
|
||||||
let contentView = self.tableView.cellForRow(at: indexPath)?.contentView,
|
let contentView = self.tableView.cellForRow(at: indexPath)?.contentView,
|
||||||
feed.unreadCount > 0 else {
|
feed.unreadCount > 0 else {
|
||||||
return nil
|
return nil
|
||||||
@ -1179,7 +1179,7 @@ private extension MasterFeedViewController {
|
|||||||
|
|
||||||
|
|
||||||
func rename(indexPath: IndexPath) {
|
func rename(indexPath: IndexPath) {
|
||||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return }
|
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem else { return }
|
||||||
|
|
||||||
let formatString = NSLocalizedString("Rename “%@”", comment: "Rename feed")
|
let formatString = NSLocalizedString("Rename “%@”", comment: "Rename feed")
|
||||||
let title = NSString.localizedStringWithFormat(formatString as NSString, feed.nameForDisplay) as String
|
let title = NSString.localizedStringWithFormat(formatString as NSString, feed.nameForDisplay) as String
|
||||||
@ -1196,7 +1196,7 @@ private extension MasterFeedViewController {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let webFeed = feed as? WebFeed {
|
if let webFeed = feed as? Feed {
|
||||||
webFeed.rename(to: name) { result in
|
webFeed.rename(to: name) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
@ -1233,7 +1233,7 @@ private extension MasterFeedViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func delete(indexPath: IndexPath) {
|
func delete(indexPath: IndexPath) {
|
||||||
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? Feed else { return }
|
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem else { return }
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let message: String
|
let message: String
|
||||||
@ -1271,7 +1271,7 @@ private extension MasterFeedViewController {
|
|||||||
|
|
||||||
if let folder = deleteNode.representedObject as? Folder {
|
if let folder = deleteNode.representedObject as? Folder {
|
||||||
ActivityManager.cleanUp(folder)
|
ActivityManager.cleanUp(folder)
|
||||||
} else if let feed = deleteNode.representedObject as? WebFeed {
|
} else if let feed = deleteNode.representedObject as? Feed {
|
||||||
ActivityManager.cleanUp(feed)
|
ActivityManager.cleanUp(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -453,7 +453,7 @@ class MasterTimelineViewController: UITableViewController, UndoableCommandRunner
|
|||||||
titleView.iconView.iconImage = coordinator.timelineIconImage
|
titleView.iconView.iconImage = coordinator.timelineIconImage
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let feed = note.userInfo?[UserInfoKey.webFeed] as? WebFeed else {
|
guard let feed = note.userInfo?[UserInfoKey.webFeed] as? Feed else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
tableView.indexPathsForVisibleRows?.forEach { indexPath in
|
tableView.indexPathsForVisibleRows?.forEach { indexPath in
|
||||||
@ -631,7 +631,7 @@ private extension MasterTimelineViewController {
|
|||||||
titleView.label.text = coordinator.timelineFeed?.nameForDisplay
|
titleView.label.text = coordinator.timelineFeed?.nameForDisplay
|
||||||
updateTitleUnreadCount()
|
updateTitleUnreadCount()
|
||||||
|
|
||||||
if coordinator.timelineFeed is WebFeed {
|
if coordinator.timelineFeed is Feed {
|
||||||
titleView.buttonize()
|
titleView.buttonize()
|
||||||
titleView.addGestureRecognizer(feedTapGestureRecognizer)
|
titleView.addGestureRecognizer(feedTapGestureRecognizer)
|
||||||
} else {
|
} else {
|
||||||
|
@ -33,11 +33,11 @@ enum ShowFeedName {
|
|||||||
|
|
||||||
struct FeedNode: Hashable {
|
struct FeedNode: Hashable {
|
||||||
var node: Node
|
var node: Node
|
||||||
var feedID: FeedIdentifier
|
var feedID: SidebarItemIdentifier
|
||||||
|
|
||||||
init(_ node: Node) {
|
init(_ node: Node) {
|
||||||
self.node = node
|
self.node = node
|
||||||
self.feedID = (node.representedObject as! Feed).feedID!
|
self.feedID = (node.representedObject as! SidebarItem).sidebarItemID!
|
||||||
}
|
}
|
||||||
|
|
||||||
func hash(into hasher: inout Hasher) {
|
func hash(into hasher: inout Hasher) {
|
||||||
@ -94,12 +94,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
private var lastExpandedTable = Set<ContainerIdentifier>()
|
private var lastExpandedTable = Set<ContainerIdentifier>()
|
||||||
|
|
||||||
// Which Feeds have the Read Articles Filter enabled
|
// Which Feeds have the Read Articles Filter enabled
|
||||||
private var readFilterEnabledTable = [FeedIdentifier: Bool]()
|
private var readFilterEnabledTable = [SidebarItemIdentifier: Bool]()
|
||||||
|
|
||||||
// Flattened tree structure for the Sidebar
|
// Flattened tree structure for the Sidebar
|
||||||
private var shadowTable = [(sectionID: String, feedNodes: [FeedNode])]()
|
private var shadowTable = [(sectionID: String, feedNodes: [FeedNode])]()
|
||||||
|
|
||||||
private(set) var preSearchTimelineFeed: Feed?
|
private(set) var preSearchTimelineFeed: SidebarItem?
|
||||||
private var lastSearchString = ""
|
private var lastSearchString = ""
|
||||||
private var lastSearchScope: SearchScope? = nil
|
private var lastSearchScope: SearchScope? = nil
|
||||||
private var isSearching: Bool = false
|
private var isSearching: Bool = false
|
||||||
@ -157,7 +157,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var isReadArticlesFiltered: Bool {
|
var isReadArticlesFiltered: Bool {
|
||||||
if let feedID = timelineFeed?.feedID, let readFilterEnabled = readFilterEnabledTable[feedID] {
|
if let feedID = timelineFeed?.sidebarItemID, let readFilterEnabled = readFilterEnabledTable[feedID] {
|
||||||
return readFilterEnabled
|
return readFilterEnabled
|
||||||
} else {
|
} else {
|
||||||
return timelineDefaultReadFilterType != .none
|
return timelineDefaultReadFilterType != .none
|
||||||
@ -183,7 +183,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private var exceptionArticleFetcher: ArticleFetcher?
|
private var exceptionArticleFetcher: ArticleFetcher?
|
||||||
private(set) var timelineFeed: Feed?
|
private(set) var timelineFeed: SidebarItem?
|
||||||
|
|
||||||
var timelineMiddleIndexPath: IndexPath?
|
var timelineMiddleIndexPath: IndexPath?
|
||||||
|
|
||||||
@ -363,7 +363,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
|
|
||||||
if let readArticlesFilterState = windowState[UserInfoKey.readArticlesFilterState] as? [[AnyHashable: AnyHashable]: Bool] {
|
if let readArticlesFilterState = windowState[UserInfoKey.readArticlesFilterState] as? [[AnyHashable: AnyHashable]: Bool] {
|
||||||
for key in readArticlesFilterState.keys {
|
for key in readArticlesFilterState.keys {
|
||||||
if let feedIdentifier = FeedIdentifier(userInfo: key) {
|
if let feedIdentifier = SidebarItemIdentifier(userInfo: key) {
|
||||||
readFilterEnabledTable[feedIdentifier] = readArticlesFilterState[key]
|
readFilterEnabledTable[feedIdentifier] = readArticlesFilterState[key]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -545,7 +545,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func userDidAddFeed(_ notification: Notification) {
|
@objc func userDidAddFeed(_ notification: Notification) {
|
||||||
guard let webFeed = notification.userInfo?[UserInfoKey.webFeed] as? WebFeed else {
|
guard let webFeed = notification.userInfo?[UserInfoKey.webFeed] as? Feed else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
discloseWebFeed(webFeed, animations: [.scroll, .navigation])
|
discloseWebFeed(webFeed, animations: [.scroll, .navigation])
|
||||||
@ -557,7 +557,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@objc func accountDidDownloadArticles(_ note: Notification) {
|
@objc func accountDidDownloadArticles(_ note: Notification) {
|
||||||
guard let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<WebFeed> else {
|
guard let feeds = note.userInfo?[Account.UserInfoKey.webFeeds] as? Set<Feed> else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -625,7 +625,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func toggleReadArticlesFilter() {
|
func toggleReadArticlesFilter() {
|
||||||
guard let feedID = timelineFeed?.feedID else {
|
guard let feedID = timelineFeed?.sidebarItemID else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -638,10 +638,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
refreshTimeline(resetScroll: false)
|
refreshTimeline(resetScroll: false)
|
||||||
}
|
}
|
||||||
|
|
||||||
func nodeFor(feedID: FeedIdentifier) -> Node? {
|
func nodeFor(feedID: SidebarItemIdentifier) -> Node? {
|
||||||
return treeController.rootNode.descendantNode(where: { node in
|
return treeController.rootNode.descendantNode(where: { node in
|
||||||
if let feed = node.representedObject as? Feed {
|
if let feed = node.representedObject as? SidebarItem {
|
||||||
return feed.feedID == feedID
|
return feed.sidebarItemID == feedID
|
||||||
} else {
|
} else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@ -783,7 +783,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
return indexPathFor(node)
|
return indexPathFor(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectFeed(_ feed: Feed?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) {
|
func selectFeed(_ feed: SidebarItem?, animations: Animations = [], deselectArticle: Bool = true, completion: (() -> Void)? = nil) {
|
||||||
let indexPath: IndexPath? = {
|
let indexPath: IndexPath? = {
|
||||||
if let feed = feed, let indexPath = indexPathFor(feed as AnyObject) {
|
if let feed = feed, let indexPath = indexPathFor(feed as AnyObject) {
|
||||||
return indexPath
|
return indexPath
|
||||||
@ -807,7 +807,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
selectArticle(nil)
|
selectArticle(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let ip = indexPath, let node = nodeFor(ip), let feed = node.representedObject as? Feed {
|
if let ip = indexPath, let node = nodeFor(ip), let feed = node.representedObject as? SidebarItem {
|
||||||
|
|
||||||
self.activityManager.selecting(feed: feed)
|
self.activityManager.selecting(feed: feed)
|
||||||
self.installTimelineControllerIfNecessary(animated: animations.contains(.navigation))
|
self.installTimelineControllerIfNecessary(animated: animations.contains(.navigation))
|
||||||
@ -1120,15 +1120,15 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred)
|
markArticlesWithUndo([article], statusKey: .starred, flag: !article.status.starred)
|
||||||
}
|
}
|
||||||
|
|
||||||
func timelineFeedIsEqualTo(_ feed: WebFeed) -> Bool {
|
func timelineFeedIsEqualTo(_ feed: Feed) -> Bool {
|
||||||
guard let timelineFeed = timelineFeed as? WebFeed else {
|
guard let timelineFeed = timelineFeed as? Feed else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return timelineFeed == feed
|
return timelineFeed == feed
|
||||||
}
|
}
|
||||||
|
|
||||||
func discloseWebFeed(_ webFeed: WebFeed, initialLoad: Bool = false, animations: Animations = [], completion: (() -> Void)? = nil) {
|
func discloseWebFeed(_ webFeed: Feed, initialLoad: Bool = false, animations: Animations = [], completion: (() -> Void)? = nil) {
|
||||||
if isSearching {
|
if isSearching {
|
||||||
masterTimelineViewController?.hideSearch()
|
masterTimelineViewController?.hideSearch()
|
||||||
}
|
}
|
||||||
@ -1145,10 +1145,10 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
markExpanded(parentFolder)
|
markExpanded(parentFolder)
|
||||||
}
|
}
|
||||||
|
|
||||||
if let webFeedFeedID = webFeed.feedID {
|
if let webFeedFeedID = webFeed.sidebarItemID {
|
||||||
self.treeControllerDelegate.addFilterException(webFeedFeedID)
|
self.treeControllerDelegate.addFilterException(webFeedFeedID)
|
||||||
}
|
}
|
||||||
if let parentFolderFeedID = parentFolder?.feedID {
|
if let parentFolderFeedID = parentFolder?.sidebarItemID {
|
||||||
self.treeControllerDelegate.addFilterException(parentFolderFeedID)
|
self.treeControllerDelegate.addFilterException(parentFolderFeedID)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1196,7 +1196,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func showFeedInspector() {
|
func showFeedInspector() {
|
||||||
let timelineWebFeed = timelineFeed as? WebFeed
|
let timelineWebFeed = timelineFeed as? Feed
|
||||||
let articleFeed = currentArticle?.webFeed
|
let articleFeed = currentArticle?.webFeed
|
||||||
guard let feed = timelineWebFeed ?? articleFeed else {
|
guard let feed = timelineWebFeed ?? articleFeed else {
|
||||||
return
|
return
|
||||||
@ -1204,7 +1204,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
showFeedInspector(for: feed)
|
showFeedInspector(for: feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
func showFeedInspector(for feed: WebFeed) {
|
func showFeedInspector(for feed: Feed) {
|
||||||
let feedInspectorNavController =
|
let feedInspectorNavController =
|
||||||
UIStoryboard.inspector.instantiateViewController(identifier: "FeedInspectorNavigationViewController") as! UINavigationController
|
UIStoryboard.inspector.instantiateViewController(identifier: "FeedInspectorNavigationViewController") as! UINavigationController
|
||||||
let feedInspectorController = feedInspectorNavController.topViewController as! WebFeedInspectorViewController
|
let feedInspectorController = feedInspectorNavController.topViewController as! WebFeedInspectorViewController
|
||||||
@ -1248,7 +1248,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner {
|
|||||||
|
|
||||||
func homePageURLForFeed(_ indexPath: IndexPath) -> URL? {
|
func homePageURLForFeed(_ indexPath: IndexPath) -> URL? {
|
||||||
guard let node = nodeFor(indexPath),
|
guard let node = nodeFor(indexPath),
|
||||||
let feed = node.representedObject as? WebFeed,
|
let feed = node.representedObject as? Feed,
|
||||||
let homePageURL = feed.homePageURL,
|
let homePageURL = feed.homePageURL,
|
||||||
let url = URL(string: homePageURL) else {
|
let url = URL(string: homePageURL) else {
|
||||||
return nil
|
return nil
|
||||||
@ -1458,7 +1458,7 @@ private extension SceneCoordinator {
|
|||||||
articleDictionaryNeedsUpdate = false
|
articleDictionaryNeedsUpdate = false
|
||||||
}
|
}
|
||||||
|
|
||||||
func ensureFeedIsAvailableToSelect(_ feed: Feed, completion: @escaping () -> Void) {
|
func ensureFeedIsAvailableToSelect(_ feed: SidebarItem, completion: @escaping () -> Void) {
|
||||||
addToFilterExeptionsIfNecessary(feed)
|
addToFilterExeptionsIfNecessary(feed)
|
||||||
addShadowTableToFilterExceptions()
|
addShadowTableToFilterExceptions()
|
||||||
|
|
||||||
@ -1468,15 +1468,15 @@ private extension SceneCoordinator {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func addToFilterExeptionsIfNecessary(_ feed: Feed?) {
|
func addToFilterExeptionsIfNecessary(_ feed: SidebarItem?) {
|
||||||
if isReadFeedsFiltered, let feedID = feed?.feedID {
|
if isReadFeedsFiltered, let feedID = feed?.sidebarItemID {
|
||||||
if feed is SmartFeed {
|
if feed is SmartFeed {
|
||||||
treeControllerDelegate.addFilterException(feedID)
|
treeControllerDelegate.addFilterException(feedID)
|
||||||
} else if let folderFeed = feed as? Folder {
|
} else if let folderFeed = feed as? Folder {
|
||||||
if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil {
|
if folderFeed.account?.existingFolder(withID: folderFeed.folderID) != nil {
|
||||||
treeControllerDelegate.addFilterException(feedID)
|
treeControllerDelegate.addFilterException(feedID)
|
||||||
}
|
}
|
||||||
} else if let webFeed = feed as? WebFeed {
|
} else if let webFeed = feed as? Feed {
|
||||||
if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil {
|
if webFeed.account?.existingWebFeed(withWebFeedID: webFeed.webFeedID) != nil {
|
||||||
treeControllerDelegate.addFilterException(feedID)
|
treeControllerDelegate.addFilterException(feedID)
|
||||||
addParentFolderToFilterExceptions(webFeed)
|
addParentFolderToFilterExceptions(webFeed)
|
||||||
@ -1485,10 +1485,10 @@ private extension SceneCoordinator {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func addParentFolderToFilterExceptions(_ feed: Feed) {
|
func addParentFolderToFilterExceptions(_ feed: SidebarItem) {
|
||||||
guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject),
|
guard let node = treeController.rootNode.descendantNodeRepresentingObject(feed as AnyObject),
|
||||||
let folder = node.parent?.representedObject as? Folder,
|
let folder = node.parent?.representedObject as? Folder,
|
||||||
let folderFeedID = folder.feedID else {
|
let folderFeedID = folder.sidebarItemID else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1498,7 +1498,7 @@ private extension SceneCoordinator {
|
|||||||
func addShadowTableToFilterExceptions() {
|
func addShadowTableToFilterExceptions() {
|
||||||
for section in shadowTable {
|
for section in shadowTable {
|
||||||
for feedNode in section.feedNodes {
|
for feedNode in section.feedNodes {
|
||||||
if let feed = feedNode.node.representedObject as? Feed, let feedID = feed.feedID {
|
if let feed = feedNode.node.representedObject as? SidebarItem, let feedID = feed.sidebarItemID {
|
||||||
treeControllerDelegate.addFilterException(feedID)
|
treeControllerDelegate.addFilterException(feedID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1628,10 +1628,10 @@ private extension SceneCoordinator {
|
|||||||
return ShadowTableChanges(deletes: deletes, inserts: inserts, moves: moves, rowChanges: changes)
|
return ShadowTableChanges(deletes: deletes, inserts: inserts, moves: moves, rowChanges: changes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func shadowTableContains(_ feed: Feed) -> Bool {
|
func shadowTableContains(_ feed: SidebarItem) -> Bool {
|
||||||
for section in shadowTable {
|
for section in shadowTable {
|
||||||
for feedNode in section.feedNodes {
|
for feedNode in section.feedNodes {
|
||||||
if let nodeFeed = feedNode.node.representedObject as? Feed, nodeFeed.feedID == feed.feedID {
|
if let nodeFeed = feedNode.node.representedObject as? SidebarItem, nodeFeed.sidebarItemID == feed.sidebarItemID {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1652,7 +1652,7 @@ private extension SceneCoordinator {
|
|||||||
return indexPathFor(node)
|
return indexPathFor(node)
|
||||||
}
|
}
|
||||||
|
|
||||||
func setTimelineFeed(_ feed: Feed?, animated: Bool, completion: (() -> Void)? = nil) {
|
func setTimelineFeed(_ feed: SidebarItem?, animated: Bool, completion: (() -> Void)? = nil) {
|
||||||
timelineFeed = feed
|
timelineFeed = feed
|
||||||
|
|
||||||
fetchAndReplaceArticlesAsync(animated: animated) {
|
fetchAndReplaceArticlesAsync(animated: animated) {
|
||||||
@ -1663,7 +1663,7 @@ private extension SceneCoordinator {
|
|||||||
|
|
||||||
func updateShowNamesAndIcons() {
|
func updateShowNamesAndIcons() {
|
||||||
|
|
||||||
if timelineFeed is WebFeed {
|
if timelineFeed is Feed {
|
||||||
showFeedNames = {
|
showFeedNames = {
|
||||||
for article in articles {
|
for article in articles {
|
||||||
if !article.byline().isEmpty {
|
if !article.byline().isEmpty {
|
||||||
@ -2074,11 +2074,11 @@ private extension SceneCoordinator {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func timelineFetcherContainsAnyFeed(_ feeds: Set<WebFeed>) -> Bool {
|
func timelineFetcherContainsAnyFeed(_ feeds: Set<Feed>) -> Bool {
|
||||||
|
|
||||||
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
||||||
|
|
||||||
if let feed = timelineFeed as? WebFeed {
|
if let feed = timelineFeed as? Feed {
|
||||||
for oneFeed in feeds {
|
for oneFeed in feeds {
|
||||||
if feed.webFeedID == oneFeed.webFeedID || feed.url == oneFeed.url {
|
if feed.webFeedID == oneFeed.webFeedID || feed.url == oneFeed.url {
|
||||||
return true
|
return true
|
||||||
@ -2242,7 +2242,7 @@ private extension SceneCoordinator {
|
|||||||
func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) {
|
func handleSelectFeed(_ userInfo: [AnyHashable : Any]?) {
|
||||||
guard let userInfo = userInfo,
|
guard let userInfo = userInfo,
|
||||||
let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable],
|
let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable],
|
||||||
let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo) else {
|
let feedIdentifier = SidebarItemIdentifier(userInfo: feedIdentifierUserInfo) else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2327,7 +2327,7 @@ private extension SceneCoordinator {
|
|||||||
|
|
||||||
func restoreFeedSelection(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool {
|
func restoreFeedSelection(_ userInfo: [AnyHashable : Any], accountID: String, webFeedID: String, articleID: String) -> Bool {
|
||||||
guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable],
|
guard let feedIdentifierUserInfo = userInfo[UserInfoKey.feedIdentifier] as? [AnyHashable : AnyHashable],
|
||||||
let feedIdentifier = FeedIdentifier(userInfo: feedIdentifierUserInfo),
|
let feedIdentifier = SidebarItemIdentifier(userInfo: feedIdentifierUserInfo),
|
||||||
let isShowingExtractedArticle = userInfo[UserInfoKey.isShowingExtractedArticle] as? Bool,
|
let isShowingExtractedArticle = userInfo[UserInfoKey.isShowingExtractedArticle] as? Bool,
|
||||||
let articleWindowScrollY = userInfo[UserInfoKey.articleWindowScrollY] as? Int else {
|
let articleWindowScrollY = userInfo[UserInfoKey.articleWindowScrollY] as? Int else {
|
||||||
return false
|
return false
|
||||||
@ -2349,7 +2349,7 @@ private extension SceneCoordinator {
|
|||||||
let found = selectFeedAndArticle(feedIdentifier: feedIdentifier, articleID: articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY)
|
let found = selectFeedAndArticle(feedIdentifier: feedIdentifier, articleID: articleID, isShowingExtractedArticle: isShowingExtractedArticle, articleWindowScrollY: articleWindowScrollY)
|
||||||
if found {
|
if found {
|
||||||
treeControllerDelegate.addFilterException(feedIdentifier)
|
treeControllerDelegate.addFilterException(feedIdentifier)
|
||||||
if let webFeedNode = nodeFor(feedID: feedIdentifier), let folder = webFeedNode.parent?.representedObject as? Folder, let folderFeedID = folder.feedID {
|
if let webFeedNode = nodeFor(feedID: feedIdentifier), let folder = webFeedNode.parent?.representedObject as? Folder, let folderFeedID = folder.sidebarItemID {
|
||||||
treeControllerDelegate.addFilterException(folderFeedID)
|
treeControllerDelegate.addFilterException(folderFeedID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -2379,13 +2379,13 @@ private extension SceneCoordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func findWebFeedNode(webFeedID: String, beginningAt startingNode: Node) -> Node? {
|
func findWebFeedNode(webFeedID: String, beginningAt startingNode: Node) -> Node? {
|
||||||
if let node = startingNode.descendantNode(where: { ($0.representedObject as? WebFeed)?.webFeedID == webFeedID }) {
|
if let node = startingNode.descendantNode(where: { ($0.representedObject as? Feed)?.webFeedID == webFeedID }) {
|
||||||
return node
|
return node
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func selectFeedAndArticle(feedIdentifier: FeedIdentifier, articleID: String, isShowingExtractedArticle: Bool, articleWindowScrollY: Int) -> Bool {
|
func selectFeedAndArticle(feedIdentifier: SidebarItemIdentifier, articleID: String, isShowingExtractedArticle: Bool, articleWindowScrollY: Int) -> Bool {
|
||||||
guard let feedNode = nodeFor(feedID: feedIdentifier), let feedIndexPath = indexPathFor(feedNode) else { return false }
|
guard let feedNode = nodeFor(feedID: feedIdentifier), let feedIndexPath = indexPathFor(feedNode) else { return false }
|
||||||
|
|
||||||
selectFeed(indexPath: feedIndexPath) {
|
selectFeed(indexPath: feedIndexPath) {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user