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.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
import RSCore
|
2017-09-16 15:25:38 -07:00
|
|
|
|
import Data
|
2017-09-17 12:08:50 -07:00
|
|
|
|
import RSParser
|
2017-09-17 17:03:58 -07:00
|
|
|
|
import Database
|
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 {
|
|
|
|
|
|
|
|
|
|
public static let AccountRefreshDidBegin = Notification.Name(rawValue: "AccountRefreshDidBegin")
|
|
|
|
|
public static let AccountRefreshDidFinish = Notification.Name(rawValue: "AccountRefreshDidFinish")
|
2017-10-07 17:20:19 -07:00
|
|
|
|
public static let AccountRefreshProgressDidChange = Notification.Name(rawValue: "AccountRefreshProgressDidChange")
|
2017-10-08 01:54:37 -07:00
|
|
|
|
public static let AccountDidDownloadArticles = Notification.Name(rawValue: "AccountDidDownloadArticles")
|
2017-10-08 21:06:25 -07:00
|
|
|
|
|
|
|
|
|
public 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
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 12:08:50 -07:00
|
|
|
|
public let accountID: String
|
2017-07-03 10:29:44 -07:00
|
|
|
|
public let type: AccountType
|
2017-09-17 12:08:50 -07:00
|
|
|
|
public var nameForDisplay = ""
|
2017-07-03 10:29:44 -07:00
|
|
|
|
public let hashValue: Int
|
2017-10-07 21:41:21 -07:00
|
|
|
|
public var children = [AnyObject]()
|
2017-10-22 11:08:51 -07:00
|
|
|
|
var urlToFeedDictionary = [String: Feed]()
|
|
|
|
|
var idToFeedDictionary = [String: Feed]()
|
2017-07-03 10:29:44 -07:00
|
|
|
|
let settingsFile: String
|
|
|
|
|
let dataFolder: String
|
2017-09-17 17:03:58 -07:00
|
|
|
|
let database: Database
|
2017-10-07 17:43:10 -07:00
|
|
|
|
let delegate: AccountDelegate
|
2017-07-03 10:29:44 -07:00
|
|
|
|
var username: String?
|
2017-10-07 18:15:42 -07:00
|
|
|
|
var saveTimer: Timer?
|
|
|
|
|
|
2017-10-07 21:41:21 -07:00
|
|
|
|
public var dirty = false {
|
2017-10-07 18:15:42 -07:00
|
|
|
|
didSet {
|
2017-10-07 20:24:58 -07:00
|
|
|
|
|
|
|
|
|
if refreshInProgress {
|
|
|
|
|
if let _ = saveTimer {
|
|
|
|
|
removeSaveTimer()
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-07 18:15:42 -07:00
|
|
|
|
if dirty {
|
2017-10-07 20:11:17 -07:00
|
|
|
|
resetSaveTimer()
|
2017-10-07 18:15:42 -07:00
|
|
|
|
}
|
2017-10-07 20:11:17 -07:00
|
|
|
|
else {
|
|
|
|
|
removeSaveTimer()
|
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()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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)
|
2017-10-07 20:24:58 -07:00
|
|
|
|
if dirty {
|
|
|
|
|
resetSaveTimer()
|
|
|
|
|
}
|
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 {
|
|
|
|
|
get {
|
|
|
|
|
return delegate.refreshProgress
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-28 13:16:47 -07:00
|
|
|
|
var supportsSubFolders: Bool {
|
|
|
|
|
get {
|
|
|
|
|
return delegate.supportsSubFolders
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-10-07 14:40:14 -07:00
|
|
|
|
|
2017-10-07 17:43:10 -07:00
|
|
|
|
init?(dataFolder: String, settingsFile: String, type: AccountType, accountID: String) {
|
2017-10-07 14:40:14 -07:00
|
|
|
|
|
|
|
|
|
// TODO: support various syncing systems.
|
2017-10-07 17:43:10 -07:00
|
|
|
|
precondition(type == .onMyMac)
|
|
|
|
|
self.delegate = LocalAccountDelegate()
|
2017-09-17 17:03:58 -07:00
|
|
|
|
|
|
|
|
|
self.accountID = accountID
|
|
|
|
|
self.type = type
|
|
|
|
|
self.settingsFile = settingsFile
|
|
|
|
|
self.dataFolder = dataFolder
|
|
|
|
|
self.hashValue = accountID.hashValue
|
2017-12-19 17:48:30 -08:00
|
|
|
|
|
2017-09-17 17:03:58 -07:00
|
|
|
|
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("DB.sqlite3")
|
|
|
|
|
self.database = Database(databaseFilePath: databaseFilePath, accountID: accountID)
|
2017-09-27 13:29:05 -07:00
|
|
|
|
|
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)
|
2017-11-25 13:48:14 -08:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
|
2018-01-23 21:49:33 -08:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
2017-11-25 13:48:14 -08:00
|
|
|
|
|
2017-09-27 13:29:05 -07:00
|
|
|
|
pullObjectsFromDisk()
|
2017-10-18 18:37:45 -07:00
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.updateUnreadCount()
|
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)
|
2017-07-03 10:29:44 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 11:32:58 -07:00
|
|
|
|
// MARK: - API
|
|
|
|
|
|
|
|
|
|
public func refreshAll() {
|
|
|
|
|
|
2017-10-07 17:43:10 -07:00
|
|
|
|
delegate.refreshAll(for: self)
|
2017-07-03 10:29:44 -07:00
|
|
|
|
}
|
2017-09-16 15:30:26 -07:00
|
|
|
|
|
2017-12-02 17:47:08 -08:00
|
|
|
|
public func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: @escaping RSVoidCompletionBlock) {
|
2017-10-08 01:54:37 -07:00
|
|
|
|
|
2017-11-25 13:48:14 -08:00
|
|
|
|
feed.takeSettings(from: parsedFeed)
|
|
|
|
|
|
2017-10-08 01:54:37 -07:00
|
|
|
|
database.update(feed: feed, parsedFeed: parsedFeed) { (newArticles, updatedArticles) in
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
}
|
2018-01-17 17:28:09 -08:00
|
|
|
|
userInfo[UserInfoKey.feeds] = Set([feed])
|
2017-09-17 12:08:50 -07:00
|
|
|
|
|
2017-10-08 01:54:37 -07:00
|
|
|
|
completion()
|
|
|
|
|
|
2018-01-17 17:28:09 -08:00
|
|
|
|
NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo)
|
2017-10-08 01:54:37 -07:00
|
|
|
|
}
|
2017-09-17 12:08:50 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-29 11:14:10 -07:00
|
|
|
|
public func markArticles(_ articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>? {
|
|
|
|
|
|
|
|
|
|
// Returns set of Articles whose statuses did change.
|
|
|
|
|
|
2017-10-09 21:54:08 -07:00
|
|
|
|
guard let updatedStatuses = database.mark(articles, statusKey: statusKey, flag: flag) else {
|
2017-10-29 11:14:10 -07:00
|
|
|
|
return nil
|
2017-10-08 21:06:25 -07:00
|
|
|
|
}
|
2017-10-09 21:54:08 -07:00
|
|
|
|
|
|
|
|
|
let updatedArticleIDs = updatedStatuses.articleIDs()
|
|
|
|
|
let updatedArticles = Set(articles.filter{ updatedArticleIDs.contains($0.articleID) })
|
2017-10-10 13:23:12 -07:00
|
|
|
|
|
|
|
|
|
noteStatusesForArticlesDidChange(updatedArticles)
|
2017-10-29 11:14:10 -07:00
|
|
|
|
return updatedArticles
|
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)
|
|
|
|
|
children += [folder]
|
|
|
|
|
dirty = true
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-25 13:31:36 -07:00
|
|
|
|
public func canAddFeed(_ feed: Feed, to folder: Folder?) -> Bool {
|
|
|
|
|
|
|
|
|
|
// If folder is nil, then it should go at the top level.
|
|
|
|
|
// The same feed in multiple folders is allowed.
|
|
|
|
|
// But the same feed can’t appear twice in the same folder
|
|
|
|
|
// (or at the top level).
|
|
|
|
|
|
|
|
|
|
return true // TODO
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-21 16:32:29 -07:00
|
|
|
|
@discardableResult
|
2017-09-25 13:31:36 -07:00
|
|
|
|
public func addFeed(_ feed: Feed, to folder: Folder?) -> Bool {
|
|
|
|
|
|
|
|
|
|
// Return false if it couldn’t be added.
|
|
|
|
|
// If it already existed in that folder, return true.
|
|
|
|
|
|
2017-10-01 10:59:35 -07:00
|
|
|
|
var didAddFeed = false
|
|
|
|
|
let uniquedFeed = existingFeed(with: feed.feedID) ?? feed
|
|
|
|
|
|
|
|
|
|
if let folder = folder {
|
|
|
|
|
didAddFeed = folder.addFeed(uniquedFeed)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if !topLevelObjectsContainsFeed(uniquedFeed) {
|
2017-10-07 21:41:21 -07:00
|
|
|
|
children += [uniquedFeed]
|
2017-10-21 16:06:25 -07:00
|
|
|
|
postChildrenDidChangeNotification()
|
2017-10-01 10:59:35 -07:00
|
|
|
|
}
|
|
|
|
|
didAddFeed = true
|
|
|
|
|
}
|
2017-12-11 23:12:54 -08:00
|
|
|
|
|
|
|
|
|
if didAddFeed {
|
|
|
|
|
addToFeedDictionaries(uniquedFeed)
|
|
|
|
|
dirty = true
|
|
|
|
|
}
|
2017-10-01 10:59:35 -07:00
|
|
|
|
|
2017-12-11 23:12:54 -08:00
|
|
|
|
return didAddFeed
|
2017-09-25 13:31:36 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-01 11:28:44 -07:00
|
|
|
|
public func createFeed(with name: String?, editedName: String?, url: String) -> Feed? {
|
2017-09-30 16:56:48 -07:00
|
|
|
|
|
|
|
|
|
// For syncing, this may need to be an async method with a callback,
|
|
|
|
|
// since it will likely need to call the server.
|
|
|
|
|
|
|
|
|
|
if let feed = existingFeed(withURL: url) {
|
2017-10-01 11:28:44 -07:00
|
|
|
|
if let editedName = editedName {
|
|
|
|
|
feed.editedName = editedName
|
|
|
|
|
}
|
2017-09-30 16:56:48 -07:00
|
|
|
|
return feed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let feed = Feed(accountID: accountID, url: url, feedID: url)
|
|
|
|
|
feed.name = name
|
2017-10-01 10:59:35 -07:00
|
|
|
|
feed.editedName = editedName
|
2017-09-30 16:56:48 -07:00
|
|
|
|
return feed
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-26 21:43:40 -07:00
|
|
|
|
public func canAddFolder(_ folder: Folder, to containingFolder: Folder?) -> Bool {
|
2017-09-25 13:31:36 -07:00
|
|
|
|
|
|
|
|
|
return false // TODO
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-21 21:00:21 -07:00
|
|
|
|
@discardableResult
|
|
|
|
|
public func addFolder(_ folder: Folder, to parentFolder: Folder?) -> Bool {
|
2017-09-25 13:31:36 -07:00
|
|
|
|
|
2017-11-05 12:14:36 -08:00
|
|
|
|
// TODO: support subfolders, maybe, some day, if one of the sync systems
|
|
|
|
|
// supports subfolders. But, for now, parentFolder is ignored.
|
|
|
|
|
|
|
|
|
|
if objectIsChild(folder) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
children += [folder]
|
|
|
|
|
postChildrenDidChangeNotification()
|
2017-11-25 18:05:20 -08:00
|
|
|
|
rebuildFeedDictionaries()
|
2017-11-05 12:14:36 -08:00
|
|
|
|
return true
|
2017-09-25 13:31:36 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-11-04 22:51:14 -07:00
|
|
|
|
public func importOPML(_ opmlDocument: RSOPMLDocument) {
|
2017-10-07 21:41:21 -07:00
|
|
|
|
|
|
|
|
|
guard let children = opmlDocument.children else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-12-11 23:12:54 -08:00
|
|
|
|
rebuildFeedDictionaries()
|
2017-10-21 21:00:21 -07:00
|
|
|
|
importOPMLItems(children, parentFolder: nil)
|
2017-12-18 21:46:35 -08:00
|
|
|
|
saveToDisk()
|
2017-12-03 12:54:51 -08:00
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.refreshAll()
|
|
|
|
|
}
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-08 01:54:37 -07:00
|
|
|
|
database.fetchUnreadCounts(for: feeds) { (unreadCountDictionary) in
|
|
|
|
|
|
|
|
|
|
for feed in feeds {
|
|
|
|
|
if let unreadCount = unreadCountDictionary[feed] {
|
|
|
|
|
feed.unreadCount = unreadCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-08 18:58:15 -07:00
|
|
|
|
public func fetchArticles(for feed: Feed) -> Set<Article> {
|
2017-12-26 11:27:55 -08:00
|
|
|
|
|
|
|
|
|
let articles = database.fetchArticles(for: feed)
|
|
|
|
|
validateUnreadCount(feed, articles)
|
|
|
|
|
return articles
|
2017-10-08 18:58:15 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func fetchArticles(folder: Folder) -> Set<Article> {
|
2017-12-26 11:27:55 -08:00
|
|
|
|
|
|
|
|
|
let feeds = folder.flattenedFeeds()
|
|
|
|
|
let articles = database.fetchUnreadArticles(for: feeds)
|
|
|
|
|
feeds.forEach { validateUnreadCount($0, articles) }
|
|
|
|
|
return articles
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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()
|
2017-11-19 12:45:35 -08:00
|
|
|
|
database.fetchUnreadCount(for: flattenedFeeds(), 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) {
|
|
|
|
|
|
|
|
|
|
database.fetchStarredAndUnreadCount(for: flattenedFeeds(), callback: callback)
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-19 16:28:26 -08:00
|
|
|
|
public func markEverywhereAsRead() {
|
|
|
|
|
|
|
|
|
|
// Does not support undo.
|
|
|
|
|
|
|
|
|
|
database.markEverywhereAsRead()
|
|
|
|
|
flattenedFeeds().forEach { $0.unreadCount = 0 }
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-25 11:13:15 -08:00
|
|
|
|
// MARK: - Debug
|
|
|
|
|
|
|
|
|
|
public func debugDropConditionalGetInfo() {
|
|
|
|
|
|
|
|
|
|
#if DEBUG
|
|
|
|
|
flattenedFeeds().forEach{ $0.debugDropConditionalGetInfo() }
|
|
|
|
|
#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) {
|
2017-10-13 06:50:33 -07:00
|
|
|
|
|
2017-10-18 19:14:40 -07:00
|
|
|
|
// Update the unread count if it’s a direct child.
|
|
|
|
|
// If the object is owned by this account, then mark dirty —
|
|
|
|
|
// since unread counts are saved to disk along with other feed info.
|
2017-10-18 19:46:35 -07:00
|
|
|
|
|
2017-10-13 06:58:15 -07:00
|
|
|
|
if let object = note.object {
|
2017-10-18 19:14:40 -07:00
|
|
|
|
|
2017-10-13 06:58:15 -07:00
|
|
|
|
if objectIsChild(object as AnyObject) {
|
2017-10-13 06:50:33 -07:00
|
|
|
|
updateUnreadCount()
|
2017-10-18 19:14:40 -07:00
|
|
|
|
self.dirty = true
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let feed = object as? Feed {
|
|
|
|
|
if feed.account === self {
|
|
|
|
|
self.dirty = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if let folder = object as? Folder {
|
|
|
|
|
if folder.account === self {
|
|
|
|
|
self.dirty = true
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
|
rebuildFeedDictionaries()
|
2017-11-15 13:26:10 -08:00
|
|
|
|
updateUnreadCount()
|
|
|
|
|
}
|
2017-10-07 17:20:19 -07:00
|
|
|
|
|
2017-11-25 13:48:14 -08:00
|
|
|
|
@objc func feedSettingDidChange(_ note: Notification) {
|
|
|
|
|
|
|
|
|
|
if let feed = note.object as? Feed, let feedAccount = feed.account, feedAccount === self {
|
|
|
|
|
dirty = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-23 21:49:33 -08:00
|
|
|
|
@objc func displayNameDidChange(_ note: Notification) {
|
|
|
|
|
|
|
|
|
|
if let feed = note.object as? Feed, let feedAccount = feed.account, feedAccount === self {
|
|
|
|
|
dirty = true
|
|
|
|
|
}
|
|
|
|
|
if let folder = note.object as? Folder, let folderAccount = folder.account, folderAccount === self {
|
|
|
|
|
dirty = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2017-10-07 20:11:17 -07:00
|
|
|
|
// MARK: - Disk (Public)
|
2017-09-27 13:29:05 -07:00
|
|
|
|
|
|
|
|
|
extension Account {
|
|
|
|
|
|
2017-10-07 20:11:17 -07:00
|
|
|
|
func objects(with diskObjects: [[String: Any]]) -> [AnyObject] {
|
2017-09-27 13:29:05 -07:00
|
|
|
|
|
2017-10-07 20:11:17 -07:00
|
|
|
|
return diskObjects.flatMap { object(with: $0) }
|
2017-09-27 13:29:05 -07:00
|
|
|
|
}
|
2017-10-07 20:11:17 -07:00
|
|
|
|
}
|
2017-09-27 13:29:05 -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 {
|
|
|
|
|
|
|
|
|
|
struct Key {
|
|
|
|
|
static let children = "children"
|
2017-10-18 19:14:40 -07:00
|
|
|
|
static let userInfo = "userInfo"
|
|
|
|
|
static let unreadCount = "unreadCount"
|
2017-09-27 13:29:05 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-28 13:16:47 -07:00
|
|
|
|
func object(with diskObject: [String: Any]) -> AnyObject? {
|
2017-09-27 13:29:05 -07:00
|
|
|
|
|
2017-09-28 13:16:47 -07:00
|
|
|
|
if Feed.isFeedDictionary(diskObject) {
|
2017-09-27 13:29:05 -07:00
|
|
|
|
return Feed(accountID: accountID, dictionary: diskObject)
|
|
|
|
|
}
|
2017-09-28 13:16:47 -07:00
|
|
|
|
return Folder(account: self, dictionary: diskObject)
|
2017-09-27 13:29:05 -07:00
|
|
|
|
}
|
2017-10-07 18:15:42 -07:00
|
|
|
|
|
2017-10-07 20:11:17 -07:00
|
|
|
|
func pullObjectsFromDisk() {
|
2017-10-07 18:15:42 -07:00
|
|
|
|
|
2017-10-07 20:11:17 -07:00
|
|
|
|
let settingsFileURL = URL(fileURLWithPath: settingsFile)
|
|
|
|
|
guard let d = NSDictionary(contentsOf: settingsFileURL) as? [String: Any] else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
guard let childrenArray = d[Key.children] as? [[String: Any]] else {
|
|
|
|
|
return
|
2017-10-07 18:15:42 -07:00
|
|
|
|
}
|
2017-10-07 21:41:21 -07:00
|
|
|
|
children = objects(with: childrenArray)
|
2017-10-22 11:08:51 -07:00
|
|
|
|
rebuildFeedDictionaries()
|
2017-10-18 19:14:40 -07:00
|
|
|
|
|
|
|
|
|
if let savedUnreadCount = d[Key.unreadCount] as? Int {
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.unreadCount = savedUnreadCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let userInfo = d[Key.userInfo] as? NSDictionary
|
|
|
|
|
delegate.update(account: self, withUserInfo: userInfo)
|
2017-10-07 18:15:42 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-07 20:11:17 -07:00
|
|
|
|
func diskDictionary() -> NSDictionary {
|
2017-10-07 18:15:42 -07:00
|
|
|
|
|
2017-10-07 21:41:21 -07:00
|
|
|
|
let diskObjects = children.flatMap { (object) -> [String: Any]? in
|
2017-10-07 18:15:42 -07:00
|
|
|
|
|
|
|
|
|
if let folder = object as? Folder {
|
|
|
|
|
return folder.dictionary
|
|
|
|
|
}
|
|
|
|
|
else if let feed = object as? Feed {
|
|
|
|
|
return feed.dictionary
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var d = [String: Any]()
|
|
|
|
|
d[Key.children] = diskObjects as NSArray
|
2017-10-18 19:14:40 -07:00
|
|
|
|
d[Key.unreadCount] = unreadCount
|
|
|
|
|
|
|
|
|
|
if let userInfo = delegate.userInfo(for: self) {
|
|
|
|
|
d[Key.userInfo] = userInfo
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-07 18:15:42 -07:00
|
|
|
|
return d as NSDictionary
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-07 20:11:17 -07:00
|
|
|
|
func saveToDiskIfNeeded() {
|
|
|
|
|
|
|
|
|
|
if !dirty {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if refreshInProgress {
|
|
|
|
|
resetSaveTimer()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveToDisk()
|
|
|
|
|
dirty = false
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-07 18:15:42 -07:00
|
|
|
|
func saveToDisk() {
|
|
|
|
|
|
|
|
|
|
let d = diskDictionary()
|
|
|
|
|
do {
|
|
|
|
|
try RSPlist.write(d, filePath: settingsFile)
|
|
|
|
|
}
|
|
|
|
|
catch let error as NSError {
|
|
|
|
|
NSApplication.shared.presentError(error)
|
|
|
|
|
}
|
2017-10-07 20:11:17 -07:00
|
|
|
|
}
|
2017-10-07 18:15:42 -07:00
|
|
|
|
|
2017-10-07 20:11:17 -07:00
|
|
|
|
func resetSaveTimer() {
|
|
|
|
|
|
|
|
|
|
saveTimer?.rs_invalidateIfValid()
|
|
|
|
|
|
|
|
|
|
saveTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: false) { (timer) in
|
|
|
|
|
self.saveToDiskIfNeeded()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func removeSaveTimer() {
|
|
|
|
|
|
|
|
|
|
saveTimer?.rs_invalidateIfValid()
|
|
|
|
|
saveTimer = nil
|
2017-10-07 18:15:42 -07:00
|
|
|
|
}
|
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 {
|
|
|
|
|
|
2017-10-22 11:08:51 -07:00
|
|
|
|
func rebuildFeedDictionaries() {
|
|
|
|
|
|
|
|
|
|
var urlDictionary = [String: Feed]()
|
|
|
|
|
var idDictionary = [String: Feed]()
|
|
|
|
|
|
|
|
|
|
flattenedFeeds().forEach { (feed) in
|
|
|
|
|
urlDictionary[feed.url] = feed
|
|
|
|
|
idDictionary[feed.feedID] = feed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
urlToFeedDictionary = urlDictionary
|
|
|
|
|
idToFeedDictionary = idDictionary
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func addToFeedDictionaries(_ feed: Feed) {
|
|
|
|
|
|
|
|
|
|
urlToFeedDictionary[feed.url] = feed
|
|
|
|
|
idToFeedDictionary[feed.feedID] = feed
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-01 10:59:35 -07:00
|
|
|
|
func topLevelObjectsContainsFeed(_ feed: Feed) -> Bool {
|
|
|
|
|
|
2017-10-07 21:41:21 -07:00
|
|
|
|
return children.contains(where: { (object) -> Bool in
|
2017-10-01 10:59:35 -07:00
|
|
|
|
if let oneFeed = object as? Feed {
|
|
|
|
|
if oneFeed.feedID == feed.feedID {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
})
|
|
|
|
|
}
|
2017-10-07 21:41:21 -07:00
|
|
|
|
|
2017-10-21 21:00:21 -07:00
|
|
|
|
func createFeed(with opmlFeedSpecifier: RSOPMLFeedSpecifier) -> Feed {
|
|
|
|
|
|
|
|
|
|
let feed = Feed(accountID: accountID, url: opmlFeedSpecifier.feedURL, feedID: opmlFeedSpecifier.feedURL)
|
|
|
|
|
feed.editedName = opmlFeedSpecifier.title
|
|
|
|
|
return feed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func importOPMLItems(_ items: [RSOPMLItem], parentFolder: Folder?) {
|
2017-10-07 21:41:21 -07:00
|
|
|
|
|
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 {
|
2017-10-21 21:00:21 -07:00
|
|
|
|
let feed = createFeed(with: feedSpecifier)
|
|
|
|
|
addFeed(feed, to: parentFolder)
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-10-07 21:41:21 -07:00
|
|
|
|
|
2017-10-21 21:00:21 -07:00
|
|
|
|
guard item.isFolder, let itemChildren = item.children else {
|
|
|
|
|
return
|
2017-10-07 21:41:21 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-21 21:00:21 -07:00
|
|
|
|
// TODO: possibly support sub folders.
|
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.
|
|
|
|
|
importOPMLItems(itemChildren, parentFolder: parentFolder)
|
|
|
|
|
return
|
2017-10-07 21:41:21 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-21 21:00:21 -07:00
|
|
|
|
if let folder = ensureFolder(with: folderName) {
|
|
|
|
|
importOPMLItems(itemChildren, parentFolder: folder)
|
2017-10-07 21:41:21 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-10-10 13:23:12 -07:00
|
|
|
|
|
|
|
|
|
func updateUnreadCount() {
|
2017-11-11 19:34:10 -08:00
|
|
|
|
|
|
|
|
|
unreadCount = calculateUnreadCount(flattenedFeeds())
|
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
|
|
|
|
|
2017-10-12 21:02:27 -07:00
|
|
|
|
let feeds = Set(articles.flatMap { $0.feed })
|
|
|
|
|
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() {
|
|
|
|
|
|
|
|
|
|
database.fetchAllNonZeroUnreadCounts { (unreadCountDictionary) in
|
|
|
|
|
|
|
|
|
|
if unreadCountDictionary.isEmpty {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.flattenedFeeds().forEach{ (feed) in
|
|
|
|
|
|
|
|
|
|
// When the unread count is zero, it won’t appear in unreadCountDictionary.
|
|
|
|
|
|
|
|
|
|
if let unreadCount = unreadCountDictionary[feed] {
|
|
|
|
|
feed.unreadCount = unreadCount
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
feed.unreadCount = 0
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-07-03 10:29:44 -07:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-22 11:08:51 -07:00
|
|
|
|
// MARK: - Container Overrides
|
|
|
|
|
|
|
|
|
|
extension Account {
|
|
|
|
|
|
|
|
|
|
public func existingFeed(withURL url: String) -> Feed? {
|
|
|
|
|
|
|
|
|
|
return urlToFeedDictionary[url]
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func existingFeed(with feedID: String) -> Feed? {
|
|
|
|
|
|
|
|
|
|
return idToFeedDictionary[feedID]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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 = ""
|
2017-10-07 21:41:21 -07:00
|
|
|
|
for oneObject in children {
|
2017-09-16 15:25:38 -07:00
|
|
|
|
if let oneOPMLObject = oneObject as? OPMLRepresentable {
|
|
|
|
|
s += oneOPMLObject.OPMLString(indentLevel: indentLevel + 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
}
|