2017-07-03 19:29:44 +02: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-17 00:25:38 +02:00
|
|
|
|
import Data
|
2017-09-17 21:08:50 +02:00
|
|
|
|
import RSParser
|
2017-09-18 02:03:58 +02:00
|
|
|
|
import Database
|
2017-10-08 02:20:19 +02:00
|
|
|
|
import RSWeb
|
2017-07-03 19:29:44 +02:00
|
|
|
|
|
2017-10-07 23:40:14 +02:00
|
|
|
|
public extension Notification.Name {
|
|
|
|
|
|
|
|
|
|
public static let AccountRefreshDidBegin = Notification.Name(rawValue: "AccountRefreshDidBegin")
|
|
|
|
|
public static let AccountRefreshDidFinish = Notification.Name(rawValue: "AccountRefreshDidFinish")
|
2017-10-08 02:20:19 +02:00
|
|
|
|
public static let AccountRefreshProgressDidChange = Notification.Name(rawValue: "AccountRefreshProgressDidChange")
|
2017-10-07 23:40:14 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-07-03 19:29:44 +02: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-09-17 20:32:58 +02:00
|
|
|
|
public final class Account: DisplayNameProvider, Hashable {
|
2017-07-03 19:29:44 +02:00
|
|
|
|
|
2017-09-17 21:08:50 +02:00
|
|
|
|
public let accountID: String
|
2017-07-03 19:29:44 +02:00
|
|
|
|
public let type: AccountType
|
2017-09-17 21:08:50 +02:00
|
|
|
|
public var nameForDisplay = ""
|
2017-07-03 19:29:44 +02:00
|
|
|
|
public let hashValue: Int
|
|
|
|
|
let settingsFile: String
|
|
|
|
|
let dataFolder: String
|
2017-09-18 02:03:58 +02:00
|
|
|
|
let database: Database
|
2017-10-08 02:43:10 +02:00
|
|
|
|
let delegate: AccountDelegate
|
2017-09-28 22:16:47 +02:00
|
|
|
|
var topLevelObjects = [AnyObject]()
|
2017-07-03 19:29:44 +02:00
|
|
|
|
var feedIDDictionary = [String: Feed]()
|
|
|
|
|
var username: String?
|
2017-10-08 03:15:42 +02:00
|
|
|
|
var saveTimer: Timer?
|
|
|
|
|
|
2017-10-08 05:24:58 +02:00
|
|
|
|
var dirty = false {
|
2017-10-08 03:15:42 +02:00
|
|
|
|
didSet {
|
2017-10-08 05:24:58 +02:00
|
|
|
|
|
|
|
|
|
if refreshInProgress {
|
|
|
|
|
if let _ = saveTimer {
|
|
|
|
|
removeSaveTimer()
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-08 03:15:42 +02:00
|
|
|
|
if dirty {
|
2017-10-08 05:11:17 +02:00
|
|
|
|
resetSaveTimer()
|
2017-10-08 03:15:42 +02:00
|
|
|
|
}
|
2017-10-08 05:11:17 +02:00
|
|
|
|
else {
|
|
|
|
|
removeSaveTimer()
|
2017-10-08 03:15:42 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-10-07 23:40:14 +02:00
|
|
|
|
|
|
|
|
|
var refreshInProgress = false {
|
|
|
|
|
didSet {
|
|
|
|
|
if refreshInProgress != oldValue {
|
|
|
|
|
if refreshInProgress {
|
|
|
|
|
NotificationCenter.default.post(name: .AccountRefreshDidBegin, object: self)
|
|
|
|
|
}
|
|
|
|
|
else {
|
2017-10-08 03:31:34 +02:00
|
|
|
|
NotificationCenter.default.post(name: .AccountRefreshDidFinish, object: self)
|
2017-10-08 05:24:58 +02:00
|
|
|
|
if dirty {
|
|
|
|
|
resetSaveTimer()
|
|
|
|
|
}
|
2017-10-07 23:40:14 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-28 22:16:47 +02:00
|
|
|
|
|
2017-10-08 02:20:19 +02:00
|
|
|
|
var refreshProgress: DownloadProgress {
|
|
|
|
|
get {
|
|
|
|
|
return delegate.refreshProgress
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-02 22:15:07 +02:00
|
|
|
|
var hasAtLeastOneFeed: Bool {
|
|
|
|
|
get {
|
|
|
|
|
return !feedIDDictionary.isEmpty
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-28 22:16:47 +02:00
|
|
|
|
var supportsSubFolders: Bool {
|
|
|
|
|
get {
|
|
|
|
|
return delegate.supportsSubFolders
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-10-07 23:40:14 +02:00
|
|
|
|
|
2017-10-08 02:43:10 +02:00
|
|
|
|
init?(dataFolder: String, settingsFile: String, type: AccountType, accountID: String) {
|
2017-10-07 23:40:14 +02:00
|
|
|
|
|
|
|
|
|
// TODO: support various syncing systems.
|
2017-10-08 02:43:10 +02:00
|
|
|
|
precondition(type == .onMyMac)
|
|
|
|
|
self.delegate = LocalAccountDelegate()
|
2017-09-18 02:03:58 +02:00
|
|
|
|
|
|
|
|
|
self.accountID = accountID
|
|
|
|
|
self.type = type
|
|
|
|
|
self.settingsFile = settingsFile
|
|
|
|
|
self.dataFolder = dataFolder
|
|
|
|
|
self.hashValue = accountID.hashValue
|
|
|
|
|
|
|
|
|
|
let databaseFilePath = (dataFolder as NSString).appendingPathComponent("DB.sqlite3")
|
|
|
|
|
self.database = Database(databaseFilePath: databaseFilePath, accountID: accountID)
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
2017-10-08 02:43:10 +02:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(downloadProgressDidChange(_:)), name: .DownloadProgressDidChange, object: nil)
|
|
|
|
|
|
2017-09-27 22:29:05 +02:00
|
|
|
|
pullObjectsFromDisk()
|
2017-07-03 19:29:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 20:32:58 +02:00
|
|
|
|
// MARK: - API
|
|
|
|
|
|
|
|
|
|
public func refreshAll() {
|
|
|
|
|
|
2017-10-08 02:43:10 +02:00
|
|
|
|
delegate.refreshAll(for: self)
|
2017-07-03 19:29:44 +02:00
|
|
|
|
}
|
2017-09-17 00:30:26 +02:00
|
|
|
|
|
2017-09-17 21:08:50 +02:00
|
|
|
|
func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: RSVoidCompletionBlock) {
|
|
|
|
|
|
2017-10-08 02:53:37 +02:00
|
|
|
|
completion()
|
2017-09-17 21:08:50 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 21:54:08 +02:00
|
|
|
|
public func markArticles(_ articles: Set<Article>, statusKey: String, flag: Bool) {
|
|
|
|
|
|
2017-09-19 17:07:06 +02:00
|
|
|
|
database.mark(articles, statusKey: statusKey, flag: flag)
|
2017-09-18 01:30:45 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 22:07:55 +02:00
|
|
|
|
public func ensureFolder(with name: String) -> Folder? {
|
|
|
|
|
|
|
|
|
|
return nil //TODO
|
|
|
|
|
}
|
2017-09-25 22:31:36 +02: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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 19:59:35 +02:00
|
|
|
|
var didAddFeed = false
|
|
|
|
|
let uniquedFeed = existingFeed(with: feed.feedID) ?? feed
|
|
|
|
|
|
|
|
|
|
if let folder = folder {
|
|
|
|
|
didAddFeed = folder.addFeed(uniquedFeed)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
if !topLevelObjectsContainsFeed(uniquedFeed) {
|
|
|
|
|
topLevelObjects += [uniquedFeed]
|
|
|
|
|
}
|
|
|
|
|
didAddFeed = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateFeedIDDictionary()
|
|
|
|
|
return didAddFeed // TODO
|
2017-09-25 22:31:36 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-01 20:28:44 +02:00
|
|
|
|
public func createFeed(with name: String?, editedName: String?, url: String) -> Feed? {
|
2017-10-01 01:56:48 +02: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 20:28:44 +02:00
|
|
|
|
if let editedName = editedName {
|
|
|
|
|
feed.editedName = editedName
|
|
|
|
|
}
|
2017-10-01 01:56:48 +02:00
|
|
|
|
return feed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let feed = Feed(accountID: accountID, url: url, feedID: url)
|
|
|
|
|
feed.name = name
|
2017-10-01 19:59:35 +02:00
|
|
|
|
feed.editedName = editedName
|
2017-10-01 01:56:48 +02:00
|
|
|
|
return feed
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-27 06:43:40 +02:00
|
|
|
|
public func canAddFolder(_ folder: Folder, to containingFolder: Folder?) -> Bool {
|
2017-09-25 22:31:36 +02:00
|
|
|
|
|
|
|
|
|
return false // TODO
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-27 06:43:40 +02:00
|
|
|
|
public func addFolder(_ folder: Folder, to containingFolder: Folder?) -> Bool {
|
2017-09-25 22:31:36 +02:00
|
|
|
|
|
|
|
|
|
return false // TODO
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-19 07:00:35 +02:00
|
|
|
|
public func importOPML(_ opmlDocument: RSOPMLDocument) {
|
|
|
|
|
|
|
|
|
|
// TODO
|
|
|
|
|
}
|
2017-10-08 02:20:19 +02:00
|
|
|
|
|
2017-10-08 02:43:10 +02:00
|
|
|
|
// MARK: - Notifications
|
|
|
|
|
|
|
|
|
|
@objc func downloadProgressDidChange(_ note: Notification) {
|
2017-10-08 02:20:19 +02:00
|
|
|
|
|
2017-10-08 02:43:10 +02:00
|
|
|
|
guard let noteObject = note.object as? DownloadProgress, noteObject === refreshProgress else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2017-10-08 02:20:19 +02:00
|
|
|
|
|
|
|
|
|
refreshInProgress = refreshProgress.numberRemaining > 0
|
|
|
|
|
NotificationCenter.default.post(name: .AccountRefreshProgressDidChange, object: self)
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 20:32:58 +02:00
|
|
|
|
// MARK: - Equatable
|
2017-09-17 00:30:26 +02:00
|
|
|
|
|
2017-09-17 20:32:58 +02:00
|
|
|
|
public class func ==(lhs: Account, rhs: Account) -> Bool {
|
2017-09-17 00:30:26 +02:00
|
|
|
|
|
2017-09-17 20:32:58 +02:00
|
|
|
|
return lhs === rhs
|
2017-09-17 00:30:26 +02:00
|
|
|
|
}
|
2017-07-03 19:29:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2017-10-08 05:11:17 +02:00
|
|
|
|
// MARK: - Disk (Public)
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
|
|
|
|
extension Account {
|
|
|
|
|
|
2017-10-08 05:11:17 +02:00
|
|
|
|
func objects(with diskObjects: [[String: Any]]) -> [AnyObject] {
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
2017-10-08 05:11:17 +02:00
|
|
|
|
return diskObjects.flatMap { object(with: $0) }
|
2017-09-27 22:29:05 +02:00
|
|
|
|
}
|
2017-10-08 05:11:17 +02:00
|
|
|
|
}
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
2017-10-08 05:11:17 +02:00
|
|
|
|
// MARK: - Disk (Private)
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
2017-10-08 05:11:17 +02:00
|
|
|
|
private extension Account {
|
|
|
|
|
|
|
|
|
|
struct Key {
|
|
|
|
|
static let children = "children"
|
2017-09-27 22:29:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-28 22:16:47 +02:00
|
|
|
|
func object(with diskObject: [String: Any]) -> AnyObject? {
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
2017-09-28 22:16:47 +02:00
|
|
|
|
if Feed.isFeedDictionary(diskObject) {
|
2017-09-27 22:29:05 +02:00
|
|
|
|
return Feed(accountID: accountID, dictionary: diskObject)
|
|
|
|
|
}
|
2017-09-28 22:16:47 +02:00
|
|
|
|
return Folder(account: self, dictionary: diskObject)
|
2017-09-27 22:29:05 +02:00
|
|
|
|
}
|
2017-10-08 03:15:42 +02:00
|
|
|
|
|
2017-10-08 05:11:17 +02:00
|
|
|
|
func pullObjectsFromDisk() {
|
2017-10-08 03:15:42 +02:00
|
|
|
|
|
2017-10-08 05:11:17 +02: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-08 03:15:42 +02:00
|
|
|
|
}
|
2017-10-08 05:11:17 +02:00
|
|
|
|
topLevelObjects = objects(with: childrenArray)
|
|
|
|
|
updateFeedIDDictionary()
|
2017-10-08 03:15:42 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-08 05:11:17 +02:00
|
|
|
|
func diskDictionary() -> NSDictionary {
|
2017-10-08 03:15:42 +02:00
|
|
|
|
|
|
|
|
|
let diskObjects = topLevelObjects.flatMap { (object) -> [String: Any]? in
|
|
|
|
|
|
|
|
|
|
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
|
|
|
|
|
return d as NSDictionary
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-08 05:11:17 +02:00
|
|
|
|
func saveToDiskIfNeeded() {
|
|
|
|
|
|
|
|
|
|
if !dirty {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if refreshInProgress {
|
|
|
|
|
resetSaveTimer()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
saveToDisk()
|
|
|
|
|
dirty = false
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-08 03:15:42 +02:00
|
|
|
|
func saveToDisk() {
|
|
|
|
|
|
|
|
|
|
let d = diskDictionary()
|
|
|
|
|
do {
|
|
|
|
|
try RSPlist.write(d, filePath: settingsFile)
|
|
|
|
|
}
|
|
|
|
|
catch let error as NSError {
|
|
|
|
|
NSApplication.shared.presentError(error)
|
|
|
|
|
}
|
2017-10-08 05:11:17 +02:00
|
|
|
|
}
|
2017-10-08 03:15:42 +02:00
|
|
|
|
|
2017-10-08 05:11:17 +02: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-08 03:15:42 +02:00
|
|
|
|
}
|
2017-09-27 22:29:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-10-01 19:59:35 +02:00
|
|
|
|
// MARK: - Private
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
|
|
|
|
private extension Account {
|
|
|
|
|
|
|
|
|
|
func updateFeedIDDictionary() {
|
|
|
|
|
|
|
|
|
|
var d = [String: Feed]()
|
|
|
|
|
for feed in flattenedFeeds() {
|
|
|
|
|
d[feed.feedID] = feed
|
|
|
|
|
}
|
|
|
|
|
feedIDDictionary = d
|
|
|
|
|
}
|
2017-10-01 19:59:35 +02:00
|
|
|
|
|
|
|
|
|
func topLevelObjectsContainsFeed(_ feed: Feed) -> Bool {
|
|
|
|
|
|
|
|
|
|
return topLevelObjects.contains(where: { (object) -> Bool in
|
|
|
|
|
if let oneFeed = object as? Feed {
|
|
|
|
|
if oneFeed.feedID == feed.feedID {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
})
|
|
|
|
|
}
|
2017-07-03 19:29:44 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-27 22:29:05 +02:00
|
|
|
|
// MARK: - OPMLRepresentable
|
|
|
|
|
|
2017-09-17 00:25:38 +02:00
|
|
|
|
extension Account: OPMLRepresentable {
|
|
|
|
|
|
|
|
|
|
public func OPMLString(indentLevel: Int) -> String {
|
|
|
|
|
|
|
|
|
|
var s = ""
|
|
|
|
|
for oneObject in topLevelObjects {
|
|
|
|
|
if let oneOPMLObject = oneObject as? OPMLRepresentable {
|
|
|
|
|
s += oneOPMLObject.OPMLString(indentLevel: indentLevel + 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
}
|