NetNewsWire/Frameworks/Account/Account.swift

370 lines
7.7 KiB
Swift
Raw Normal View History

//
// Account.swift
// DataModel
//
// Created by Brent Simmons on 7/1/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import RSCore
import Data
2017-09-17 21:08:50 +02:00
import RSParser
2017-09-18 02:03:58 +02:00
import Database
import RSWeb
public extension Notification.Name {
public static let AccountRefreshDidBegin = Notification.Name(rawValue: "AccountRefreshDidBegin")
public static let AccountRefreshDidFinish = Notification.Name(rawValue: "AccountRefreshDidFinish")
public static let AccountRefreshProgressDidChange = Notification.Name(rawValue: "AccountRefreshProgressDidChange")
}
public enum AccountType: Int {
// Raw values should not change since theyre 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-09-17 21:08:50 +02:00
public let accountID: String
public let type: AccountType
2017-09-17 21:08:50 +02:00
public var nameForDisplay = ""
public let hashValue: Int
let settingsFile: String
let dataFolder: String
2017-09-18 02:03:58 +02:00
let database: Database
let delegate: AccountDelegate
2017-09-28 22:16:47 +02:00
var topLevelObjects = [AnyObject]()
var feedIDDictionary = [String: Feed]()
var username: String?
var saveTimer: Timer?
var dirty = false {
didSet {
if refreshInProgress {
if let _ = saveTimer {
removeSaveTimer()
}
return
}
if dirty {
resetSaveTimer()
}
else {
removeSaveTimer()
}
}
}
var refreshInProgress = false {
didSet {
if refreshInProgress != oldValue {
if refreshInProgress {
NotificationCenter.default.post(name: .AccountRefreshDidBegin, object: self)
}
else {
NotificationCenter.default.post(name: .AccountRefreshDidFinish, object: self)
if dirty {
resetSaveTimer()
}
}
}
}
}
2017-09-28 22:16:47 +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
}
}
init?(dataFolder: String, settingsFile: String, type: AccountType, accountID: String) {
// TODO: support various syncing systems.
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)
NotificationCenter.default.addObserver(self, selector: #selector(downloadProgressDidChange(_:)), name: .DownloadProgressDidChange, object: nil)
pullObjectsFromDisk()
}
2017-09-17 20:32:58 +02:00
// MARK: - API
public func refreshAll() {
delegate.refreshAll(for: self)
}
2017-09-17 21:08:50 +02:00
func update(_ feed: Feed, with parsedFeed: ParsedFeed, _ completion: RSVoidCompletionBlock) {
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) {
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
}
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 cant 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 couldnt 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-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 {
return false // TODO
}
2017-09-27 06:43:40 +02:00
public func addFolder(_ folder: Folder, to containingFolder: Folder?) -> Bool {
return false // TODO
}
public func importOPML(_ opmlDocument: RSOPMLDocument) {
// TODO
}
// MARK: - Notifications
@objc func downloadProgressDidChange(_ note: Notification) {
guard let noteObject = note.object as? DownloadProgress, noteObject === refreshProgress else {
return
}
refreshInProgress = refreshProgress.numberRemaining > 0
NotificationCenter.default.post(name: .AccountRefreshProgressDidChange, object: self)
}
2017-09-17 20:32:58 +02:00
// MARK: - Equatable
2017-09-17 20:32:58 +02:00
public class func ==(lhs: Account, rhs: Account) -> Bool {
2017-09-17 20:32:58 +02:00
return lhs === rhs
}
}
// MARK: - Disk (Public)
extension Account {
func objects(with diskObjects: [[String: Any]]) -> [AnyObject] {
return diskObjects.flatMap { object(with: $0) }
}
}
// MARK: - Disk (Private)
private extension Account {
struct Key {
static let children = "children"
}
2017-09-28 22:16:47 +02:00
func object(with diskObject: [String: Any]) -> AnyObject? {
2017-09-28 22:16:47 +02:00
if Feed.isFeedDictionary(diskObject) {
return Feed(accountID: accountID, dictionary: diskObject)
}
2017-09-28 22:16:47 +02:00
return Folder(account: self, dictionary: diskObject)
}
func pullObjectsFromDisk() {
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
}
topLevelObjects = objects(with: childrenArray)
updateFeedIDDictionary()
}
func diskDictionary() -> NSDictionary {
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
}
func saveToDiskIfNeeded() {
if !dirty {
return
}
if refreshInProgress {
resetSaveTimer()
return
}
saveToDisk()
dirty = false
}
func saveToDisk() {
let d = diskDictionary()
do {
try RSPlist.write(d, filePath: settingsFile)
}
catch let error as NSError {
NSApplication.shared.presentError(error)
}
}
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-01 19:59:35 +02:00
// MARK: - Private
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
})
}
}
// MARK: - OPMLRepresentable
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
}
}