-
Notifications
You must be signed in to change notification settings - Fork 535
/
Folder.swift
211 lines (161 loc) · 4.89 KB
/
Folder.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
//
// Folder.swift
// NetNewsWire
//
// Created by Brent Simmons on 7/1/17.
// Copyright © 2017 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import Articles
import Core
@MainActor public final class Folder: Renamable, Container, DisplayNameProvider, UnreadCountProvider, Hashable {
public var containerID: ContainerIdentifier? {
guard let accountID = account?.accountID else {
assertionFailure("Expected feed.account, but got nil.")
return nil
}
return ContainerIdentifier.folder(accountID, nameForDisplay)
}
public weak var account: Account?
public var topLevelFeeds: Set<Feed> = Set<Feed>()
public var folders: Set<Folder>? = nil // subfolders are not supported, so this is always nil
public var name: String? {
didSet {
postDisplayNameDidChangeNotification()
}
}
static let untitledName = NSLocalizedString("Untitled ƒ", comment: "Folder name")
public let folderID: Int // not saved: per-run only
public var externalID: String? = nil
static var incrementingID = 0
// MARK: - DisplayNameProvider
public var nameForDisplay: String {
return name ?? Folder.untitledName
}
// MARK: - UnreadCountProvider
public var unreadCount = 0 {
didSet {
if unreadCount != oldValue {
postUnreadCountDidChangeNotification()
}
}
}
// MARK: - Renamable
public func rename(to name: String) async throws {
guard let account else {
return
}
try await account.renameFolder(self, to: name)
}
// MARK: - Init
init(account: Account, name: String?) {
self.account = account
self.name = name
let folderID = Folder.incrementingID
Folder.incrementingID += 1
self.folderID = folderID
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(childrenDidChange(_:)), name: .ChildrenDidChange, object: self)
}
// MARK: - Notifications
@MainActor @objc func unreadCountDidChange(_ note: Notification) {
if let object = note.object {
if objectIsChild(object as AnyObject) {
updateUnreadCount()
}
}
}
@MainActor @objc func childrenDidChange(_ note: Notification) {
updateUnreadCount()
}
// MARK: Container
public func flattenedFeeds() -> Set<Feed> {
// Since sub-folders are not supported, it’s always the top-level feeds.
return topLevelFeeds
}
public func objectIsChild(_ object: AnyObject) -> Bool {
// Folders contain Feed objects only, at least for now.
guard let feed = object as? Feed else {
return false
}
return topLevelFeeds.contains(feed)
}
public func addFeed(_ feed: Feed) {
topLevelFeeds.insert(feed)
postChildrenDidChangeNotification()
}
public func addFeeds(_ feeds: Set<Feed>) {
guard !feeds.isEmpty else {
return
}
topLevelFeeds.formUnion(feeds)
postChildrenDidChangeNotification()
}
public func removeFeed(_ feed: Feed) {
topLevelFeeds.remove(feed)
postChildrenDidChangeNotification()
}
public func removeFeeds(_ feeds: Set<Feed>) {
guard !feeds.isEmpty else {
return
}
topLevelFeeds.subtract(feeds)
postChildrenDidChangeNotification()
}
// MARK: - Hashable
public func hash(into hasher: inout Hasher) {
hasher.combine(folderID)
}
// MARK: - Equatable
static public func ==(lhs: Folder, rhs: Folder) -> Bool {
return lhs === rhs
}
}
// MARK: - Private
private extension Folder {
@MainActor func updateUnreadCount() {
var updatedUnreadCount = 0
for feed in topLevelFeeds {
updatedUnreadCount += feed.unreadCount
}
unreadCount = updatedUnreadCount
}
func childrenContain(_ feed: Feed) -> Bool {
return topLevelFeeds.contains(feed)
}
}
// MARK: - OPMLRepresentable
extension Folder: OPMLRepresentable {
public func OPMLString(indentLevel: Int, allowCustomAttributes: Bool) -> String {
let attrExternalID: String = {
if allowCustomAttributes, let externalID = externalID {
return " nnw_externalID=\"\(externalID.escapingSpecialXMLCharacters)\""
} else {
return ""
}
}()
let escapedTitle = nameForDisplay.escapingSpecialXMLCharacters
var s = "<outline text=\"\(escapedTitle)\" title=\"\(escapedTitle)\"\(attrExternalID)>\n"
s = s.prepending(tabCount: indentLevel)
var hasAtLeastOneChild = false
for feed in topLevelFeeds.sorted() {
s += feed.OPMLString(indentLevel: indentLevel + 1, allowCustomAttributes: allowCustomAttributes)
hasAtLeastOneChild = true
}
if !hasAtLeastOneChild {
s = "<outline text=\"\(escapedTitle)\" title=\"\(escapedTitle)\"\(attrExternalID)/>\n"
s = s.prepending(tabCount: indentLevel)
return s
}
s = s + String(tabCount: indentLevel) + "</outline>\n"
return s
}
}
// MARK: Set
extension Set where Element == Folder {
@MainActor func sorted() -> Array<Folder> {
return sorted(by: { (folder1, folder2) -> Bool in
return folder1.nameForDisplay.localizedStandardCompare(folder2.nameForDisplay) == .orderedAscending
})
}
}