2017-07-03 10:29:44 -07:00
|
|
|
|
//
|
|
|
|
|
// Account.swift
|
|
|
|
|
// DataModel
|
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 7/1/17.
|
|
|
|
|
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
2019-04-17 13:01:26 -05:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
import UIKit
|
|
|
|
|
#endif
|
|
|
|
|
|
2017-07-03 10:29:44 -07:00
|
|
|
|
import Foundation
|
|
|
|
|
import RSCore
|
2018-07-23 18:29:08 -07:00
|
|
|
|
import Articles
|
2017-09-17 12:08:50 -07:00
|
|
|
|
import RSParser
|
2018-07-23 18:29:08 -07:00
|
|
|
|
import ArticlesDatabase
|
2017-10-07 17:20:19 -07:00
|
|
|
|
import RSWeb
|
2017-07-03 10:29:44 -07:00
|
|
|
|
|
2017-10-07 14:40:14 -07:00
|
|
|
|
public extension Notification.Name {
|
2019-02-12 10:04:18 -05:00
|
|
|
|
static let AccountRefreshDidBegin = Notification.Name(rawValue: "AccountRefreshDidBegin")
|
|
|
|
|
static let AccountRefreshDidFinish = Notification.Name(rawValue: "AccountRefreshDidFinish")
|
|
|
|
|
static let AccountRefreshProgressDidChange = Notification.Name(rawValue: "AccountRefreshProgressDidChange")
|
|
|
|
|
static let AccountDidDownloadArticles = Notification.Name(rawValue: "AccountDidDownloadArticles")
|
2019-05-02 05:41:44 -05:00
|
|
|
|
static let AccountStateDidChange = Notification.Name(rawValue: "AccountStateDidChange")
|
2019-02-12 10:04:18 -05:00
|
|
|
|
static let StatusesDidChange = Notification.Name(rawValue: "StatusesDidChange")
|
2017-10-07 14:40:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-07-03 10:29:44 -07:00
|
|
|
|
public enum AccountType: Int {
|
|
|
|
|
// Raw values should not change since they’re stored on disk.
|
|
|
|
|
case onMyMac = 1
|
|
|
|
|
case feedly = 16
|
|
|
|
|
case feedbin = 17
|
|
|
|
|
case feedWrangler = 18
|
|
|
|
|
case newsBlur = 19
|
|
|
|
|
// TODO: more
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-10 13:23:12 -07:00
|
|
|
|
public final class Account: DisplayNameProvider, UnreadCountProvider, Container, Hashable {
|
2017-07-03 10:29:44 -07:00
|
|
|
|
|
2017-11-04 22:51:14 -07:00
|
|
|
|
public struct UserInfoKey {
|
2017-10-08 21:06:25 -07:00
|
|
|
|
public static let newArticles = "newArticles" // AccountDidDownloadArticles
|
|
|
|
|
public static let updatedArticles = "updatedArticles" // AccountDidDownloadArticles
|
2017-10-08 22:25:33 -07:00
|
|
|
|
public static let statuses = "statuses" // StatusesDidChange
|
|
|
|
|
public static let articles = "articles" // StatusesDidChange
|
2018-01-17 17:28:09 -08:00
|
|
|
|
public static let feeds = "feeds" // AccountDidDownloadArticles, StatusesDidChange
|
2017-10-08 01:54:37 -07:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-19 16:52:21 -05:00
|
|
|
|
public static let defaultLocalAccountName: String = {
|
|
|
|
|
let defaultName: String
|
|
|
|
|
#if os(macOS)
|
|
|
|
|
defaultName = NSLocalizedString("On My Mac", comment: "Account name")
|
|
|
|
|
#else
|
|
|
|
|
if UIDevice.current.userInterfaceIdiom == .pad {
|
|
|
|
|
defaultName = NSLocalizedString("On My iPad", comment: "Account name")
|
|
|
|
|
} else {
|
|
|
|
|
defaultName = NSLocalizedString("On My iPhone", comment: "Account name")
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
return defaultName
|
|
|
|
|
}()
|
|
|
|
|
|
2019-05-31 08:05:26 -05:00
|
|
|
|
public var isDeleted = false
|
|
|
|
|
|
2019-05-29 20:47:52 -05:00
|
|
|
|
public var account: Account? {
|
|
|
|
|
return self
|
|
|
|
|
}
|
2017-09-17 12:08:50 -07:00
|
|
|
|
public let accountID: String
|
2017-07-03 10:29:44 -07:00
|
|
|
|
public let type: AccountType
|
2019-03-27 22:10:14 -07:00
|
|
|
|
public var nameForDisplay: String {
|
|
|
|
|
guard let name = name, !name.isEmpty else {
|
|
|
|
|
return defaultName
|
|
|
|
|
}
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var name: String? {
|
|
|
|
|
get {
|
2019-05-05 07:49:59 -05:00
|
|
|
|
return metadata.name
|
2019-03-27 22:10:14 -07:00
|
|
|
|
}
|
|
|
|
|
set {
|
2019-03-31 16:12:03 -07:00
|
|
|
|
let currentNameForDisplay = nameForDisplay
|
2019-05-05 07:49:59 -05:00
|
|
|
|
if newValue != metadata.name {
|
|
|
|
|
metadata.name = newValue
|
2019-03-31 16:12:03 -07:00
|
|
|
|
if currentNameForDisplay != nameForDisplay {
|
|
|
|
|
postDisplayNameDidChangeNotification()
|
|
|
|
|
}
|
2019-03-27 22:10:14 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
public let defaultName: String
|
2019-05-02 05:41:44 -05:00
|
|
|
|
|
|
|
|
|
public var isActive: Bool {
|
|
|
|
|
get {
|
2019-05-05 07:49:59 -05:00
|
|
|
|
return metadata.isActive
|
2019-05-02 05:41:44 -05:00
|
|
|
|
}
|
|
|
|
|
set {
|
2019-05-05 07:49:59 -05:00
|
|
|
|
if newValue != metadata.isActive {
|
|
|
|
|
metadata.isActive = newValue
|
2019-05-02 05:41:44 -05:00
|
|
|
|
NotificationCenter.default.post(name: .AccountStateDidChange, object: self, userInfo: nil)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-03-27 22:10:14 -07:00
|
|
|
|
|
2018-09-16 17:54:42 -07:00
|
|
|
|
public var topLevelFeeds = Set<Feed>()
|
|
|
|
|
public var folders: Set<Folder>? = Set<Folder>()
|
|
|
|
|
private var feedDictionaryNeedsUpdate = true
|
|
|
|
|
private var _idToFeedDictionary = [String: Feed]()
|
|
|
|
|
var idToFeedDictionary: [String: Feed] {
|
|
|
|
|
if feedDictionaryNeedsUpdate {
|
|
|
|
|
rebuildFeedDictionaries()
|
|
|
|
|
}
|
|
|
|
|
return _idToFeedDictionary
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-02 18:17:52 -05:00
|
|
|
|
var username: String? {
|
|
|
|
|
get {
|
2019-05-05 07:49:59 -05:00
|
|
|
|
return metadata.username
|
2019-05-02 18:17:52 -05:00
|
|
|
|
}
|
|
|
|
|
set {
|
2019-05-05 07:49:59 -05:00
|
|
|
|
if newValue != metadata.username {
|
|
|
|
|
metadata.username = newValue
|
2019-05-02 18:17:52 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-16 17:54:42 -07:00
|
|
|
|
private var fetchingAllUnreadCounts = false
|
2019-04-27 16:16:46 -05:00
|
|
|
|
var isUnreadCountsInitialized = false
|
2018-09-16 17:54:42 -07:00
|
|
|
|
|
2017-07-03 10:29:44 -07:00
|
|
|
|
let dataFolder: String
|
2018-07-23 18:29:08 -07:00
|
|
|
|
let database: ArticlesDatabase
|
2019-05-05 03:25:21 -05:00
|
|
|
|
var delegate: AccountDelegate
|
2018-02-17 15:38:54 -08:00
|
|
|
|
static let saveQueue = CoalescingQueue(name: "Account Save Queue", interval: 1.0)
|
2017-10-07 18:15:42 -07:00
|
|
|
|
|
2018-09-14 22:06:03 -07:00
|
|
|
|
private var unreadCounts = [String: Int]() // [feedID: Int]
|
2018-09-14 13:25:38 -07:00
|
|
|
|
private let opmlFilePath: String
|
|
|
|
|
|
2018-09-16 17:54:42 -07:00
|
|
|
|
private var _flattenedFeeds = Set<Feed>()
|
|
|
|
|
private var flattenedFeedsNeedUpdate = true
|
|
|
|
|
|
2019-05-05 07:49:59 -05:00
|
|
|
|
private let metadataPath: String
|
2019-05-05 17:46:53 -05:00
|
|
|
|
var metadata = AccountMetadata()
|
2019-05-05 07:49:59 -05:00
|
|
|
|
private var metadataDirty = false {
|
2019-03-27 22:10:14 -07:00
|
|
|
|
didSet {
|
2019-05-05 07:49:59 -05:00
|
|
|
|
queueSaveAccountMetadatafNeeded()
|
2019-03-27 22:10:14 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
2019-03-20 22:10:22 -07:00
|
|
|
|
|
2019-03-13 23:41:43 -07:00
|
|
|
|
private let feedMetadataPath: String
|
|
|
|
|
private typealias FeedMetadataDictionary = [String: FeedMetadata]
|
|
|
|
|
private var feedMetadata = FeedMetadataDictionary()
|
|
|
|
|
private var feedMetadataDirty = false {
|
|
|
|
|
didSet {
|
2019-05-05 07:49:59 -05:00
|
|
|
|
queueSaveFeedMetadataIfNeeded()
|
2019-03-13 23:41:43 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-16 20:02:24 -07:00
|
|
|
|
private var startingUp = true
|
|
|
|
|
|
2017-10-07 21:41:21 -07:00
|
|
|
|
public var dirty = false {
|
2017-10-07 18:15:42 -07:00
|
|
|
|
didSet {
|
2019-02-07 21:29:05 -08:00
|
|
|
|
queueSaveToDiskIfNeeded()
|
2017-10-07 18:15:42 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-10-07 14:40:14 -07:00
|
|
|
|
|
2017-10-10 13:23:12 -07:00
|
|
|
|
public var unreadCount = 0 {
|
|
|
|
|
didSet {
|
|
|
|
|
if unreadCount != oldValue {
|
|
|
|
|
postUnreadCountDidChangeNotification()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-28 17:42:19 -05:00
|
|
|
|
public var usesTags: Bool {
|
|
|
|
|
return delegate.usesTags
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-07 14:40:14 -07:00
|
|
|
|
var refreshInProgress = false {
|
|
|
|
|
didSet {
|
|
|
|
|
if refreshInProgress != oldValue {
|
|
|
|
|
if refreshInProgress {
|
|
|
|
|
NotificationCenter.default.post(name: .AccountRefreshDidBegin, object: self)
|
|
|
|
|
}
|
|
|
|
|
else {
|
2017-10-07 18:31:34 -07:00
|
|
|
|
NotificationCenter.default.post(name: .AccountRefreshDidFinish, object: self)
|
2018-02-17 18:02:40 -08:00
|
|
|
|
queueSaveToDiskIfNeeded()
|
2017-10-07 14:40:14 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-28 13:16:47 -07:00
|
|
|
|
|
2017-10-07 17:20:19 -07:00
|
|
|
|
var refreshProgress: DownloadProgress {
|
2018-02-14 13:14:25 -08:00
|
|
|
|
return delegate.refreshProgress
|
2017-10-07 17:20:19 -07:00
|
|
|
|
}
|
2018-02-14 13:14:25 -08:00
|
|
|
|
|
2017-09-28 13:16:47 -07:00
|
|
|
|
var supportsSubFolders: Bool {
|
2018-02-14 13:14:25 -08:00
|
|
|
|
return delegate.supportsSubFolders
|
2017-09-28 13:16:47 -07:00
|
|
|
|
}
|
2018-02-14 13:14:25 -08:00
|
|
|
|
|
2019-05-12 07:22:33 -05:00
|
|
|
|
init?(dataFolder: String, type: AccountType, accountID: String, transport: Transport? = nil) {
|
2018-02-14 13:14:25 -08:00
|
|
|
|
|
2019-05-02 18:17:52 -05:00
|
|
|
|
switch type {
|
|
|
|
|
case .onMyMac:
|
|
|
|
|
self.delegate = LocalAccountDelegate()
|
|
|
|
|
case .feedbin:
|
2019-05-14 18:24:19 -05:00
|
|
|
|
self.delegate = FeedbinAccountDelegate(dataFolder: dataFolder, transport: transport)
|
2019-05-02 18:17:52 -05:00
|
|
|
|
default:
|
|
|
|
|
fatalError("Only Local and Feedbin accounts are supported")
|
|
|
|
|
}
|
2017-09-17 17:03:58 -07:00
|
|
|
|
|
|
|
|
|
self.accountID = accountID
|
|
|
|
|
self.type = type
|
|
|
|
|
self.dataFolder = dataFolder
|
2017-12-19 17:48:30 -08:00
|
|
|
|
|
2018-09-14 13:25:38 -07:00
|
|
|
|
self.opmlFilePath = (dataFolder as NSString).appendingPathComponent("Subscriptions.opml")
|
|
|
|
|
|
2017-09-17 17:03:58 -07:00
|
|
|
|
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("DB.sqlite3")
|
2018-07-23 18:29:08 -07:00
|
|
|
|
self.database = ArticlesDatabase(databaseFilePath: databaseFilePath, accountID: accountID)
|
2017-09-27 13:29:05 -07:00
|
|
|
|
|
2019-03-13 23:41:43 -07:00
|
|
|
|
self.feedMetadataPath = (dataFolder as NSString).appendingPathComponent("FeedMetadata.plist")
|
2019-05-05 07:49:59 -05:00
|
|
|
|
self.metadataPath = (dataFolder as NSString).appendingPathComponent("Settings.plist")
|
2018-09-12 22:28:21 -07:00
|
|
|
|
|
2019-03-27 22:10:14 -07:00
|
|
|
|
switch type {
|
|
|
|
|
case .onMyMac:
|
2019-05-19 16:52:21 -05:00
|
|
|
|
defaultName = Account.defaultLocalAccountName
|
2019-03-27 22:10:14 -07:00
|
|
|
|
case .feedly:
|
|
|
|
|
defaultName = "Feedly"
|
|
|
|
|
case .feedbin:
|
|
|
|
|
defaultName = "Feedbin"
|
|
|
|
|
case .feedWrangler:
|
|
|
|
|
defaultName = "FeedWrangler"
|
|
|
|
|
case .newsBlur:
|
|
|
|
|
defaultName = "NewsBlur"
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-07 17:43:10 -07:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(downloadProgressDidChange(_:)), name: .DownloadProgressDidChange, object: nil)
|
2017-10-12 21:02:27 -07:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
2017-11-15 13:26:10 -08:00
|
|
|
|
|
2018-01-23 21:49:33 -08:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(batchUpdateDidPerform(_:)), name: .BatchUpdateDidPerform, object: nil)
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
2018-02-24 15:54:32 -08:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(childrenDidChange(_:)), name: .ChildrenDidChange, object: nil)
|
2017-11-25 13:48:14 -08:00
|
|
|
|
|
2019-05-05 03:25:21 -05:00
|
|
|
|
|
2017-09-27 13:29:05 -07:00
|
|
|
|
pullObjectsFromDisk()
|
2017-10-18 18:37:45 -07:00
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
2017-12-03 11:57:53 -08:00
|
|
|
|
self.fetchAllUnreadCounts()
|
2017-10-18 18:37:45 -07:00
|
|
|
|
}
|
2017-12-19 17:48:30 -08:00
|
|
|
|
|
|
|
|
|
self.delegate.accountDidInitialize(self)
|
2018-09-16 20:02:24 -07:00
|
|
|
|
startingUp = false
|
2019-05-02 18:17:52 -05:00
|
|
|
|
|
2017-07-03 10:29:44 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 11:32:58 -07:00
|
|
|
|
// MARK: - API
|
2019-05-02 18:17:52 -05:00
|
|
|
|
|
2019-05-04 15:14:49 -05:00
|
|
|
|
public func storeCredentials(_ credentials: Credentials) throws {
|
2019-05-05 03:25:21 -05:00
|
|
|
|
guard let server = delegate.server else {
|
2019-05-04 15:14:49 -05:00
|
|
|
|
throw CredentialsError.incompleteCredentials
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-05 03:25:21 -05:00
|
|
|
|
switch credentials {
|
|
|
|
|
case .basic(let username, _):
|
|
|
|
|
self.username = username
|
2019-05-04 15:14:49 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-05 03:25:21 -05:00
|
|
|
|
try CredentialsManager.storeCredentials(credentials, server: server)
|
2019-05-04 15:14:49 -05:00
|
|
|
|
|
2019-05-05 06:02:28 -05:00
|
|
|
|
delegate.credentials = credentials
|
|
|
|
|
|
2019-05-04 15:14:49 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-05 03:25:21 -05:00
|
|
|
|
public func retrieveBasicCredentials() throws -> Credentials? {
|
2019-05-04 15:14:49 -05:00
|
|
|
|
guard let username = self.username, let server = delegate.server else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2019-05-05 03:25:21 -05:00
|
|
|
|
return try CredentialsManager.retrieveBasicCredentials(server: server, username: username)
|
2019-05-04 15:14:49 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-05 03:25:21 -05:00
|
|
|
|
public func removeBasicCredentials() throws {
|
2019-05-04 15:14:49 -05:00
|
|
|
|
guard let username = self.username, let server = delegate.server else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-05-05 03:25:21 -05:00
|
|
|
|
try CredentialsManager.removeBasicCredentials(server: server, username: username)
|
2019-05-04 15:14:49 -05:00
|
|
|
|
self.username = nil
|
2019-05-02 18:17:52 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-06 10:53:20 -05:00
|
|
|
|
public static func validateCredentials(transport: Transport = URLSession.webserviceTransport(), type: AccountType, credentials: Credentials, completion: @escaping (Result<Bool, Error>) -> Void) {
|
2019-05-02 18:17:52 -05:00
|
|
|
|
switch type {
|
|
|
|
|
case .onMyMac:
|
2019-05-06 10:53:20 -05:00
|
|
|
|
LocalAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
|
2019-05-02 18:17:52 -05:00
|
|
|
|
case .feedbin:
|
2019-05-06 10:53:20 -05:00
|
|
|
|
FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
|
2019-05-02 18:17:52 -05:00
|
|
|
|
default:
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-17 11:32:58 -07:00
|
|
|
|
|
2019-05-26 11:54:32 -05:00
|
|
|
|
public func refreshAll(completion: @escaping (Result<Void, Error>) -> Void) {
|
2019-06-05 21:17:43 +01:00
|
|
|
|
delegate.sendArticleStatus(for: self) { [unowned self] in
|
|
|
|
|
self.delegate.refreshAll(for: self, completion: completion)
|
|
|
|
|
}
|
2017-07-03 10:29:44 -07:00
|
|
|
|
}
|
2017-09-16 15:30:26 -07:00
|
|
|
|
|
2019-05-15 11:52:56 -05:00
|
|
|
|
public func syncArticleStatus(completion: (() -> Void)? = nil) {
|
|
|
|
|
delegate.sendArticleStatus(for: self) { [unowned self] in
|
|
|
|
|
self.delegate.refreshArticleStatus(for: self) {
|
|
|
|
|
completion?()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-11 12:26:23 -05:00
|
|
|
|
public func importOPML(_ opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
2019-05-17 10:44:22 -05:00
|
|
|
|
|
|
|
|
|
guard !delegate.opmlImportInProgress else {
|
|
|
|
|
completion(.failure(AccountError.opmlImportInProgress))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-12 19:31:29 -05:00
|
|
|
|
delegate.importOPML(for: self, opmlFile: opmlFile) { [weak self] result in
|
|
|
|
|
switch result {
|
|
|
|
|
case .success:
|
|
|
|
|
guard let self = self else { return }
|
2019-05-17 10:04:13 -05:00
|
|
|
|
// Reset the last fetch date to get the article history for the added feeds.
|
|
|
|
|
self.metadata.lastArticleFetch = nil
|
2019-05-26 11:54:32 -05:00
|
|
|
|
self.delegate.refreshAll(for: self, completion: completion)
|
2019-05-12 19:31:29 -05:00
|
|
|
|
case .failure(let error):
|
|
|
|
|
completion(.failure(error))
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-05-17 10:44:22 -05:00
|
|
|
|
|
2017-09-17 12:08:50 -07:00
|
|
|
|
}
|
2019-05-11 12:26:23 -05:00
|
|
|
|
|
2017-10-29 11:14:10 -07:00
|
|
|
|
public func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
2019-05-14 15:34:05 -05:00
|
|
|
|
return delegate.markArticles(for: self, articles: articles, statusKey: statusKey, flag: flag)
|
2017-09-17 16:30:45 -07:00
|
|
|
|
}
|
2017-10-18 19:46:35 -07:00
|
|
|
|
|
|
|
|
|
@discardableResult
|
2017-09-17 13:07:55 -07:00
|
|
|
|
public func ensureFolder(with name: String) -> Folder? {
|
2017-10-18 19:46:35 -07:00
|
|
|
|
|
|
|
|
|
// TODO: support subfolders, maybe, some day
|
|
|
|
|
|
|
|
|
|
if name.isEmpty {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let folder = existingFolder(with: name) {
|
|
|
|
|
return folder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let folder = Folder(account: self, name: name)
|
2018-09-16 17:54:42 -07:00
|
|
|
|
folders!.insert(folder)
|
2018-09-16 20:02:24 -07:00
|
|
|
|
structureDidChange()
|
2017-10-18 19:46:35 -07:00
|
|
|
|
|
2017-10-19 13:27:59 -07:00
|
|
|
|
postChildrenDidChangeNotification()
|
2017-10-18 19:46:35 -07:00
|
|
|
|
return folder
|
2017-09-17 13:07:55 -07:00
|
|
|
|
}
|
2017-09-25 13:31:36 -07:00
|
|
|
|
|
2017-11-04 19:03:47 -07:00
|
|
|
|
public func ensureFolder(withFolderNames folderNames: [String]) -> Folder? {
|
|
|
|
|
|
|
|
|
|
// TODO: support subfolders, maybe, some day.
|
|
|
|
|
// Since we don’t, just take the last name and make sure there’s a Folder.
|
|
|
|
|
|
|
|
|
|
guard let folderName = folderNames.last else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return ensureFolder(with: folderName)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-11 16:07:27 -05:00
|
|
|
|
func newFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> Feed {
|
|
|
|
|
let feedURL = opmlFeedSpecifier.feedURL
|
|
|
|
|
let metadata = feedMetadata(feedURL: feedURL, feedID: feedURL)
|
|
|
|
|
let feed = Feed(account: self, url: opmlFeedSpecifier.feedURL, metadata: metadata)
|
|
|
|
|
if let feedTitle = opmlFeedSpecifier.title {
|
|
|
|
|
if feed.name == nil {
|
|
|
|
|
feed.name = feedTitle
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return feed
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-29 20:47:52 -05:00
|
|
|
|
public func addFeed(_ feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
2019-05-29 21:04:44 -05:00
|
|
|
|
delegate.addFeed(for: self, with: feed, to: container, completion: completion)
|
2018-09-16 17:54:42 -07:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-28 09:45:02 -05:00
|
|
|
|
public func createFeed(url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
|
|
|
|
delegate.createFeed(for: self, url: url, name: name, container: container, completion: completion)
|
2019-05-07 10:51:41 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-08 17:41:19 -05:00
|
|
|
|
func createFeed(with name: String?, url: String, feedID: String, homePageURL: String?) -> Feed {
|
2019-05-07 10:51:41 -05:00
|
|
|
|
|
2019-05-08 09:54:55 -05:00
|
|
|
|
let metadata = feedMetadata(feedURL: url, feedID: feedID)
|
|
|
|
|
let feed = Feed(account: self, url: url, metadata: metadata)
|
2019-05-07 10:51:41 -05:00
|
|
|
|
feed.name = name
|
|
|
|
|
feed.homePageURL = homePageURL
|
|
|
|
|
|
2017-09-30 16:56:48 -07:00
|
|
|
|
return feed
|
2019-05-07 10:51:41 -05:00
|
|
|
|
|
2017-09-30 16:56:48 -07:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-29 21:04:44 -05:00
|
|
|
|
public func removeFeed(_ feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
|
delegate.removeFeed(for: self, with: feed, from: container, completion: completion)
|
2019-05-09 16:09:21 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-30 10:12:34 -05:00
|
|
|
|
public func moveFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
|
delegate.moveFeed(for: self, with: feed, from: from, to: to, completion: completion)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-08 17:41:19 -05:00
|
|
|
|
public func renameFeed(_ feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
|
delegate.renameFeed(for: self, with: feed, to: name, completion: completion)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-28 09:45:02 -05:00
|
|
|
|
public func restoreFeed(_ feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
|
delegate.restoreFeed(for: self, feed: feed, container: container, completion: completion)
|
2019-05-09 16:09:21 -05:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-30 14:36:21 -05:00
|
|
|
|
public func addFolder(_ name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
|
|
|
|
delegate.addFolder(for: self, name: name, completion: completion)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-29 20:53:00 -05:00
|
|
|
|
public func removeFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
|
delegate.removeFolder(for: self, with: folder, completion: completion)
|
2017-09-25 13:31:36 -07:00
|
|
|
|
}
|
2019-05-06 10:53:20 -05:00
|
|
|
|
|
|
|
|
|
public func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
2019-05-06 17:34:41 -05:00
|
|
|
|
delegate.renameFolder(for: self, with: folder, to: name, completion: completion)
|
2019-05-06 10:53:20 -05:00
|
|
|
|
}
|
2017-09-25 13:31:36 -07:00
|
|
|
|
|
2019-05-09 16:09:21 -05:00
|
|
|
|
public func restoreFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
|
delegate.restoreFolder(for: self, folder: folder, completion: completion)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-31 07:47:05 -05:00
|
|
|
|
func clearFeedMetadata(_ feed: Feed) {
|
|
|
|
|
feedMetadata[feed.url] = nil
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-09 16:09:21 -05:00
|
|
|
|
func addFolder(_ folder: Folder) {
|
|
|
|
|
folders!.insert(folder)
|
|
|
|
|
postChildrenDidChangeNotification()
|
|
|
|
|
structureDidChange()
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-11 12:26:23 -05:00
|
|
|
|
func loadOPML(_ opmlDocument: RSOPMLDocument) {
|
2017-10-07 21:41:21 -07:00
|
|
|
|
|
|
|
|
|
guard let children = opmlDocument.children else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-05-11 12:26:23 -05:00
|
|
|
|
loadOPMLItems(children, parentFolder: nil)
|
2018-09-16 17:54:42 -07:00
|
|
|
|
structureDidChange()
|
2017-12-03 12:54:51 -08:00
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
2019-05-26 11:54:32 -05:00
|
|
|
|
self.refreshAll() { result in }
|
2017-12-03 12:54:51 -08:00
|
|
|
|
}
|
2019-05-26 11:54:32 -05:00
|
|
|
|
|
2017-09-18 22:00:35 -07:00
|
|
|
|
}
|
2017-10-07 17:20:19 -07:00
|
|
|
|
|
2017-10-08 01:54:37 -07:00
|
|
|
|
public func updateUnreadCounts(for feeds: Set<Feed>) {
|
|
|
|
|
|
2017-10-18 21:53:45 -07:00
|
|
|
|
if feeds.isEmpty {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2018-07-28 12:16:14 -07:00
|
|
|
|
database.fetchUnreadCounts(for: feeds.feedIDs()) { (unreadCountDictionary) in
|
2017-10-08 01:54:37 -07:00
|
|
|
|
|
|
|
|
|
for feed in feeds {
|
2018-07-28 12:16:14 -07:00
|
|
|
|
if let unreadCount = unreadCountDictionary[feed.feedID] {
|
2017-10-08 01:54:37 -07:00
|
|
|
|
feed.unreadCount = unreadCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-14 06:20:53 -05:00
|
|
|
|
public func fetchArticles(forArticleIDs articleIDs: Set<String>) -> Set<Article> {
|
|
|
|
|
return database.fetchArticles(forArticleIDs: articleIDs)
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-08 18:58:15 -07:00
|
|
|
|
public func fetchArticles(for feed: Feed) -> Set<Article> {
|
2017-12-26 11:27:55 -08:00
|
|
|
|
|
2018-07-28 12:16:14 -07:00
|
|
|
|
let articles = database.fetchArticles(for: feed.feedID)
|
2017-12-26 11:27:55 -08:00
|
|
|
|
validateUnreadCount(feed, articles)
|
|
|
|
|
return articles
|
2017-10-08 18:58:15 -07:00
|
|
|
|
}
|
2018-02-04 10:57:41 -08:00
|
|
|
|
|
|
|
|
|
public func fetchUnreadArticles(for feed: Feed) -> Set<Article> {
|
|
|
|
|
|
2018-07-28 12:16:14 -07:00
|
|
|
|
let articles = database.fetchUnreadArticles(for: Set([feed.feedID]))
|
2018-02-04 10:57:41 -08:00
|
|
|
|
validateUnreadCount(feed, articles)
|
|
|
|
|
return articles
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-10 13:00:16 -08:00
|
|
|
|
public func fetchUnreadArticles() -> Set<Article> {
|
|
|
|
|
|
|
|
|
|
return fetchUnreadArticles(forContainer: self)
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-08 18:58:15 -07:00
|
|
|
|
public func fetchArticles(folder: Folder) -> Set<Article> {
|
2017-12-26 11:27:55 -08:00
|
|
|
|
|
2018-02-10 13:00:16 -08:00
|
|
|
|
return fetchUnreadArticles(forContainer: folder)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func fetchUnreadArticles(forContainer container: Container) -> Set<Article> {
|
|
|
|
|
|
|
|
|
|
let feeds = container.flattenedFeeds()
|
2018-07-28 12:16:14 -07:00
|
|
|
|
let articles = database.fetchUnreadArticles(for: feeds.feedIDs())
|
2018-09-10 22:08:38 -07:00
|
|
|
|
|
|
|
|
|
// Validate unread counts. This was the site of a performance slowdown:
|
|
|
|
|
// it was calling going through the entire list of articles once per feed:
|
|
|
|
|
// feeds.forEach { validateUnreadCount($0, articles) }
|
|
|
|
|
// Now we loop through articles exactly once. This makes a huge difference.
|
|
|
|
|
|
|
|
|
|
var unreadCountStorage = [String: Int]() // [FeedID: Int]
|
|
|
|
|
articles.forEach { (article) in
|
|
|
|
|
precondition(!article.status.read)
|
|
|
|
|
unreadCountStorage[article.feedID, default: 0] += 1
|
|
|
|
|
}
|
|
|
|
|
feeds.forEach { (feed) in
|
|
|
|
|
let unreadCount = unreadCountStorage[feed.feedID, default: 0]
|
|
|
|
|
feed.unreadCount = unreadCount
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-26 11:27:55 -08:00
|
|
|
|
return articles
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-10 17:37:47 -08:00
|
|
|
|
public func fetchTodayArticles() -> Set<Article> {
|
|
|
|
|
|
2018-07-28 12:16:14 -07:00
|
|
|
|
return database.fetchTodayArticles(for: flattenedFeeds().feedIDs())
|
2018-02-10 17:37:47 -08:00
|
|
|
|
}
|
|
|
|
|
|
2018-02-11 12:07:55 -08:00
|
|
|
|
public func fetchStarredArticles() -> Set<Article> {
|
|
|
|
|
|
2018-07-28 12:16:14 -07:00
|
|
|
|
return database.fetchStarredArticles(for: flattenedFeeds().feedIDs())
|
2018-02-11 12:07:55 -08:00
|
|
|
|
}
|
|
|
|
|
|
2019-02-24 19:22:16 -08:00
|
|
|
|
public func fetchArticlesMatching(_ searchString: String) -> Set<Article> {
|
|
|
|
|
return database.fetchArticlesMatching(searchString, for: flattenedFeeds().feedIDs())
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-26 11:27:55 -08:00
|
|
|
|
private func validateUnreadCount(_ feed: Feed, _ articles: Set<Article>) {
|
|
|
|
|
|
|
|
|
|
// articles must contain all the unread articles for the feed.
|
|
|
|
|
// The unread number should match the feed’s unread count.
|
|
|
|
|
|
|
|
|
|
let feedUnreadCount = articles.reduce(0) { (result, article) -> Int in
|
|
|
|
|
if article.feed == feed && !article.status.read {
|
|
|
|
|
return result + 1
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-29 10:13:11 -08:00
|
|
|
|
feed.unreadCount = feedUnreadCount
|
2017-10-08 18:58:15 -07:00
|
|
|
|
}
|
2017-11-19 12:12:43 -08:00
|
|
|
|
|
2017-11-19 12:45:35 -08:00
|
|
|
|
public func fetchUnreadCountForToday(_ callback: @escaping (Int) -> Void) {
|
2017-11-19 12:12:43 -08:00
|
|
|
|
|
|
|
|
|
let startOfToday = NSCalendar.startOfToday()
|
2018-07-28 12:16:14 -07:00
|
|
|
|
database.fetchUnreadCount(for: flattenedFeeds().feedIDs(), since: startOfToday, callback: callback)
|
2017-11-19 12:12:43 -08:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-19 15:40:02 -08:00
|
|
|
|
public func fetchUnreadCountForStarredArticles(_ callback: @escaping (Int) -> Void) {
|
|
|
|
|
|
2018-07-28 12:16:14 -07:00
|
|
|
|
database.fetchStarredAndUnreadCount(for: flattenedFeeds().feedIDs(), callback: callback)
|
2017-11-19 15:40:02 -08:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-14 06:20:53 -05:00
|
|
|
|
public func fetchUnreadArticleIDs() -> Set<String> {
|
|
|
|
|
return database.fetchUnreadArticleIDs()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func fetchStarredArticleIDs() -> Set<String> {
|
|
|
|
|
return database.fetchStarredArticleIDs()
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-17 14:56:27 -05:00
|
|
|
|
public func fetchArticleIDsForStatusesWithoutArticles() -> Set<String> {
|
|
|
|
|
return database.fetchArticleIDsForStatusesWithoutArticles()
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-14 13:25:38 -07:00
|
|
|
|
public func opmlDocument() -> String {
|
|
|
|
|
let escapedTitle = nameForDisplay.rs_stringByEscapingSpecialXMLCharacters()
|
|
|
|
|
let openingText =
|
|
|
|
|
"""
|
|
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<!-- OPML generated by NetNewsWire -->
|
|
|
|
|
<opml version="1.1">
|
|
|
|
|
<head>
|
|
|
|
|
<title>\(escapedTitle)</title>
|
|
|
|
|
</head>
|
|
|
|
|
<body>
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
let middleText = OPMLString(indentLevel: 0)
|
|
|
|
|
|
|
|
|
|
let closingText =
|
|
|
|
|
"""
|
|
|
|
|
</body>
|
|
|
|
|
</opml>
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
let opml = openingText + middleText + closingText
|
|
|
|
|
return opml
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-14 22:06:03 -07:00
|
|
|
|
public func unreadCount(for feed: Feed) -> Int {
|
|
|
|
|
return unreadCounts[feed.feedID] ?? 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func setUnreadCount(_ unreadCount: Int, for feed: Feed) {
|
|
|
|
|
unreadCounts[feed.feedID] = unreadCount
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-16 17:54:42 -07:00
|
|
|
|
public func structureDidChange() {
|
|
|
|
|
// Feeds were added or deleted. Or folders added or deleted.
|
|
|
|
|
// Or feeds inside folders were added or deleted.
|
2018-09-16 20:02:24 -07:00
|
|
|
|
if !startingUp {
|
|
|
|
|
dirty = true
|
|
|
|
|
}
|
2018-09-16 17:54:42 -07:00
|
|
|
|
flattenedFeedsNeedUpdate = true
|
|
|
|
|
feedDictionaryNeedsUpdate = true
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-11 12:26:23 -05:00
|
|
|
|
func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping (() -> Void)) {
|
|
|
|
|
feed.takeSettings(from: parsedFeed)
|
2019-05-12 18:32:32 -05:00
|
|
|
|
update(feed, parsedItems: parsedFeed.items, completion)
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-13 08:32:03 -05:00
|
|
|
|
func update(_ feed: Feed, parsedItems: Set<ParsedItem>, defaultRead: Bool = false, _ completion: @escaping (() -> Void)) {
|
2019-05-11 12:26:23 -05:00
|
|
|
|
|
2019-05-13 08:32:03 -05:00
|
|
|
|
database.update(feedID: feed.feedID, parsedItems: parsedItems, defaultRead: defaultRead) { (newArticles, updatedArticles) in
|
2019-05-11 12:26:23 -05:00
|
|
|
|
|
|
|
|
|
var userInfo = [String: Any]()
|
|
|
|
|
if let newArticles = newArticles, !newArticles.isEmpty {
|
|
|
|
|
self.updateUnreadCounts(for: Set([feed]))
|
|
|
|
|
userInfo[UserInfoKey.newArticles] = newArticles
|
|
|
|
|
}
|
|
|
|
|
if let updatedArticles = updatedArticles, !updatedArticles.isEmpty {
|
|
|
|
|
userInfo[UserInfoKey.updatedArticles] = updatedArticles
|
|
|
|
|
}
|
|
|
|
|
userInfo[UserInfoKey.feeds] = Set([feed])
|
|
|
|
|
|
|
|
|
|
completion()
|
|
|
|
|
|
|
|
|
|
NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo)
|
|
|
|
|
}
|
2019-05-12 18:32:32 -05:00
|
|
|
|
|
2019-05-11 12:26:23 -05:00
|
|
|
|
}
|
2019-05-12 18:32:32 -05:00
|
|
|
|
|
2019-05-14 15:34:05 -05:00
|
|
|
|
func update(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
|
|
|
|
|
|
|
|
|
// Returns set of Articles whose statuses did change.
|
|
|
|
|
|
|
|
|
|
guard let updatedStatuses = database.mark(articles, statusKey: statusKey, flag: flag) else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let updatedArticleIDs = updatedStatuses.articleIDs()
|
|
|
|
|
let updatedArticles = Set(articles.filter{ updatedArticleIDs.contains($0.articleID) })
|
|
|
|
|
|
|
|
|
|
noteStatusesForArticlesDidChange(updatedArticles)
|
|
|
|
|
return updatedArticles
|
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-14 10:06:29 -05:00
|
|
|
|
func ensureStatuses(_ articleIDs: Set<String>, _ statusKey: ArticleStatus.Key, _ flag: Bool) {
|
|
|
|
|
database.ensureStatuses(articleIDs, statusKey, flag)
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-16 17:54:42 -07:00
|
|
|
|
// MARK: - Container
|
|
|
|
|
|
|
|
|
|
public func flattenedFeeds() -> Set<Feed> {
|
|
|
|
|
if flattenedFeedsNeedUpdate {
|
|
|
|
|
updateFlattenedFeeds()
|
|
|
|
|
}
|
|
|
|
|
return _flattenedFeeds
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-29 20:47:52 -05:00
|
|
|
|
public func removeFeed(_ feed: Feed) {
|
2019-05-09 07:25:45 -05:00
|
|
|
|
topLevelFeeds.remove(feed)
|
2019-05-07 17:41:32 -05:00
|
|
|
|
structureDidChange()
|
|
|
|
|
postChildrenDidChangeNotification()
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-29 20:47:52 -05:00
|
|
|
|
public func addFeed(_ feed: Feed) {
|
2019-05-09 13:31:18 -05:00
|
|
|
|
topLevelFeeds.insert(feed)
|
2018-09-16 17:54:42 -07:00
|
|
|
|
structureDidChange()
|
|
|
|
|
postChildrenDidChangeNotification()
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-28 13:38:40 -05:00
|
|
|
|
func addFeedIfNotInAnyFolder(_ feed: Feed) {
|
|
|
|
|
if !flattenedFeeds().contains(feed) {
|
|
|
|
|
addFeed(feed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-29 20:53:00 -05:00
|
|
|
|
func removeFolder(_ folder: Folder) {
|
2018-09-16 17:54:42 -07:00
|
|
|
|
folders?.remove(folder)
|
|
|
|
|
structureDidChange()
|
|
|
|
|
postChildrenDidChangeNotification()
|
|
|
|
|
}
|
2019-05-06 17:34:41 -05:00
|
|
|
|
|
2017-11-25 11:13:15 -08:00
|
|
|
|
// MARK: - Debug
|
|
|
|
|
|
|
|
|
|
public func debugDropConditionalGetInfo() {
|
|
|
|
|
|
|
|
|
|
#if DEBUG
|
|
|
|
|
flattenedFeeds().forEach{ $0.debugDropConditionalGetInfo() }
|
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2019-02-18 22:29:43 -08:00
|
|
|
|
public func debugRunSearch() {
|
|
|
|
|
#if DEBUG
|
2019-02-24 19:22:16 -08:00
|
|
|
|
let t1 = Date()
|
|
|
|
|
let articles = fetchArticlesMatching("Brent NetNewsWire")
|
|
|
|
|
let t2 = Date()
|
|
|
|
|
print(t2.timeIntervalSince(t1))
|
|
|
|
|
print(articles.count)
|
2019-02-18 22:29:43 -08:00
|
|
|
|
#endif
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-07 17:43:10 -07:00
|
|
|
|
// MARK: - Notifications
|
|
|
|
|
|
|
|
|
|
@objc func downloadProgressDidChange(_ note: Notification) {
|
2017-10-07 17:20:19 -07:00
|
|
|
|
|
2017-10-07 17:43:10 -07:00
|
|
|
|
guard let noteObject = note.object as? DownloadProgress, noteObject === refreshProgress else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-10-07 17:20:19 -07:00
|
|
|
|
|
|
|
|
|
refreshInProgress = refreshProgress.numberRemaining > 0
|
|
|
|
|
NotificationCenter.default.post(name: .AccountRefreshProgressDidChange, object: self)
|
|
|
|
|
}
|
2017-10-12 21:02:27 -07:00
|
|
|
|
|
|
|
|
|
@objc func unreadCountDidChange(_ note: Notification) {
|
2018-09-16 19:07:07 -07:00
|
|
|
|
if let feed = note.object as? Feed, feed.account === self {
|
2018-09-13 22:25:10 -07:00
|
|
|
|
updateUnreadCount()
|
2017-10-13 06:50:33 -07:00
|
|
|
|
}
|
2017-10-12 21:02:27 -07:00
|
|
|
|
}
|
2017-11-15 13:26:10 -08:00
|
|
|
|
|
|
|
|
|
@objc func batchUpdateDidPerform(_ note: Notification) {
|
2017-11-25 18:05:20 -08:00
|
|
|
|
|
2018-09-16 17:54:42 -07:00
|
|
|
|
flattenedFeedsNeedUpdate = true
|
2017-11-25 18:05:20 -08:00
|
|
|
|
rebuildFeedDictionaries()
|
2017-11-15 13:26:10 -08:00
|
|
|
|
updateUnreadCount()
|
|
|
|
|
}
|
2017-10-07 17:20:19 -07:00
|
|
|
|
|
2018-02-24 15:54:32 -08:00
|
|
|
|
@objc func childrenDidChange(_ note: Notification) {
|
|
|
|
|
|
|
|
|
|
guard let object = note.object else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if let account = object as? Account, account === self {
|
2018-09-16 17:54:42 -07:00
|
|
|
|
structureDidChange()
|
2019-05-16 09:54:19 -05:00
|
|
|
|
updateUnreadCount()
|
2018-02-24 15:54:32 -08:00
|
|
|
|
}
|
|
|
|
|
if let folder = object as? Folder, folder.account === self {
|
2018-09-16 17:54:42 -07:00
|
|
|
|
structureDidChange()
|
2018-02-24 15:54:32 -08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-23 21:49:33 -08:00
|
|
|
|
@objc func displayNameDidChange(_ note: Notification) {
|
|
|
|
|
|
2018-02-24 15:54:32 -08:00
|
|
|
|
if let folder = note.object as? Folder, folder.account === self {
|
2018-09-16 17:54:42 -07:00
|
|
|
|
structureDidChange()
|
2018-01-23 21:49:33 -08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-17 15:38:54 -08:00
|
|
|
|
@objc func saveToDiskIfNeeded() {
|
|
|
|
|
|
2019-05-31 08:05:26 -05:00
|
|
|
|
if dirty && !isDeleted {
|
2018-02-24 15:54:32 -08:00
|
|
|
|
saveToDisk()
|
2018-02-17 15:38:54 -08:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-05 07:49:59 -05:00
|
|
|
|
@objc func saveFeedMetadataIfNeeded() {
|
2019-05-31 08:05:26 -05:00
|
|
|
|
if feedMetadataDirty && !isDeleted {
|
2019-03-13 23:41:43 -07:00
|
|
|
|
saveFeedMetadata()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-05 07:49:59 -05:00
|
|
|
|
@objc func saveAccountMetadataIfNeeded() {
|
2019-05-31 08:05:26 -05:00
|
|
|
|
if metadataDirty && !isDeleted {
|
2019-05-05 07:49:59 -05:00
|
|
|
|
saveAccountMetadata()
|
2019-03-27 22:10:14 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-08-25 11:54:58 -07:00
|
|
|
|
// MARK: - Hashable
|
|
|
|
|
|
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
|
|
|
hasher.combine(accountID)
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 11:32:58 -07:00
|
|
|
|
// MARK: - Equatable
|
2017-09-16 15:30:26 -07:00
|
|
|
|
|
2017-09-17 11:32:58 -07:00
|
|
|
|
public class func ==(lhs: Account, rhs: Account) -> Bool {
|
2017-09-16 15:30:26 -07:00
|
|
|
|
|
2017-09-17 11:32:58 -07:00
|
|
|
|
return lhs === rhs
|
2017-09-16 15:30:26 -07:00
|
|
|
|
}
|
2017-07-03 10:29:44 -07:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-05 07:49:59 -05:00
|
|
|
|
// MARK: - AccountMetadataDelegate
|
2019-05-05 07:21:26 -05:00
|
|
|
|
|
2019-05-05 07:49:59 -05:00
|
|
|
|
extension Account: AccountMetadataDelegate {
|
|
|
|
|
func valueDidChange(_ accountMetadata: AccountMetadata, key: AccountMetadata.CodingKeys) {
|
|
|
|
|
metadataDirty = true
|
2019-05-05 07:21:26 -05:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-07-03 10:29:44 -07:00
|
|
|
|
|
2019-03-13 23:41:43 -07:00
|
|
|
|
// MARK: - FeedMetadataDelegate
|
|
|
|
|
|
|
|
|
|
extension Account: FeedMetadataDelegate {
|
|
|
|
|
|
|
|
|
|
func valueDidChange(_ feedMetadata: FeedMetadata, key: FeedMetadata.CodingKeys) {
|
|
|
|
|
feedMetadataDirty = true
|
2019-03-17 12:47:04 -07:00
|
|
|
|
guard let feed = existingFeed(with: feedMetadata.feedID) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
feed.postFeedSettingDidChangeNotification(key)
|
2019-03-13 23:41:43 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-07 20:11:17 -07:00
|
|
|
|
// MARK: - Disk (Private)
|
2017-09-27 13:29:05 -07:00
|
|
|
|
|
2017-10-07 20:11:17 -07:00
|
|
|
|
private extension Account {
|
|
|
|
|
|
2018-02-17 18:02:40 -08:00
|
|
|
|
func queueSaveToDiskIfNeeded() {
|
|
|
|
|
Account.saveQueue.add(self, #selector(saveToDiskIfNeeded))
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-07 20:11:17 -07:00
|
|
|
|
func pullObjectsFromDisk() {
|
2019-05-11 12:26:23 -05:00
|
|
|
|
loadAccountMetadata()
|
|
|
|
|
loadFeedMetadata()
|
|
|
|
|
loadOPMLFile(path: opmlFilePath)
|
2017-10-07 18:15:42 -07:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-11 12:26:23 -05:00
|
|
|
|
func loadAccountMetadata() {
|
2019-05-05 07:49:59 -05:00
|
|
|
|
let url = URL(fileURLWithPath: metadataPath)
|
2019-03-20 22:10:22 -07:00
|
|
|
|
guard let data = try? Data(contentsOf: url) else {
|
2019-05-05 07:49:59 -05:00
|
|
|
|
metadata.delegate = self
|
2019-03-20 22:10:22 -07:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let decoder = PropertyListDecoder()
|
2019-05-05 07:49:59 -05:00
|
|
|
|
metadata = (try? decoder.decode(AccountMetadata.self, from: data)) ?? AccountMetadata()
|
|
|
|
|
metadata.delegate = self
|
2019-03-20 22:10:22 -07:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-11 12:26:23 -05:00
|
|
|
|
func loadFeedMetadata() {
|
2019-03-13 23:41:43 -07:00
|
|
|
|
let url = URL(fileURLWithPath: feedMetadataPath)
|
|
|
|
|
guard let data = try? Data(contentsOf: url) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
let decoder = PropertyListDecoder()
|
|
|
|
|
feedMetadata = (try? decoder.decode(FeedMetadataDictionary.self, from: data)) ?? FeedMetadataDictionary()
|
|
|
|
|
feedMetadata.values.forEach { $0.delegate = self }
|
|
|
|
|
}
|
|
|
|
|
|
2019-05-11 12:26:23 -05:00
|
|
|
|
func loadOPMLFile(path: String) {
|
2018-09-16 12:42:46 -07:00
|
|
|
|
let opmlFileURL = URL(fileURLWithPath: path)
|
|
|
|
|
var fileData: Data?
|
|
|
|
|
do {
|
|
|
|
|
fileData = try Data(contentsOf: opmlFileURL)
|
|
|
|
|
} catch {
|
2018-09-26 22:05:35 -07:00
|
|
|
|
// Commented out because it’s not an error on first run.
|
|
|
|
|
// TODO: make it so we know if it’s first run or not.
|
|
|
|
|
//NSApplication.shared.presentError(error)
|
2018-09-16 12:42:46 -07:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
guard let opmlData = fileData else {
|
|
|
|
|
return
|
2017-10-07 18:15:42 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-16 12:42:46 -07:00
|
|
|
|
let parserData = ParserData(url: opmlFileURL.absoluteString, data: opmlData)
|
|
|
|
|
var opmlDocument: RSOPMLDocument?
|
2017-10-18 19:14:40 -07:00
|
|
|
|
|
2018-09-16 12:42:46 -07:00
|
|
|
|
do {
|
|
|
|
|
opmlDocument = try RSOPMLParser.parseOPML(with: parserData)
|
|
|
|
|
} catch {
|
2019-04-15 13:30:10 -05:00
|
|
|
|
#if os(macOS)
|
2018-09-16 12:42:46 -07:00
|
|
|
|
NSApplication.shared.presentError(error)
|
2019-04-15 13:30:10 -05:00
|
|
|
|
#else
|
|
|
|
|
UIApplication.shared.presentError(error)
|
|
|
|
|
#endif
|
2018-09-16 12:42:46 -07:00
|
|
|
|
return
|
|
|
|
|
}
|
2018-09-16 17:54:42 -07:00
|
|
|
|
guard let parsedOPML = opmlDocument, let children = parsedOPML.children else {
|
2018-09-16 12:42:46 -07:00
|
|
|
|
return
|
2017-10-18 19:14:40 -07:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-16 17:54:42 -07:00
|
|
|
|
BatchUpdate.shared.perform {
|
2019-05-11 12:26:23 -05:00
|
|
|
|
loadOPMLItems(children, parentFolder: nil)
|
2018-09-16 17:54:42 -07:00
|
|
|
|
}
|
2019-05-08 09:54:55 -05:00
|
|
|
|
|
2017-10-07 18:15:42 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func saveToDisk() {
|
|
|
|
|
|
2018-09-14 13:25:38 -07:00
|
|
|
|
dirty = false
|
|
|
|
|
|
|
|
|
|
let opmlDocumentString = opmlDocument()
|
|
|
|
|
do {
|
|
|
|
|
let url = URL(fileURLWithPath: opmlFilePath)
|
|
|
|
|
try opmlDocumentString.write(to: url, atomically: true, encoding: .utf8)
|
|
|
|
|
}
|
|
|
|
|
catch let error as NSError {
|
2019-04-15 13:30:10 -05:00
|
|
|
|
#if os(macOS)
|
2018-09-14 13:25:38 -07:00
|
|
|
|
NSApplication.shared.presentError(error)
|
2019-04-15 13:30:10 -05:00
|
|
|
|
#else
|
|
|
|
|
UIApplication.shared.presentError(error)
|
|
|
|
|
#endif
|
2018-09-14 13:25:38 -07:00
|
|
|
|
}
|
2017-10-07 20:11:17 -07:00
|
|
|
|
}
|
2019-03-13 23:41:43 -07:00
|
|
|
|
|
2019-05-05 07:49:59 -05:00
|
|
|
|
func queueSaveFeedMetadataIfNeeded() {
|
|
|
|
|
Account.saveQueue.add(self, #selector(saveFeedMetadataIfNeeded))
|
2019-03-13 23:41:43 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func metadataForOnlySubscribedToFeeds() -> FeedMetadataDictionary {
|
|
|
|
|
let feedIDs = idToFeedDictionary.keys
|
|
|
|
|
return feedMetadata.filter { (feedID: String, metadata: FeedMetadata) -> Bool in
|
2019-05-08 09:54:55 -05:00
|
|
|
|
return feedIDs.contains(metadata.feedID)
|
2019-03-13 23:41:43 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func saveFeedMetadata() {
|
|
|
|
|
feedMetadataDirty = false
|
|
|
|
|
|
|
|
|
|
let d = metadataForOnlySubscribedToFeeds()
|
|
|
|
|
let encoder = PropertyListEncoder()
|
|
|
|
|
encoder.outputFormat = .binary
|
|
|
|
|
let url = URL(fileURLWithPath: feedMetadataPath)
|
|
|
|
|
do {
|
|
|
|
|
let data = try encoder.encode(d)
|
|
|
|
|
try data.write(to: url)
|
|
|
|
|
}
|
|
|
|
|
catch {
|
|
|
|
|
assertionFailure(error.localizedDescription)
|
|
|
|
|
}
|
|
|
|
|
}
|
2019-03-27 22:10:14 -07:00
|
|
|
|
|
2019-05-05 07:49:59 -05:00
|
|
|
|
func queueSaveAccountMetadatafNeeded() {
|
|
|
|
|
Account.saveQueue.add(self, #selector(saveAccountMetadataIfNeeded))
|
2019-03-27 22:10:14 -07:00
|
|
|
|
}
|
|
|
|
|
|
2019-05-05 07:49:59 -05:00
|
|
|
|
func saveAccountMetadata() {
|
|
|
|
|
metadataDirty = false
|
2019-03-27 22:10:14 -07:00
|
|
|
|
|
|
|
|
|
let encoder = PropertyListEncoder()
|
|
|
|
|
encoder.outputFormat = .binary
|
2019-05-05 07:49:59 -05:00
|
|
|
|
let url = URL(fileURLWithPath: metadataPath)
|
2019-03-27 22:10:14 -07:00
|
|
|
|
do {
|
2019-05-05 07:49:59 -05:00
|
|
|
|
let data = try encoder.encode(metadata)
|
2019-03-27 22:10:14 -07:00
|
|
|
|
try data.write(to: url)
|
|
|
|
|
}
|
|
|
|
|
catch {
|
|
|
|
|
assertionFailure(error.localizedDescription)
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-27 13:29:05 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-01 10:59:35 -07:00
|
|
|
|
// MARK: - Private
|
2017-09-27 13:29:05 -07:00
|
|
|
|
|
|
|
|
|
private extension Account {
|
|
|
|
|
|
2019-05-08 09:54:55 -05:00
|
|
|
|
func feedMetadata(feedURL: String, feedID: String) -> FeedMetadata {
|
|
|
|
|
if let d = feedMetadata[feedURL] {
|
2019-03-16 17:30:30 -07:00
|
|
|
|
assert(d.delegate === self)
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
let d = FeedMetadata(feedID: feedID)
|
|
|
|
|
d.delegate = self
|
2019-05-08 09:54:55 -05:00
|
|
|
|
feedMetadata[feedURL] = d
|
2019-03-16 17:30:30 -07:00
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-16 17:54:42 -07:00
|
|
|
|
func updateFlattenedFeeds() {
|
|
|
|
|
var feeds = Set<Feed>()
|
|
|
|
|
feeds.formUnion(topLevelFeeds)
|
|
|
|
|
for folder in folders! {
|
|
|
|
|
feeds.formUnion(folder.flattenedFeeds())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
_flattenedFeeds = feeds
|
|
|
|
|
flattenedFeedsNeedUpdate = false
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-22 11:08:51 -07:00
|
|
|
|
func rebuildFeedDictionaries() {
|
|
|
|
|
var idDictionary = [String: Feed]()
|
|
|
|
|
|
|
|
|
|
flattenedFeeds().forEach { (feed) in
|
|
|
|
|
idDictionary[feed.feedID] = feed
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-16 17:54:42 -07:00
|
|
|
|
_idToFeedDictionary = idDictionary
|
|
|
|
|
feedDictionaryNeedsUpdate = false
|
2017-10-01 10:59:35 -07:00
|
|
|
|
}
|
2017-10-07 21:41:21 -07:00
|
|
|
|
|
2019-05-11 12:26:23 -05:00
|
|
|
|
func loadOPMLItems(_ items: [RSOPMLItem], parentFolder: Folder?) {
|
2017-10-07 21:41:21 -07:00
|
|
|
|
|
2018-09-16 17:54:42 -07:00
|
|
|
|
var feedsToAdd = Set<Feed>()
|
|
|
|
|
|
2017-10-21 21:00:21 -07:00
|
|
|
|
items.forEach { (item) in
|
2017-10-07 21:41:21 -07:00
|
|
|
|
|
|
|
|
|
if let feedSpecifier = item.feedSpecifier {
|
2019-05-11 16:07:27 -05:00
|
|
|
|
let feed = newFeed(with: feedSpecifier)
|
2018-09-16 17:54:42 -07:00
|
|
|
|
feedsToAdd.insert(feed)
|
2017-10-21 21:00:21 -07:00
|
|
|
|
return
|
|
|
|
|
}
|
2017-10-07 21:41:21 -07:00
|
|
|
|
|
2017-10-21 21:00:21 -07:00
|
|
|
|
guard let folderName = item.titleFromAttributes else {
|
|
|
|
|
// Folder doesn’t have a name, so it won’t be created, and its items will go one level up.
|
2019-01-27 21:42:58 -08:00
|
|
|
|
if let itemChildren = item.children {
|
2019-05-11 12:26:23 -05:00
|
|
|
|
loadOPMLItems(itemChildren, parentFolder: parentFolder)
|
2019-01-27 21:42:58 -08:00
|
|
|
|
}
|
2017-10-21 21:00:21 -07:00
|
|
|
|
return
|
2017-10-07 21:41:21 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-21 21:00:21 -07:00
|
|
|
|
if let folder = ensureFolder(with: folderName) {
|
2019-01-27 21:42:58 -08:00
|
|
|
|
if let itemChildren = item.children {
|
2019-05-11 12:26:23 -05:00
|
|
|
|
loadOPMLItems(itemChildren, parentFolder: folder)
|
2019-01-27 21:42:58 -08:00
|
|
|
|
}
|
2017-10-07 21:41:21 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-09-16 17:54:42 -07:00
|
|
|
|
|
2019-05-09 13:31:18 -05:00
|
|
|
|
if let parentFolder = parentFolder {
|
|
|
|
|
for feed in feedsToAdd {
|
|
|
|
|
parentFolder.addFeed(feed)
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
for feed in feedsToAdd {
|
|
|
|
|
addFeed(feed)
|
|
|
|
|
}
|
2018-09-16 17:54:42 -07:00
|
|
|
|
}
|
2019-05-09 13:31:18 -05:00
|
|
|
|
|
2017-10-07 21:41:21 -07:00
|
|
|
|
}
|
2017-10-10 13:23:12 -07:00
|
|
|
|
|
|
|
|
|
func updateUnreadCount() {
|
2018-09-16 17:54:42 -07:00
|
|
|
|
if fetchingAllUnreadCounts {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var updatedUnreadCount = 0
|
|
|
|
|
for feed in flattenedFeeds() {
|
|
|
|
|
updatedUnreadCount += feed.unreadCount
|
|
|
|
|
}
|
|
|
|
|
unreadCount = updatedUnreadCount
|
2017-10-10 13:23:12 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-12 21:02:27 -07:00
|
|
|
|
func noteStatusesForArticlesDidChange(_ articles: Set<Article>) {
|
2017-10-10 13:23:12 -07:00
|
|
|
|
|
2018-01-27 21:50:48 -05:00
|
|
|
|
let feeds = Set(articles.compactMap { $0.feed })
|
2017-10-12 21:02:27 -07:00
|
|
|
|
let statuses = Set(articles.map { $0.status })
|
2017-10-10 13:23:12 -07:00
|
|
|
|
|
|
|
|
|
// .UnreadCountDidChange notification will get sent to Folder and Account objects,
|
|
|
|
|
// which will update their own unread counts.
|
|
|
|
|
updateUnreadCounts(for: feeds)
|
|
|
|
|
|
|
|
|
|
NotificationCenter.default.post(name: .StatusesDidChange, object: self, userInfo: [UserInfoKey.statuses: statuses, UserInfoKey.articles: articles, UserInfoKey.feeds: feeds])
|
|
|
|
|
}
|
2017-12-03 11:57:53 -08:00
|
|
|
|
|
|
|
|
|
func fetchAllUnreadCounts() {
|
|
|
|
|
|
2018-09-16 17:54:42 -07:00
|
|
|
|
fetchingAllUnreadCounts = true
|
2017-12-03 11:57:53 -08:00
|
|
|
|
database.fetchAllNonZeroUnreadCounts { (unreadCountDictionary) in
|
|
|
|
|
|
|
|
|
|
if unreadCountDictionary.isEmpty {
|
2018-09-26 22:05:35 -07:00
|
|
|
|
self.fetchingAllUnreadCounts = false
|
|
|
|
|
self.updateUnreadCount()
|
2019-04-27 16:16:46 -05:00
|
|
|
|
self.isUnreadCountsInitialized = true
|
2017-12-03 11:57:53 -08:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.flattenedFeeds().forEach{ (feed) in
|
|
|
|
|
|
|
|
|
|
// When the unread count is zero, it won’t appear in unreadCountDictionary.
|
|
|
|
|
|
2018-07-28 12:16:14 -07:00
|
|
|
|
if let unreadCount = unreadCountDictionary[feed.feedID] {
|
2017-12-03 11:57:53 -08:00
|
|
|
|
feed.unreadCount = unreadCount
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
feed.unreadCount = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-09-16 17:54:42 -07:00
|
|
|
|
self.fetchingAllUnreadCounts = false
|
2018-09-13 22:25:10 -07:00
|
|
|
|
self.updateUnreadCount()
|
2019-04-27 16:16:46 -05:00
|
|
|
|
self.isUnreadCountsInitialized = true
|
2017-12-03 11:57:53 -08:00
|
|
|
|
}
|
|
|
|
|
}
|
2017-07-03 10:29:44 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-22 11:08:51 -07:00
|
|
|
|
// MARK: - Container Overrides
|
|
|
|
|
|
|
|
|
|
extension Account {
|
|
|
|
|
|
|
|
|
|
public func existingFeed(with feedID: String) -> Feed? {
|
|
|
|
|
|
|
|
|
|
return idToFeedDictionary[feedID]
|
|
|
|
|
}
|
2018-09-16 17:54:42 -07:00
|
|
|
|
|
2017-10-22 11:08:51 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-27 13:29:05 -07:00
|
|
|
|
// MARK: - OPMLRepresentable
|
|
|
|
|
|
2017-09-16 15:25:38 -07:00
|
|
|
|
extension Account: OPMLRepresentable {
|
|
|
|
|
|
|
|
|
|
public func OPMLString(indentLevel: Int) -> String {
|
|
|
|
|
|
|
|
|
|
var s = ""
|
2018-09-16 17:54:42 -07:00
|
|
|
|
for feed in topLevelFeeds {
|
|
|
|
|
s += feed.OPMLString(indentLevel: indentLevel + 1)
|
|
|
|
|
}
|
|
|
|
|
for folder in folders! {
|
|
|
|
|
s += folder.OPMLString(indentLevel: indentLevel + 1)
|
2017-09-16 15:25:38 -07:00
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
}
|