2017-05-23 22:24:42 +02:00
|
|
|
|
//
|
|
|
|
|
// LocalAccount.swift
|
2017-05-27 19:43:27 +02:00
|
|
|
|
// Evergreen
|
2017-05-23 22:24:42 +02:00
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 4/23/16.
|
|
|
|
|
// Copyright © 2016 Ranchero Software, LLC. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
import RSCore
|
2017-07-02 02:22:19 +02:00
|
|
|
|
import RSParser
|
2017-05-23 22:24:42 +02:00
|
|
|
|
import RSWeb
|
|
|
|
|
import DataModel
|
|
|
|
|
|
|
|
|
|
private let localAccountType = "OnMyMac"
|
|
|
|
|
|
|
|
|
|
public final class LocalAccount: Account, PlistProvider {
|
|
|
|
|
|
|
|
|
|
public let identifier: String
|
|
|
|
|
public let type = localAccountType
|
|
|
|
|
public let nameForDisplay = NSLocalizedString("On My Mac", comment: "Local account name")
|
|
|
|
|
|
|
|
|
|
private let settingsFile: String
|
|
|
|
|
private let dataFolder: String
|
|
|
|
|
private let diskSaver: DiskSaver
|
|
|
|
|
fileprivate let topLevelFolders = NSMutableDictionary()
|
|
|
|
|
fileprivate let topLevelFeeds = NSMutableDictionary()
|
|
|
|
|
fileprivate let localDatabase: LocalDatabase
|
|
|
|
|
private let refresher = LocalAccountRefresher()
|
|
|
|
|
|
|
|
|
|
public var flattenedFeeds: NSSet {
|
|
|
|
|
get {
|
|
|
|
|
let feeds = NSMutableSet(array: topLevelFeeds.allValues)
|
|
|
|
|
for oneFolder in topLevelFolders.allValues {
|
|
|
|
|
feeds.addObjects(from: (oneFolder as! LocalFolder).flattenedFeeds.allObjects)
|
|
|
|
|
}
|
|
|
|
|
return feeds
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var flattenedFeedIDs: Set<String> {
|
|
|
|
|
get {
|
|
|
|
|
return Set(flattenedFeeds.flatMap { ($0 as? LocalFeed)?.feedID })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var account: Account? {
|
|
|
|
|
get {
|
|
|
|
|
return self
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var unreadCount = 0 {
|
|
|
|
|
didSet {
|
|
|
|
|
postUnreadCountDidChangeNotification()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var plist: AnyObject? {
|
|
|
|
|
get {
|
|
|
|
|
return createDiskDictionary()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public var refreshInProgress: Bool {
|
|
|
|
|
get {
|
|
|
|
|
return !refresher.progress.isComplete
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
required public init(settingsFile: String, dataFolder: String, identifier: String) {
|
|
|
|
|
|
|
|
|
|
self.settingsFile = settingsFile
|
|
|
|
|
self.dataFolder = dataFolder
|
|
|
|
|
self.identifier = identifier
|
|
|
|
|
|
|
|
|
|
let databaseFile = (dataFolder as NSString).appendingPathComponent("Articles0.db")
|
|
|
|
|
self.localDatabase = LocalDatabase(databaseFile: databaseFile)
|
|
|
|
|
self.diskSaver = DiskSaver(path: settingsFile)
|
|
|
|
|
|
|
|
|
|
self.localDatabase.account = self
|
|
|
|
|
self.diskSaver.delegate = self
|
|
|
|
|
self.refresher.account = self
|
|
|
|
|
|
|
|
|
|
pullSettingsAndTopLevelItemsFromFile()
|
|
|
|
|
|
|
|
|
|
self.localDatabase.startup()
|
|
|
|
|
|
|
|
|
|
updateUnreadCountsForTopLevelFolders()
|
|
|
|
|
updateUnreadCount()
|
|
|
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(folderChildrenDidChange(_:)), name: NSNotification.Name(rawValue: FolderChildrenDidChangeNotification), object: nil)
|
|
|
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(articleStatusesDidChange(_:)), name: .ArticleStatusesDidChange, object: nil)
|
|
|
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
|
|
|
|
|
2017-05-29 21:22:06 +02:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(refreshProgressDidChange(_:)), name: .DownloadProgressDidChange, object: nil)
|
2017-05-23 22:24:42 +02:00
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async() { () -> Void in
|
|
|
|
|
self.updateUnreadCounts(feedIDs: self.flattenedFeedIDs)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public init?(plist: AnyObject) {
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Account
|
|
|
|
|
|
|
|
|
|
public func refreshAll() {
|
|
|
|
|
|
|
|
|
|
refresher.refreshFeeds(flattenedFeeds)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func markArticles(_ articles: NSSet, statusKey: ArticleStatusKey, flag: Bool) {
|
|
|
|
|
|
|
|
|
|
if statusKey == .read {
|
|
|
|
|
for oneArticle in articles {
|
|
|
|
|
if let oneArticle = oneArticle as? LocalArticle, let oneFeed = existingFeedWithID(oneArticle.feedID) as? LocalFeed {
|
|
|
|
|
oneFeed.addToUnreadCount(amount: flag ? -1 : 1)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
localDatabase.markArticles(articles, statusKey: statusKey, flag: flag)
|
|
|
|
|
postArticleStatusesDidChangeNotification(articles)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func importOPML(_ opmlDocument: Any) {
|
|
|
|
|
|
2017-07-02 02:22:19 +02:00
|
|
|
|
if let opmlItems = (opmlDocument as? RSOPMLDocument)?.children {
|
2017-05-23 22:24:42 +02:00
|
|
|
|
performDataModelBatchUpdates {
|
|
|
|
|
importOPMLItems(opmlItems)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
refreshAll()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func fetchArticles(for objects: [AnyObject]) -> [Article] {
|
|
|
|
|
|
|
|
|
|
var articlesSet = Set<LocalArticle>()
|
|
|
|
|
|
|
|
|
|
for oneObject in objects {
|
|
|
|
|
|
|
|
|
|
if let oneFeed = oneObject as? LocalFeed {
|
|
|
|
|
articlesSet.formUnion(fetchArticlesForFeed(oneFeed))
|
|
|
|
|
}
|
|
|
|
|
else if let oneFolder = oneObject as? LocalFolder {
|
|
|
|
|
articlesSet.formUnion(fetchArticlesForFolder(oneFolder))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return Array(articlesSet)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Folder
|
|
|
|
|
|
|
|
|
|
public func fetchArticles() -> [Article] {
|
|
|
|
|
|
|
|
|
|
return [Article]() // Shouldn’t get called.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func canAddItem(_ item: AnyObject) -> Bool {
|
|
|
|
|
|
|
|
|
|
return item is Feed || item is Folder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func addItem(_ item: AnyObject) -> Bool {
|
|
|
|
|
|
|
|
|
|
if !canAddItem(item) {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let feed = item as? LocalFeed {
|
|
|
|
|
return addFeed(feed)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let folder = item as? LocalFolder {
|
|
|
|
|
return addFolder(folder)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func canAddFolderWithName(_ folderName: String) -> Bool {
|
|
|
|
|
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func ensureFolderWithName(_ folderName: String) -> Folder? {
|
|
|
|
|
|
|
|
|
|
if let folder = existingFolderWithName(folderName) {
|
|
|
|
|
return folder
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let folder = LocalFolder(nameForDisplay: folderName, account: self)
|
|
|
|
|
if addItem(folder) {
|
|
|
|
|
return folder
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func createFeedWithName(_ name: String?, editedName: String?, urlString: String) -> Feed? {
|
|
|
|
|
|
|
|
|
|
let feed = LocalFeed(account: self, url: urlString, feedID: urlString)
|
|
|
|
|
feed.name = name
|
|
|
|
|
feed.editedName = editedName
|
|
|
|
|
return feed
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func deleteItems(_ items: [AnyObject]) {
|
|
|
|
|
|
|
|
|
|
items.forEach { deleteItem($0) }
|
|
|
|
|
FolderPostChildrenDidChangeNotification(self)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func existingFeedWithID(_ feedID: String) -> Feed? {
|
|
|
|
|
|
|
|
|
|
return existingFeedWithURL(feedID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func existingFeedWithURL(_ urlString: String) -> Feed? {
|
|
|
|
|
|
|
|
|
|
if let feed = topLevelFeeds[urlString] as? Feed {
|
|
|
|
|
return feed
|
|
|
|
|
}
|
|
|
|
|
for oneFolder in topLevelFolders.allValues {
|
|
|
|
|
if let oneFolder = oneFolder as? LocalFolder {
|
|
|
|
|
if let feed = oneFolder.existingFeedWithURL(urlString) {
|
|
|
|
|
return feed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: UnreadCountProvider
|
|
|
|
|
|
|
|
|
|
public func updateUnreadCount() {
|
|
|
|
|
|
|
|
|
|
var updatedUnreadCount = 0
|
|
|
|
|
let _ = visitObjects(false) { (oneChild) -> Bool in
|
|
|
|
|
|
|
|
|
|
if let oneUnreadCountProvider = oneChild as? UnreadCountProvider {
|
|
|
|
|
updatedUnreadCount += oneUnreadCountProvider.unreadCount
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if updatedUnreadCount != unreadCount {
|
|
|
|
|
unreadCount = updatedUnreadCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateUnreadCountForFeed(_ feed: LocalFeed) {
|
|
|
|
|
|
|
|
|
|
updateUnreadCounts(feedIDs: [feed.feedID])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func visitObjects(_ recurse: Bool, visitBlock: FolderVisitBlock) -> Bool {
|
|
|
|
|
|
|
|
|
|
for oneFeed in topLevelFeeds.allValues {
|
|
|
|
|
if visitBlock(oneFeed as AnyObject) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for oneFolder in topLevelFolders.allValues {
|
|
|
|
|
|
|
|
|
|
if visitBlock(oneFolder as AnyObject) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if recurse {
|
|
|
|
|
if let oneFolder = oneFolder as? Folder {
|
|
|
|
|
if oneFolder.visitObjects(recurse, visitBlock: visitBlock) {
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Notifications
|
|
|
|
|
|
|
|
|
|
dynamic func folderChildrenDidChange(_ note: Notification) {
|
|
|
|
|
|
|
|
|
|
if let _ = note.object as? LocalAccount {
|
|
|
|
|
diskSaver.dirty = true
|
|
|
|
|
}
|
|
|
|
|
else if let obj = note.object, objectIsDescendant(obj as AnyObject) {
|
|
|
|
|
diskSaver.dirty = true
|
|
|
|
|
}
|
|
|
|
|
updateUnreadCount()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dynamic func articleStatusesDidChange(_ note: Notification) {
|
|
|
|
|
|
|
|
|
|
guard let articles = note.userInfo?[articlesKey] as? NSSet else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var feedIDs = Set<String>()
|
|
|
|
|
for oneArticle in articles {
|
|
|
|
|
if let oneLocalArticle = oneArticle as? LocalArticle {
|
|
|
|
|
feedIDs.insert(oneLocalArticle.feedID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if feedIDs.isEmpty {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
updateUnreadCounts(feedIDs: feedIDs)
|
|
|
|
|
diskSaver.dirty = true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dynamic func unreadCountDidChange(_ notification: Notification) {
|
|
|
|
|
|
|
|
|
|
guard let obj = notification.object else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if obj is LocalFeed || obj is LocalFolder || obj is LocalAccount {
|
|
|
|
|
diskSaver.dirty = true
|
|
|
|
|
}
|
|
|
|
|
updateUnreadCount()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
dynamic func refreshProgressDidChange(_ notification: Notification) {
|
|
|
|
|
|
2017-05-26 22:07:55 +02:00
|
|
|
|
guard let progress = notification.object as? DownloadProgress, progress === refresher.progress else {
|
2017-05-23 22:24:42 +02:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
NotificationCenter.default.post(name: .AccountRefreshProgressDidChange, object: self, userInfo: [progressKey: progress])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Private
|
|
|
|
|
|
|
|
|
|
private func addFeed(_ feed: LocalFeed) -> Bool {
|
|
|
|
|
|
|
|
|
|
topLevelFeeds[feed.feedID] = feed
|
|
|
|
|
FolderPostChildrenDidChangeNotification(self)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func addFolder(_ folder: LocalFolder) -> Bool {
|
|
|
|
|
|
|
|
|
|
topLevelFolders[folder.folderID] = folder
|
|
|
|
|
FolderPostChildrenDidChangeNotification(self)
|
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Fetching
|
|
|
|
|
|
|
|
|
|
func fetchArticlesForFeed(_ feed: LocalFeed) -> Set<LocalArticle> {
|
|
|
|
|
|
|
|
|
|
return localDatabase.fetchArticlesForFeed(feed)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func fetchArticlesForFolder(_ folder: LocalFolder) -> Set<LocalArticle> {
|
|
|
|
|
|
|
|
|
|
return localDatabase.fetchUnreadArticlesForFolder(folder)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Updating
|
|
|
|
|
|
2017-07-02 02:22:19 +02:00
|
|
|
|
func update(_ feed: LocalFeed, parsedFeed: ParsedFeed, completionHandler: @escaping RSVoidCompletionBlock) {
|
2017-05-23 22:24:42 +02:00
|
|
|
|
|
|
|
|
|
if let titleFromFeed = parsedFeed.title {
|
|
|
|
|
if feed.name != titleFromFeed {
|
|
|
|
|
feed.name = titleFromFeed
|
|
|
|
|
self.diskSaver.dirty = true
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-07-02 02:22:19 +02:00
|
|
|
|
if let linkFromFeed = parsedFeed.homePageURL {
|
2017-05-23 22:24:42 +02:00
|
|
|
|
if feed.homePageURL != linkFromFeed {
|
|
|
|
|
feed.homePageURL = linkFromFeed
|
|
|
|
|
self.diskSaver.dirty = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
localDatabase.updateFeedWithParsedFeed(feed, parsedFeed: parsedFeed) {
|
|
|
|
|
|
|
|
|
|
feed.updateUnreadCount()
|
|
|
|
|
completionHandler()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Writing to Disk
|
|
|
|
|
|
|
|
|
|
private func createDiskDictionary() -> NSDictionary {
|
|
|
|
|
|
|
|
|
|
let d = NSMutableDictionary()
|
|
|
|
|
|
|
|
|
|
let diskChildren = NSMutableArray()
|
|
|
|
|
topLevelFolders.allValues.forEach { (oneFolder) in
|
|
|
|
|
if let oneFolder = oneFolder as? PlistProvider, let onePlist = oneFolder.plist {
|
|
|
|
|
diskChildren.add(onePlist)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
topLevelFeeds.allValues.forEach { (oneFeed) in
|
|
|
|
|
if let oneFeed = oneFeed as? PlistProvider, let onePlist = oneFeed.plist {
|
|
|
|
|
diskChildren.add(onePlist)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
d.setObject(diskChildren as NSArray, forKey: diskDictionaryChildrenKey as NSString)
|
|
|
|
|
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Reading from Disk
|
|
|
|
|
|
|
|
|
|
private func pullSettingsAndTopLevelItemsFromFile() {
|
|
|
|
|
|
|
|
|
|
guard let d = NSDictionary(contentsOfFile: settingsFile) else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
performDataModelBatchUpdates {
|
|
|
|
|
|
|
|
|
|
if let children = d[diskDictionaryChildrenKey] as? NSArray {
|
|
|
|
|
pullTopLevelItemsFromArray(children)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func objectWithDiskDictionary(_ d: NSDictionary) -> AnyObject? {
|
|
|
|
|
|
|
|
|
|
if let _ = d[feedURLKey] {
|
|
|
|
|
return LocalFeed(account: self, diskDictionary: d)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let _ = d[folderIDKey] {
|
|
|
|
|
return LocalFolder(account: self, diskDictionary: d)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func childrenForDiskArray(_ children: NSArray) -> [Any] {
|
|
|
|
|
|
|
|
|
|
var items = [Any]()
|
|
|
|
|
|
|
|
|
|
children.forEach { (oneChild) in
|
|
|
|
|
|
|
|
|
|
guard let oneDictionary = oneChild as? NSDictionary else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let oneObject = objectWithDiskDictionary(oneDictionary) {
|
|
|
|
|
items.append(oneObject)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return items
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func pullTopLevelItemsFromArray(_ children: NSArray) {
|
|
|
|
|
|
|
|
|
|
let items = childrenForDiskArray(children)
|
|
|
|
|
|
|
|
|
|
items.forEach { (oneItem) in
|
|
|
|
|
|
|
|
|
|
if let oneFolder = oneItem as? LocalFolder {
|
|
|
|
|
topLevelFolders[oneFolder.folderID] = oneFolder
|
|
|
|
|
}
|
|
|
|
|
else if let oneFeed = oneItem as? LocalFeed {
|
|
|
|
|
topLevelFeeds[oneFeed.feedID] = oneFeed
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: OPML Export
|
|
|
|
|
|
|
|
|
|
public func opmlString(indentLevel: Int) -> String {
|
|
|
|
|
|
|
|
|
|
var s = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
|
|
|
|
s += "<opml version=\"1.1\">\n"
|
|
|
|
|
s += "<head>\n"
|
|
|
|
|
s += "\t<title>mySubscriptions</title>\n"
|
|
|
|
|
s += "\t</head>\n"
|
|
|
|
|
s += "\t<body>\n"
|
|
|
|
|
|
|
|
|
|
let indentLevel = 1
|
|
|
|
|
|
|
|
|
|
let _ = visitChildren { (oneChild) -> Bool in
|
|
|
|
|
if let oneFolder = oneChild as? LocalFolder {
|
|
|
|
|
s += oneFolder.opmlString(indentLevel: indentLevel)
|
|
|
|
|
}
|
|
|
|
|
else if let oneFeed = oneChild as? LocalFeed {
|
|
|
|
|
s += oneFeed.opmlString(indentLevel: indentLevel)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s += "\t</body>\n"
|
|
|
|
|
s += "</opml>\n"
|
|
|
|
|
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private extension LocalAccount {
|
|
|
|
|
|
|
|
|
|
// MARK: Deleting
|
|
|
|
|
|
|
|
|
|
func deleteItem(_ item: AnyObject) {
|
|
|
|
|
|
|
|
|
|
if let feed = item as? LocalFeed {
|
|
|
|
|
deleteFeed(feed)
|
|
|
|
|
}
|
|
|
|
|
else if let folder = item as? LocalFolder {
|
|
|
|
|
deleteFolder(folder)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func deleteFeed(_ feed: LocalFeed) {
|
|
|
|
|
|
|
|
|
|
topLevelFeeds[feed.feedID] = nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func deleteFolder(_ folder: LocalFolder) {
|
|
|
|
|
|
|
|
|
|
topLevelFolders[folder.folderID] = nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Unread Counts
|
|
|
|
|
|
|
|
|
|
func updateUnreadCountsForFeeds(_ feeds: Set<LocalFeed>) {
|
|
|
|
|
|
|
|
|
|
let feedIDs = feeds.map { $0.feedID }
|
|
|
|
|
updateUnreadCounts(feedIDs: Set(feedIDs))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateUnreadCountsForTopLevelFolders() {
|
|
|
|
|
|
|
|
|
|
topLevelFolders.allValues.forEach { (oneFolder) in
|
|
|
|
|
if let oneFolder = oneFolder as? UnreadCountProvider {
|
|
|
|
|
oneFolder.updateUnreadCount()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: OPML Import
|
|
|
|
|
|
|
|
|
|
func importOPMLItems(_ items: [RSOPMLItem]) {
|
|
|
|
|
|
|
|
|
|
// FeedBin’s OPML duplicates everything in a folder onto the top level.
|
|
|
|
|
// So: do the folders first, then the top level feeds.
|
|
|
|
|
|
|
|
|
|
importOPMLTopLevelFolders(items)
|
|
|
|
|
importOPMLTopLevelFeeds(items)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func importOPMLTopLevelFolders(_ items: [RSOPMLItem]) {
|
|
|
|
|
|
|
|
|
|
for oneItem in items {
|
|
|
|
|
|
2017-07-02 02:22:19 +02:00
|
|
|
|
if oneItem.isFolder, let childItems = oneItem.children {
|
2017-05-23 22:24:42 +02:00
|
|
|
|
importOPMLTopLevelFolder(oneItem, childItems)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func importOPMLTopLevelFeeds(_ items: [RSOPMLItem]) {
|
|
|
|
|
|
|
|
|
|
for oneItem in items {
|
|
|
|
|
|
|
|
|
|
if !oneItem.isFolder {
|
|
|
|
|
importOPMLFeedIntoFolder(oneItem, nil)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func importOPMLTopLevelFolder(_ opmlFolder: RSOPMLItem, _ items: [RSOPMLItem]) {
|
|
|
|
|
|
|
|
|
|
let folderTitle = opmlFolder.titleFromAttributes ?? "Untitled"
|
|
|
|
|
let folder = ensureFolderWithName(folderTitle)! as! LocalFolder
|
|
|
|
|
importOPMLItemsIntoFolder(items, folder)
|
|
|
|
|
let _ = addItem(folder)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func importOPMLItemsIntoFolder(_ items: [RSOPMLItem], _ folder: LocalFolder) {
|
|
|
|
|
|
|
|
|
|
// nil folder for top level.
|
|
|
|
|
|
|
|
|
|
for oneItem in items {
|
|
|
|
|
|
2017-07-02 02:22:19 +02:00
|
|
|
|
if oneItem.isFolder, let childItems = oneItem.children {
|
2017-05-23 22:24:42 +02:00
|
|
|
|
importOPMLItemsIntoFolder(childItems, folder)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
else {
|
|
|
|
|
importOPMLFeedIntoFolder(oneItem, folder)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func importOPMLFeedIntoFolder(_ opmlFeed: RSOPMLItem, _ folder: LocalFolder?) {
|
|
|
|
|
|
|
|
|
|
guard let feedSpecifier = opmlFeed.opmlFeedSpecifier, let feedURL = feedSpecifier.feedURL else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let _ = existingFeedWithURL(feedURL) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let feed = LocalFeed(account: self, url: feedURL, feedID: feedURL)
|
|
|
|
|
if let name = feedSpecifier.title {
|
|
|
|
|
feed.editedName = name
|
|
|
|
|
}
|
|
|
|
|
if let folder = folder {
|
|
|
|
|
let _ = folder.addItem(feed)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
let _ = addItem(feed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: Unread Counts
|
|
|
|
|
|
|
|
|
|
func updateUnreadCountsWithDatabaseDictionary(_ unreadCountsDictionary: [String: Int]) {
|
|
|
|
|
|
|
|
|
|
for oneFeed in flattenedFeeds {
|
|
|
|
|
|
|
|
|
|
guard let oneFeed = oneFeed as? LocalFeed, let unreadCount = unreadCountsDictionary[oneFeed.feedID] else {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if oneFeed.unreadCount != unreadCount {
|
|
|
|
|
oneFeed.unreadCount = unreadCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateUnreadCountsForTopLevelFolders()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateUnreadCounts(feedIDs: Set<String>) {
|
|
|
|
|
|
|
|
|
|
self.localDatabase.updateUnreadCounts(for: Set(feedIDs), completion: { (unreadCounts) in
|
|
|
|
|
|
|
|
|
|
self.updateUnreadCountsWithDatabaseDictionary(unreadCounts)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|