2017-09-17 00:25:38 +02:00
|
|
|
|
//
|
|
|
|
|
// Folder.swift
|
2019-07-09 07:58:19 +02:00
|
|
|
|
// NetNewsWire
|
2017-09-17 00:25:38 +02:00
|
|
|
|
//
|
|
|
|
|
// Created by Brent Simmons on 7/1/17.
|
|
|
|
|
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import Foundation
|
2018-07-24 03:29:08 +02:00
|
|
|
|
import Articles
|
2017-11-04 22:53:21 +01:00
|
|
|
|
import RSCore
|
2017-09-17 00:25:38 +02:00
|
|
|
|
|
2019-11-15 13:19:14 +01:00
|
|
|
|
public final class Folder: Feed, Renamable, Container, Hashable {
|
|
|
|
|
|
2019-11-22 16:40:39 +01:00
|
|
|
|
public var defaultReadFilterType: ReadFilterType {
|
2019-11-22 01:22:43 +01:00
|
|
|
|
return .read
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-25 01:29:00 +01:00
|
|
|
|
public var containerID: ContainerIdentifier? {
|
|
|
|
|
guard let accountID = account?.accountID else {
|
|
|
|
|
assertionFailure("Expected feed.account, but got nil.")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return ContainerIdentifier.folder(accountID, nameForDisplay)
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 13:19:14 +01:00
|
|
|
|
public var feedID: FeedIdentifier? {
|
|
|
|
|
guard let accountID = account?.accountID else {
|
|
|
|
|
assertionFailure("Expected feed.account, but got nil.")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
return FeedIdentifier.folder(accountID, nameForDisplay)
|
|
|
|
|
}
|
2017-11-04 23:27:32 +01:00
|
|
|
|
|
2018-09-17 02:54:42 +02:00
|
|
|
|
public weak var account: Account?
|
2019-11-15 03:11:41 +01:00
|
|
|
|
public var topLevelWebFeeds: Set<WebFeed> = Set<WebFeed>()
|
2018-09-17 02:54:42 +02:00
|
|
|
|
public var folders: Set<Folder>? = nil // subfolders are not supported, so this is always nil
|
|
|
|
|
|
2018-01-23 06:59:13 +01:00
|
|
|
|
public var name: String? {
|
|
|
|
|
didSet {
|
|
|
|
|
postDisplayNameDidChangeNotification()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
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
|
2019-09-26 00:01:09 +02:00
|
|
|
|
public var externalID: String? = nil
|
2017-11-05 03:03:47 +01:00
|
|
|
|
static var incrementingID = 0
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
2017-09-17 00:25:38 +02:00
|
|
|
|
// MARK: - DisplayNameProvider
|
|
|
|
|
|
2017-09-27 22:29:05 +02:00
|
|
|
|
public var nameForDisplay: String {
|
2018-02-14 22:14:25 +01:00
|
|
|
|
return name ?? Folder.untitledName
|
2017-09-27 22:29:05 +02:00
|
|
|
|
}
|
2019-10-03 16:53:21 +02:00
|
|
|
|
|
2017-09-17 00:25:38 +02:00
|
|
|
|
// MARK: - UnreadCountProvider
|
|
|
|
|
|
|
|
|
|
public var unreadCount = 0 {
|
|
|
|
|
didSet {
|
|
|
|
|
if unreadCount != oldValue {
|
|
|
|
|
postUnreadCountDidChangeNotification()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-22 22:57:49 +01:00
|
|
|
|
// MARK: - Renamable
|
|
|
|
|
|
2019-05-06 17:53:20 +02:00
|
|
|
|
public func rename(to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
|
|
|
guard let account = account else { return }
|
|
|
|
|
account.renameFolder(self, to: name, completion: completion)
|
2018-11-22 22:57:49 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-09-17 00:25:38 +02:00
|
|
|
|
// MARK: - Init
|
|
|
|
|
|
2017-09-28 22:34:16 +02:00
|
|
|
|
init(account: Account, name: String?) {
|
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
|
|
|
|
|
|
2017-10-13 15:58:15 +02:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
2018-02-13 07:22:06 +01:00
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(childrenDidChange(_:)), name: .ChildrenDidChange, object: self)
|
2017-09-17 00:25:38 +02:00
|
|
|
|
}
|
2017-09-27 22:29:05 +02:00
|
|
|
|
|
2018-02-13 07:22:06 +01:00
|
|
|
|
// MARK: - Notifications
|
2017-10-13 15:58:15 +02:00
|
|
|
|
|
|
|
|
|
@objc func unreadCountDidChange(_ note: Notification) {
|
|
|
|
|
if let object = note.object {
|
|
|
|
|
if objectIsChild(object as AnyObject) {
|
|
|
|
|
updateUnreadCount()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2018-02-13 07:22:06 +01:00
|
|
|
|
@objc func childrenDidChange(_ note: Notification) {
|
|
|
|
|
updateUnreadCount()
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-17 02:54:42 +02:00
|
|
|
|
// MARK: Container
|
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
public func flattenedWebFeeds() -> Set<WebFeed> {
|
2018-09-17 02:54:42 +02:00
|
|
|
|
// Since sub-folders are not supported, it’s always the top-level feeds.
|
2019-11-15 03:11:41 +01:00
|
|
|
|
return topLevelWebFeeds
|
2018-09-17 02:54:42 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public func objectIsChild(_ object: AnyObject) -> Bool {
|
|
|
|
|
// Folders contain Feed objects only, at least for now.
|
2019-11-15 03:11:41 +01:00
|
|
|
|
guard let feed = object as? WebFeed else {
|
2018-09-17 02:54:42 +02:00
|
|
|
|
return false
|
|
|
|
|
}
|
2019-11-15 03:11:41 +01:00
|
|
|
|
return topLevelWebFeeds.contains(feed)
|
2018-09-17 02:54:42 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
public func addWebFeed(_ feed: WebFeed) {
|
|
|
|
|
topLevelWebFeeds.insert(feed)
|
2018-09-17 02:54:42 +02:00
|
|
|
|
postChildrenDidChangeNotification()
|
|
|
|
|
}
|
2019-05-09 20:31:18 +02:00
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
public func addFeeds(_ feeds: Set<WebFeed>) {
|
2019-10-03 10:45:16 +02:00
|
|
|
|
guard !feeds.isEmpty else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-11-15 03:11:41 +01:00
|
|
|
|
topLevelWebFeeds.formUnion(feeds)
|
2019-10-03 10:45:16 +02:00
|
|
|
|
postChildrenDidChangeNotification()
|
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
public func removeWebFeed(_ feed: WebFeed) {
|
|
|
|
|
topLevelWebFeeds.remove(feed)
|
2019-05-09 20:31:18 +02:00
|
|
|
|
postChildrenDidChangeNotification()
|
2018-09-17 02:54:42 +02:00
|
|
|
|
}
|
2019-10-03 05:30:43 +02:00
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
public func removeFeeds(_ feeds: Set<WebFeed>) {
|
2019-10-03 05:30:43 +02:00
|
|
|
|
guard !feeds.isEmpty else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2019-11-15 03:11:41 +01:00
|
|
|
|
topLevelWebFeeds.subtract(feeds)
|
2019-10-03 05:30:43 +02:00
|
|
|
|
postChildrenDidChangeNotification()
|
|
|
|
|
}
|
2019-08-22 06:05:47 +02:00
|
|
|
|
|
2018-08-25 20:54:58 +02:00
|
|
|
|
// MARK: - Hashable
|
|
|
|
|
|
|
|
|
|
public func hash(into hasher: inout Hasher) {
|
|
|
|
|
hasher.combine(folderID)
|
|
|
|
|
}
|
|
|
|
|
|
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() {
|
2018-09-17 02:54:42 +02:00
|
|
|
|
var updatedUnreadCount = 0
|
2019-11-15 03:11:41 +01:00
|
|
|
|
for feed in topLevelWebFeeds {
|
2018-09-17 02:54:42 +02:00
|
|
|
|
updatedUnreadCount += feed.unreadCount
|
|
|
|
|
}
|
|
|
|
|
unreadCount = updatedUnreadCount
|
2017-10-13 15:58:15 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
func childrenContain(_ feed: WebFeed) -> Bool {
|
|
|
|
|
return topLevelWebFeeds.contains(feed)
|
2017-10-01 19:59:35 +02:00
|
|
|
|
}
|
2017-09-17 00:25:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
2019-03-15 21:27:01 +01:00
|
|
|
|
// MARK: - OPMLRepresentable
|
2017-09-28 22:34:16 +02:00
|
|
|
|
|
2017-09-17 00:25:38 +02:00
|
|
|
|
extension Folder: OPMLRepresentable {
|
|
|
|
|
|
2019-09-26 00:01:09 +02:00
|
|
|
|
public func OPMLString(indentLevel: Int, strictConformance: Bool) -> String {
|
|
|
|
|
|
|
|
|
|
let attrExternalID: String = {
|
|
|
|
|
if !strictConformance, let externalID = externalID {
|
|
|
|
|
return " nnw_externalID=\"\(externalID)\""
|
|
|
|
|
} else {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
2017-09-17 00:25:38 +02:00
|
|
|
|
let escapedTitle = nameForDisplay.rs_stringByEscapingSpecialXMLCharacters()
|
2019-09-26 00:01:09 +02:00
|
|
|
|
var s = "<outline text=\"\(escapedTitle)\" title=\"\(escapedTitle)\"\(attrExternalID)>\n"
|
2017-09-17 00:25:38 +02:00
|
|
|
|
s = s.rs_string(byPrependingNumberOfTabs: indentLevel)
|
|
|
|
|
|
|
|
|
|
var hasAtLeastOneChild = false
|
|
|
|
|
|
2019-11-15 03:11:41 +01:00
|
|
|
|
for feed in topLevelWebFeeds {
|
2019-09-26 00:01:09 +02:00
|
|
|
|
s += feed.OPMLString(indentLevel: indentLevel + 1, strictConformance: strictConformance)
|
2018-09-17 02:54:42 +02:00
|
|
|
|
hasAtLeastOneChild = true
|
2017-09-17 00:25:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !hasAtLeastOneChild {
|
2019-09-26 00:01:09 +02:00
|
|
|
|
s = "<outline text=\"\(escapedTitle)\" title=\"\(escapedTitle)\"\(attrExternalID)/>\n"
|
2017-09-17 00:25:38 +02:00
|
|
|
|
s = s.rs_string(byPrependingNumberOfTabs: indentLevel)
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
s = s + NSString.rs_string(withNumberOfTabs: indentLevel) + "</outline>\n"
|
|
|
|
|
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|