2017-09-17 00:25:38 +02:00
|
|
|
|
//
|
|
|
|
|
// Folder.swift
|
|
|
|
|
// DataModel
|
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 7/1/17.
|
|
|
|
|
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
|
|
|
|
import Data
|
2017-11-04 22:53:21 +01:00
|
|
|
|
import RSCore
|
2017-09-17 00:25:38 +02:00
|
|
|
|
|
2017-11-05 06:51:14 +01:00
|
|
|
|
public final class Folder: DisplayNameProvider, Container, UnreadCountProvider, Hashable {
|
2017-11-04 23:27:32 +01:00
|
|
|
|
|
2017-10-22 01:37:40 +02:00
|
|
|
|
public weak var account: Account?
|
2017-10-08 06:41:21 +02:00
|
|
|
|
public var children = [AnyObject]()
|
2018-01-11 09:51:25 +01:00
|
|
|
|
public private(set) var name: String?
|
2017-09-28 22:16:47 +02:00
|
|
|
|
static let untitledName = NSLocalizedString("Untitled ƒ", comment: "Folder name")
|
2017-11-05 03:03:47 +01:00
|
|
|
|
public let folderID: Int // not saved: per-run only
|
|
|
|
|
static var incrementingID = 0
|
2017-11-04 23:27:32 +01:00
|
|
|
|
public let hashValue: Int
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
2017-10-09 03:58:15 +02:00
|
|
|
|
// MARK: - Fetching Articles
|
|
|
|
|
|
|
|
|
|
public func fetchArticles() -> Set<Article> {
|
2017-10-22 01:37:40 +02:00
|
|
|
|
|
|
|
|
|
guard let account = account else {
|
|
|
|
|
return Set<Article>()
|
|
|
|
|
}
|
2017-10-09 03:58:15 +02:00
|
|
|
|
return account.fetchArticles(folder: self)
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 00:25:38 +02:00
|
|
|
|
// MARK: - DisplayNameProvider
|
|
|
|
|
|
2017-09-27 22:29:05 +02:00
|
|
|
|
public var nameForDisplay: String {
|
|
|
|
|
get {
|
2017-09-28 22:16:47 +02:00
|
|
|
|
return name ?? Folder.untitledName
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-09-17 00:25:38 +02:00
|
|
|
|
|
|
|
|
|
// MARK: - UnreadCountProvider
|
|
|
|
|
|
|
|
|
|
public var unreadCount = 0 {
|
|
|
|
|
didSet {
|
|
|
|
|
if unreadCount != oldValue {
|
|
|
|
|
postUnreadCountDidChangeNotification()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Init
|
|
|
|
|
|
2017-09-28 22:34:16 +02:00
|
|
|
|
init(account: Account, name: String?) {
|
2017-09-17 00:25:38 +02:00
|
|
|
|
|
2017-09-28 15:53:01 +02:00
|
|
|
|
self.account = account
|
|
|
|
|
self.name = name
|
2017-11-05 03:03:47 +01:00
|
|
|
|
|
|
|
|
|
let folderID = Folder.incrementingID
|
|
|
|
|
Folder.incrementingID += 1
|
|
|
|
|
self.folderID = folderID
|
|
|
|
|
self.hashValue = folderID
|
|
|
|
|
|
2017-10-13 15:58:15 +02:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
2017-09-17 00:25:38 +02:00
|
|
|
|
}
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
|
|
|
|
// MARK: - Disk Dictionary
|
|
|
|
|
|
2017-09-30 20:00:18 +02:00
|
|
|
|
private struct Key {
|
2017-09-27 22:29:05 +02:00
|
|
|
|
static let name = "name"
|
|
|
|
|
static let unreadCount = "unreadCount"
|
2017-09-28 22:16:47 +02:00
|
|
|
|
static let children = "children"
|
2017-09-27 22:29:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-30 20:00:18 +02:00
|
|
|
|
convenience init?(account: Account, dictionary: [String: Any]) {
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
2017-09-28 22:34:16 +02:00
|
|
|
|
let name = dictionary[Key.name] as? String
|
2017-09-28 22:16:47 +02:00
|
|
|
|
self.init(account: account, name: name)
|
2017-09-28 22:34:16 +02:00
|
|
|
|
|
|
|
|
|
if let childrenArray = dictionary[Key.children] as? [[String: Any]] {
|
|
|
|
|
self.children = Folder.objects(with: childrenArray, account: account)
|
2017-09-27 22:29:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if let savedUnreadCount = dictionary[Key.unreadCount] as? Int {
|
|
|
|
|
self.unreadCount = savedUnreadCount
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-30 20:00:18 +02:00
|
|
|
|
var dictionary: [String: Any] {
|
2017-09-27 22:29:05 +02:00
|
|
|
|
get {
|
2017-10-22 01:37:40 +02:00
|
|
|
|
|
2017-09-27 22:29:05 +02:00
|
|
|
|
var d = [String: Any]()
|
2017-10-22 01:37:40 +02:00
|
|
|
|
guard let account = account else {
|
|
|
|
|
return d
|
|
|
|
|
}
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
|
|
|
|
if let name = name {
|
|
|
|
|
d[Key.name] = name
|
|
|
|
|
}
|
|
|
|
|
if unreadCount > 0 {
|
|
|
|
|
d[Key.unreadCount] = unreadCount
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let childObjects = children.flatMap { (child) -> [String: Any]? in
|
|
|
|
|
|
|
|
|
|
if let feed = child as? Feed {
|
2017-09-28 22:16:47 +02:00
|
|
|
|
return feed.dictionary
|
2017-09-27 22:29:05 +02:00
|
|
|
|
}
|
2017-09-28 22:16:47 +02:00
|
|
|
|
if let folder = child as? Folder, account.supportsSubFolders {
|
|
|
|
|
return folder.dictionary
|
|
|
|
|
}
|
|
|
|
|
assertionFailure("Expected a feed or a folder.");
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !childObjects.isEmpty {
|
|
|
|
|
d[Key.children] = childObjects
|
2017-09-27 22:29:05 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return d
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-10-01 19:59:35 +02:00
|
|
|
|
|
|
|
|
|
// MARK: Feeds
|
|
|
|
|
|
|
|
|
|
func addFeed(_ feed: Feed) -> Bool {
|
|
|
|
|
|
|
|
|
|
// Return true in the case where the feed is already a child.
|
|
|
|
|
|
2017-10-19 03:13:49 +02:00
|
|
|
|
if !childrenContain(feed) {
|
|
|
|
|
children += [feed]
|
2017-10-22 01:27:06 +02:00
|
|
|
|
postChildrenDidChangeNotification()
|
2017-10-01 19:59:35 +02:00
|
|
|
|
}
|
|
|
|
|
return true
|
|
|
|
|
}
|
2017-11-05 06:51:14 +01:00
|
|
|
|
|
2017-10-13 15:58:15 +02:00
|
|
|
|
// MARK: Notifications
|
|
|
|
|
|
|
|
|
|
@objc func unreadCountDidChange(_ note: Notification) {
|
|
|
|
|
|
|
|
|
|
if let object = note.object {
|
|
|
|
|
if objectIsChild(object as AnyObject) {
|
|
|
|
|
updateUnreadCount()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-11-04 23:27:32 +01:00
|
|
|
|
// MARK: - Equatable
|
|
|
|
|
|
|
|
|
|
static public func ==(lhs: Folder, rhs: Folder) -> Bool {
|
|
|
|
|
|
|
|
|
|
return lhs === rhs
|
|
|
|
|
}
|
2017-10-01 19:59:35 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// MARK: - Private
|
|
|
|
|
|
|
|
|
|
private extension Folder {
|
2017-10-13 15:58:15 +02:00
|
|
|
|
|
|
|
|
|
func updateUnreadCount() {
|
|
|
|
|
|
|
|
|
|
unreadCount = calculateUnreadCount(children)
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-19 03:13:49 +02:00
|
|
|
|
func childrenContain(_ feed: Feed) -> Bool {
|
2017-10-01 19:59:35 +02:00
|
|
|
|
|
|
|
|
|
return children.contains(where: { (object) -> Bool in
|
2017-10-19 03:13:49 +02:00
|
|
|
|
if object === feed {
|
|
|
|
|
return true
|
|
|
|
|
}
|
2017-10-01 19:59:35 +02:00
|
|
|
|
if let oneFeed = object as? Feed {
|
|
|
|
|
if oneFeed.feedID == feed.feedID {
|
2017-10-19 03:13:49 +02:00
|
|
|
|
assertionFailure("Expected feeds to match by pointer equality rather than by feedID.")
|
2017-10-01 19:59:35 +02:00
|
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
})
|
|
|
|
|
}
|
2017-09-17 00:25:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-28 22:34:16 +02:00
|
|
|
|
// MARK: - Disk
|
|
|
|
|
|
|
|
|
|
private extension Folder {
|
|
|
|
|
|
|
|
|
|
static func objects(with diskObjects: [[String: Any]], account: Account) -> [AnyObject] {
|
|
|
|
|
|
|
|
|
|
if account.supportsSubFolders {
|
|
|
|
|
return account.objects(with: diskObjects)
|
|
|
|
|
}
|
|
|
|
|
else {
|
|
|
|
|
let flattenedFeeds = feedsOnly(with: diskObjects, account: account)
|
|
|
|
|
return Array(flattenedFeeds) as [AnyObject]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static func feedsOnly(with diskObjects: [[String: Any]], account: Account) -> Set<Feed> {
|
|
|
|
|
|
|
|
|
|
// This Folder doesn’t support subfolders, but they might exist on disk.
|
|
|
|
|
// (For instance: a user might manually edit the plist to add subfolders.)
|
|
|
|
|
// Create a flattened version of the feeds.
|
|
|
|
|
|
|
|
|
|
var feeds = Set<Feed>()
|
|
|
|
|
|
|
|
|
|
for diskObject in diskObjects {
|
|
|
|
|
|
|
|
|
|
if Feed.isFeedDictionary(diskObject) {
|
|
|
|
|
if let feed = Feed(accountID: account.accountID, dictionary: diskObject) {
|
|
|
|
|
feeds.insert(feed)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else { // Folder
|
|
|
|
|
if let subFolderChildren = diskObject[Key.children] as? [[String: Any]] {
|
|
|
|
|
let subFolderFeeds = feedsOnly(with: subFolderChildren, account: account)
|
|
|
|
|
feeds.formUnion(subFolderFeeds)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return feeds
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 00:25:38 +02:00
|
|
|
|
extension Folder: OPMLRepresentable {
|
|
|
|
|
|
|
|
|
|
public func OPMLString(indentLevel: Int) -> String {
|
|
|
|
|
|
|
|
|
|
let escapedTitle = nameForDisplay.rs_stringByEscapingSpecialXMLCharacters()
|
|
|
|
|
var s = "<outline text=\"\(escapedTitle)\" title=\"\(escapedTitle)\">\n"
|
|
|
|
|
s = s.rs_string(byPrependingNumberOfTabs: indentLevel)
|
|
|
|
|
|
|
|
|
|
var hasAtLeastOneChild = false
|
|
|
|
|
|
2017-10-08 06:41:21 +02:00
|
|
|
|
for child in children {
|
|
|
|
|
if let opmlObject = child as? OPMLRepresentable {
|
|
|
|
|
s += opmlObject.OPMLString(indentLevel: indentLevel + 1)
|
2017-09-17 00:25:38 +02:00
|
|
|
|
hasAtLeastOneChild = true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !hasAtLeastOneChild {
|
|
|
|
|
s = "<outline text=\"\(escapedTitle)\" title=\"\(escapedTitle)\"/>\n"
|
|
|
|
|
s = s.rs_string(byPrependingNumberOfTabs: indentLevel)
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s = s + NSString.rs_string(withNumberOfTabs: indentLevel) + "</outline>\n"
|
|
|
|
|
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|