mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2025-02-02 20:16:54 +01:00
Merge branch 'master' into google_reader_compatible_syncing
This commit is contained in:
commit
98c32b9987
@ -62,6 +62,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
return defaultName
|
||||
}()
|
||||
|
||||
public var isDeleted = false
|
||||
|
||||
public var account: Account? {
|
||||
return self
|
||||
}
|
||||
public let accountID: String
|
||||
public let type: AccountType
|
||||
public var nameForDisplay: String {
|
||||
@ -179,6 +184,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
}
|
||||
}
|
||||
|
||||
public var usesTags: Bool {
|
||||
return delegate.usesTags
|
||||
}
|
||||
|
||||
var refreshInProgress = false {
|
||||
didSet {
|
||||
if refreshInProgress != oldValue {
|
||||
@ -406,16 +415,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
return feed
|
||||
}
|
||||
|
||||
func addFeed(container: Container, feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.addFeed(for: self, to: container, with: feed, completion: completion)
|
||||
public func addFeed(_ feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.addFeed(for: self, with: feed, to: container, completion: completion)
|
||||
}
|
||||
|
||||
func removeFeed(_ feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.removeFeed(for: self, from: container, with: feed, completion: completion)
|
||||
}
|
||||
|
||||
public func createFeed(url: String, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
delegate.createFeed(for: self, url: url, completion: completion)
|
||||
public func createFeed(url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
delegate.createFeed(for: self, url: url, name: name, container: container, completion: completion)
|
||||
}
|
||||
|
||||
func createFeed(with name: String?, url: String, feedID: String, homePageURL: String?) -> Feed {
|
||||
@ -429,21 +434,28 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
|
||||
}
|
||||
|
||||
public func deleteFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
feedMetadata[feed.url] = nil
|
||||
delegate.deleteFeed(for: self, with: feed, completion: completion)
|
||||
public func removeFeed(_ feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.removeFeed(for: self, with: feed, from: container, completion: completion)
|
||||
}
|
||||
|
||||
public func moveFeed(_ feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.moveFeed(for: self, with: feed, from: from, to: to, completion: completion)
|
||||
}
|
||||
|
||||
public func renameFeed(_ feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.renameFeed(for: self, with: feed, to: name, completion: completion)
|
||||
}
|
||||
|
||||
public func restoreFeed(_ feed: Feed, folder: Folder?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.restoreFeed(for: self, feed: feed, folder: folder, completion: completion)
|
||||
public func restoreFeed(_ feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.restoreFeed(for: self, feed: feed, container: container, completion: completion)
|
||||
}
|
||||
|
||||
public func deleteFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.deleteFolder(for: self, with: folder, completion: completion)
|
||||
public func addFolder(_ name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
||||
delegate.addFolder(for: self, name: name, completion: completion)
|
||||
}
|
||||
|
||||
public func removeFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.removeFolder(for: self, with: folder, completion: completion)
|
||||
}
|
||||
|
||||
public func renameFolder(_ folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
@ -454,6 +466,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
delegate.restoreFolder(for: self, folder: folder, completion: completion)
|
||||
}
|
||||
|
||||
func clearFeedMetadata(_ feed: Feed) {
|
||||
feedMetadata[feed.url] = nil
|
||||
}
|
||||
|
||||
func addFolder(_ folder: Folder) {
|
||||
folders!.insert(folder)
|
||||
postChildrenDidChangeNotification()
|
||||
@ -692,27 +708,25 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
return _flattenedFeeds
|
||||
}
|
||||
|
||||
public func removeFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.removeFeed(for: self, from: self, with: feed, completion: completion)
|
||||
}
|
||||
|
||||
public func addFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
delegate.addFeed(for: self, to: self, with: feed, completion: completion)
|
||||
}
|
||||
|
||||
func removeFeed(_ feed: Feed) {
|
||||
public func removeFeed(_ feed: Feed) {
|
||||
topLevelFeeds.remove(feed)
|
||||
structureDidChange()
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
func addFeed(_ feed: Feed) {
|
||||
public func addFeed(_ feed: Feed) {
|
||||
topLevelFeeds.insert(feed)
|
||||
structureDidChange()
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
func deleteFolder(_ folder: Folder) {
|
||||
func addFeedIfNotInAnyFolder(_ feed: Feed) {
|
||||
if !flattenedFeeds().contains(feed) {
|
||||
addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
func removeFolder(_ folder: Folder) {
|
||||
folders?.remove(folder)
|
||||
structureDidChange()
|
||||
postChildrenDidChangeNotification()
|
||||
@ -785,19 +799,19 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
|
||||
@objc func saveToDiskIfNeeded() {
|
||||
|
||||
if dirty {
|
||||
if dirty && !isDeleted {
|
||||
saveToDisk()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func saveFeedMetadataIfNeeded() {
|
||||
if feedMetadataDirty {
|
||||
if feedMetadataDirty && !isDeleted {
|
||||
saveFeedMetadata()
|
||||
}
|
||||
}
|
||||
|
||||
@objc func saveAccountMetadataIfNeeded() {
|
||||
if metadataDirty {
|
||||
if metadataDirty && !isDeleted {
|
||||
saveAccountMetadata()
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ protocol AccountDelegate {
|
||||
|
||||
// Local account does not; some synced accounts might.
|
||||
var supportsSubFolders: Bool { get }
|
||||
var usesTags: Bool { get }
|
||||
var opmlImportInProgress: Bool { get }
|
||||
|
||||
var server: String? { get }
|
||||
@ -28,17 +29,17 @@ protocol AccountDelegate {
|
||||
|
||||
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
func addFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> Void)
|
||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func deleteFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
func createFeed(for account: Account, url: String, completion: @escaping (Result<Feed, Error>) -> Void)
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void)
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func deleteFeed(for account: Account, with feed: Feed, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
func addFeed(for account: Account, to container: Container, with: Feed, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func removeFeed(for account: Account, from container: Container, with: Feed, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func addFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
func restoreFeed(for account: Account, feed: Feed, folder: Folder?, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
|
||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>?
|
||||
|
@ -127,6 +127,7 @@ public final class AccountManager: UnreadCountProvider {
|
||||
}
|
||||
|
||||
accountsDictionary.removeValue(forKey: account.accountID)
|
||||
account.isDeleted = true
|
||||
|
||||
do {
|
||||
try FileManager.default.removeItem(atPath: account.dataFolder)
|
||||
|
@ -18,6 +18,7 @@ extension Notification.Name {
|
||||
|
||||
public protocol Container: class {
|
||||
|
||||
var account: Account? { get }
|
||||
var topLevelFeeds: Set<Feed> { get set }
|
||||
var folders: Set<Folder>? { get set }
|
||||
|
||||
@ -27,8 +28,8 @@ public protocol Container: class {
|
||||
func hasChildFolder(with: String) -> Bool
|
||||
func childFolder(with: String) -> Folder?
|
||||
|
||||
func removeFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func addFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void)
|
||||
func removeFeed(_ feed: Feed)
|
||||
func addFeed(_ feed: Feed)
|
||||
|
||||
//Recursive — checks subfolders
|
||||
func flattenedFeeds() -> Set<Feed>
|
||||
|
@ -11,38 +11,54 @@ import RSParser
|
||||
import RSWeb
|
||||
import RSCore
|
||||
|
||||
protocol FeedFinderDelegate: class {
|
||||
|
||||
func feedFinder(_: FeedFinder, didFindFeeds: Set<FeedSpecifier>)
|
||||
}
|
||||
|
||||
class FeedFinder {
|
||||
|
||||
static func find(url: URL, completion: @escaping (Result<Set<FeedSpecifier>, Error>) -> Void) {
|
||||
|
||||
private weak var delegate: FeedFinderDelegate?
|
||||
private var feedSpecifiers = [String: FeedSpecifier]()
|
||||
private var didNotifyDelegate = false
|
||||
|
||||
var initialDownloadError: Error?
|
||||
var initialDownloadStatusCode = -1
|
||||
|
||||
init(url: URL, delegate: FeedFinderDelegate) {
|
||||
|
||||
self.delegate = delegate
|
||||
|
||||
DispatchQueue.main.async() { () -> Void in
|
||||
|
||||
self.findFeeds(url)
|
||||
downloadUsingCache(url) { (data, response, error) in
|
||||
|
||||
if response?.forcedStatusCode == 404 {
|
||||
completion(.failure(AccountError.createErrorNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
if let error = error {
|
||||
completion(.failure(error))
|
||||
return
|
||||
}
|
||||
|
||||
guard let data = data, let response = response else {
|
||||
completion(.failure(AccountError.createErrorNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
if !response.statusIsOK || data.isEmpty {
|
||||
completion(.failure(AccountError.createErrorNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
if FeedFinder.isFeed(data, url.absoluteString) {
|
||||
let feedSpecifier = FeedSpecifier(title: nil, urlString: url.absoluteString, source: .UserEntered)
|
||||
completion(.success(Set([feedSpecifier])))
|
||||
return
|
||||
}
|
||||
|
||||
if !FeedFinder.isHTML(data) {
|
||||
completion(.failure(AccountError.createErrorNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
FeedFinder.findFeedsInHTMLPage(htmlData: data, urlString: url.absoluteString, completion: completion)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
deinit {
|
||||
notifyDelegateIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
private extension FeedFinder {
|
||||
|
||||
func addFeedSpecifier(_ feedSpecifier: FeedSpecifier) {
|
||||
static func addFeedSpecifier(_ feedSpecifier: FeedSpecifier, feedSpecifiers: inout [String: FeedSpecifier]) {
|
||||
|
||||
// If there’s an existing feed specifier, merge the two so that we have the best data. If one has a title and one doesn’t, use that non-nil title. Use the better source.
|
||||
|
||||
@ -55,7 +71,7 @@ private extension FeedFinder {
|
||||
}
|
||||
}
|
||||
|
||||
func findFeedsInHTMLPage(htmlData: Data, urlString: String) {
|
||||
static func findFeedsInHTMLPage(htmlData: Data, urlString: String, completion: @escaping (Result<Set<FeedSpecifier>, Error>) -> Void) {
|
||||
|
||||
// Feeds in the <head> section we automatically assume are feeds.
|
||||
// If there are none from the <head> section,
|
||||
@ -63,31 +79,35 @@ private extension FeedFinder {
|
||||
// and added once we determine they are feeds.
|
||||
|
||||
let possibleFeedSpecifiers = possibleFeedsInHTMLPage(htmlData: htmlData, urlString: urlString)
|
||||
var feedSpecifiers = [String: FeedSpecifier]()
|
||||
var feedSpecifiersToDownload = Set<FeedSpecifier>()
|
||||
|
||||
var didFindFeedInHTMLHead = false
|
||||
|
||||
for oneFeedSpecifier in possibleFeedSpecifiers {
|
||||
if oneFeedSpecifier.source == .HTMLHead {
|
||||
addFeedSpecifier(oneFeedSpecifier)
|
||||
addFeedSpecifier(oneFeedSpecifier, feedSpecifiers: &feedSpecifiers)
|
||||
didFindFeedInHTMLHead = true
|
||||
}
|
||||
else {
|
||||
if !feedSpecifiersContainsURLString(oneFeedSpecifier.urlString) {
|
||||
if feedSpecifiers[oneFeedSpecifier.urlString] == nil {
|
||||
feedSpecifiersToDownload.insert(oneFeedSpecifier)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if didFindFeedInHTMLHead || feedSpecifiersToDownload.isEmpty {
|
||||
stopFinding()
|
||||
}
|
||||
else {
|
||||
downloadFeedSpecifiers(feedSpecifiersToDownload)
|
||||
if didFindFeedInHTMLHead {
|
||||
completion(.success(Set(feedSpecifiers.values)))
|
||||
return
|
||||
} else if feedSpecifiersToDownload.isEmpty {
|
||||
completion(.failure(AccountError.createErrorNotFound))
|
||||
return
|
||||
} else {
|
||||
downloadFeedSpecifiers(feedSpecifiersToDownload, feedSpecifiers: feedSpecifiers, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
func possibleFeedsInHTMLPage(htmlData: Data, urlString: String) -> Set<FeedSpecifier> {
|
||||
static func possibleFeedsInHTMLPage(htmlData: Data, urlString: String) -> Set<FeedSpecifier> {
|
||||
|
||||
let parserData = ParserData(url: urlString, data: htmlData)
|
||||
var feedSpecifiers = HTMLFeedFinder(parserData: parserData).feedSpecifiers
|
||||
@ -109,105 +129,42 @@ private extension FeedFinder {
|
||||
return feedSpecifiers
|
||||
}
|
||||
|
||||
func feedSpecifiersContainsURLString(_ urlString: String) -> Bool {
|
||||
|
||||
if let _ = feedSpecifiers[urlString] {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isHTML(_ data: Data) -> Bool {
|
||||
|
||||
static func isHTML(_ data: Data) -> Bool {
|
||||
return (data as NSData).rs_dataIsProbablyHTML()
|
||||
}
|
||||
|
||||
func findFeeds(_ initialURL: URL) {
|
||||
static func downloadFeedSpecifiers(_ downloadFeedSpecifiers: Set<FeedSpecifier>, feedSpecifiers: [String: FeedSpecifier], completion: @escaping (Result<Set<FeedSpecifier>, Error>) -> Void) {
|
||||
|
||||
downloadInitialFeed(initialURL)
|
||||
}
|
||||
var resultFeedSpecifiers = feedSpecifiers
|
||||
let group = DispatchGroup()
|
||||
|
||||
for downloadFeedSpecifier in downloadFeedSpecifiers {
|
||||
|
||||
func downloadInitialFeed(_ initialURL: URL) {
|
||||
|
||||
downloadUsingCache(initialURL) { (data, response, error) in
|
||||
|
||||
self.initialDownloadStatusCode = response?.forcedStatusCode ?? -1
|
||||
|
||||
if let error = error {
|
||||
self.initialDownloadError = error
|
||||
self.stopFinding()
|
||||
return
|
||||
}
|
||||
guard let data = data, let response = response else {
|
||||
self.stopFinding()
|
||||
return
|
||||
}
|
||||
|
||||
if !response.statusIsOK || data.isEmpty {
|
||||
self.stopFinding()
|
||||
return
|
||||
}
|
||||
|
||||
if self.isFeed(data, initialURL.absoluteString) {
|
||||
let feedSpecifier = FeedSpecifier(title: nil, urlString: initialURL.absoluteString, source: .UserEntered)
|
||||
self.addFeedSpecifier(feedSpecifier)
|
||||
self.stopFinding()
|
||||
return
|
||||
}
|
||||
|
||||
if !self.isHTML(data) {
|
||||
self.stopFinding()
|
||||
return
|
||||
}
|
||||
|
||||
self.findFeedsInHTMLPage(htmlData: data, urlString: initialURL.absoluteString)
|
||||
}
|
||||
}
|
||||
|
||||
func downloadFeedSpecifiers(_ feedSpecifiers: Set<FeedSpecifier>) {
|
||||
|
||||
var pendingDownloads = feedSpecifiers
|
||||
|
||||
for oneFeedSpecifier in feedSpecifiers {
|
||||
|
||||
guard let url = URL(string: oneFeedSpecifier.urlString) else {
|
||||
pendingDownloads.remove(oneFeedSpecifier)
|
||||
guard let url = URL(string: downloadFeedSpecifier.urlString) else {
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
group.enter()
|
||||
downloadUsingCache(url) { (data, response, error) in
|
||||
|
||||
pendingDownloads.remove(oneFeedSpecifier)
|
||||
|
||||
if let data = data, let response = response, response.statusIsOK, error == nil {
|
||||
if self.isFeed(data, oneFeedSpecifier.urlString) {
|
||||
self.addFeedSpecifier(oneFeedSpecifier)
|
||||
if self.isFeed(data, downloadFeedSpecifier.urlString) {
|
||||
addFeedSpecifier(downloadFeedSpecifier, feedSpecifiers: &resultFeedSpecifiers)
|
||||
}
|
||||
}
|
||||
|
||||
if pendingDownloads.isEmpty {
|
||||
self.stopFinding()
|
||||
}
|
||||
group.leave()
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func stopFinding() {
|
||||
|
||||
notifyDelegateIfNeeded()
|
||||
}
|
||||
|
||||
func notifyDelegateIfNeeded() {
|
||||
|
||||
if !didNotifyDelegate {
|
||||
delegate?.feedFinder(self, didFindFeeds: Set(feedSpecifiers.values))
|
||||
didNotifyDelegate = true
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion(.success(Set(resultFeedSpecifiers.values)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func isFeed(_ data: Data, _ urlString: String) -> Bool {
|
||||
|
||||
static func isFeed(_ data: Data, _ urlString: String) -> Bool {
|
||||
let parserData = ParserData(url: urlString, data: data)
|
||||
return FeedParser.canParse(parserData)
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -143,25 +143,6 @@ final class FeedbinAPICaller: NSObject {
|
||||
transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion)
|
||||
}
|
||||
|
||||
func deleteTag(name: String, completion: @escaping (Result<[FeedbinTagging]?, Error>) -> Void) {
|
||||
|
||||
let callURL = feedbinBaseURL.appendingPathComponent("tags.json")
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
let payload = FeedbinDeleteTag(name: name)
|
||||
|
||||
transport.send(request: request, method: HTTPMethod.delete, payload: payload, resultType: [FeedbinTagging].self) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (_, taggings)):
|
||||
completion(.success(taggings))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveSubscriptions(completion: @escaping (Result<[FeedbinSubscription]?, Error>) -> Void) {
|
||||
|
||||
let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json")
|
||||
@ -438,13 +419,12 @@ final class FeedbinAPICaller: NSObject {
|
||||
|
||||
func retrieveEntries(page: String, completion: @escaping (Result<([FeedbinEntry]?, String?), Error>) -> Void) {
|
||||
|
||||
guard let url = URL(string: page), var callComponents = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
|
||||
guard let url = URL(string: page) else {
|
||||
completion(.success((nil, nil)))
|
||||
return
|
||||
}
|
||||
|
||||
callComponents.queryItems?.append(URLQueryItem(name: "mode", value: "extended"))
|
||||
let request = URLRequest(url: callComponents.url!, credentials: credentials)
|
||||
let request = URLRequest(url: url, credentials: credentials)
|
||||
|
||||
transport.send(request: request, resultType: [FeedbinEntry].self) { result in
|
||||
|
||||
@ -551,10 +531,11 @@ extension FeedbinAPICaller {
|
||||
}
|
||||
|
||||
if let lowerBound = link.range(of: "page=")?.upperBound {
|
||||
if let upperBound = link.range(of: "&")?.lowerBound {
|
||||
let partialLink = link[lowerBound..<link.endIndex]
|
||||
if let upperBound = partialLink.range(of: "&")?.lowerBound {
|
||||
return Int(link[lowerBound..<upperBound])
|
||||
}
|
||||
if let upperBound = link.range(of: ">")?.lowerBound {
|
||||
if let upperBound = partialLink.range(of: ">")?.lowerBound {
|
||||
return Int(link[lowerBound..<upperBound])
|
||||
}
|
||||
}
|
||||
|
@ -31,6 +31,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Feedbin")
|
||||
|
||||
let supportsSubFolders = false
|
||||
let usesTags = true
|
||||
let server: String? = "api.feedbin.com"
|
||||
var opmlImportInProgress = false
|
||||
|
||||
@ -230,6 +231,14 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func addFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
||||
if let folder = account.ensureFolder(with: name) {
|
||||
completion(.success(folder))
|
||||
} else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
}
|
||||
}
|
||||
|
||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
caller.renameTag(oldName: folder.name ?? "", newName: name) { result in
|
||||
@ -249,51 +258,46 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func deleteFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
// Feedbin uses tags and if at least one feed isn't tagged, then the folder doesn't exist on their system
|
||||
guard folder.hasAtLeastOneFeed() else {
|
||||
account.deleteFolder(folder)
|
||||
account.removeFolder(folder)
|
||||
return
|
||||
}
|
||||
|
||||
// After we successfully delete at Feedbin, we add all the feeds to the account to save them. We then
|
||||
// delete the folder. We then sync the taggings we received on the delete to remove any feeds from
|
||||
// the account that might be in another folder.
|
||||
caller.deleteTag(name: folder.name ?? "") { result in
|
||||
switch result {
|
||||
case .success(let taggings):
|
||||
DispatchQueue.main.sync {
|
||||
BatchUpdate.shared.perform {
|
||||
for feed in folder.topLevelFeeds {
|
||||
account.addFeed(feed)
|
||||
self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
}
|
||||
account.deleteFolder(folder)
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
self.syncTaggings(account, taggings)
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
let group = DispatchGroup()
|
||||
|
||||
for feed in folder.topLevelFeeds {
|
||||
group.enter()
|
||||
removeFeed(for: account, with: feed, from: folder) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
account.removeFolder(folder)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func createFeed(for account: Account, url: String, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
|
||||
caller.createSubscription(url: url) { result in
|
||||
switch result {
|
||||
case .success(let subResult):
|
||||
switch subResult {
|
||||
case .created(let subscription):
|
||||
self.createFeed(account: account, subscription: subscription, completion: completion)
|
||||
self.createFeed(account: account, subscription: subscription, name: name, container: container, completion: completion)
|
||||
case .multipleChoice(let choices):
|
||||
self.decideBestFeedChoice(account: account, url: url, choices: choices, completion: completion)
|
||||
self.decideBestFeedChoice(account: account, url: url, name: name, container: container, choices: choices, completion: completion)
|
||||
case .alreadySubscribed:
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(AccountError.createErrorAlreadySubscribed))
|
||||
@ -339,37 +343,31 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func deleteFeed(for account: Account, with feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
// This error should never happen
|
||||
guard let subscriptionID = feed.subscriptionID else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
return
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if feed.folderRelationship?.count ?? 0 > 1 {
|
||||
deleteTagging(for: account, with: feed, from: container, completion: completion)
|
||||
} else {
|
||||
account.clearFeedMetadata(feed)
|
||||
deleteSubscription(for: account, with: feed, from: container, completion: completion)
|
||||
}
|
||||
|
||||
caller.deleteSubscription(subscriptionID: subscriptionID) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
account.removeFeed(feed)
|
||||
if let folders = account.folders {
|
||||
for folder in folders {
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if from is Account {
|
||||
addFeed(for: account, with: feed, to: to, completion: completion)
|
||||
} else {
|
||||
deleteTagging(for: account, with: feed, from: from) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.addFeed(for: account, with: feed, to: to, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func addFeed(for account: Account, to container: Container, with feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
if let folder = container as? Folder, let feedID = Int(feed.feedID) {
|
||||
caller.createTagging(feedID: feedID, name: folder.name ?? "") { result in
|
||||
@ -377,6 +375,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
case .success(let taggingID):
|
||||
DispatchQueue.main.async {
|
||||
self.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: String(taggingID))
|
||||
account.removeFeed(feed)
|
||||
folder.addFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
@ -388,55 +387,24 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let account = container as? Account {
|
||||
account.addFeed(feed)
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
if let account = container as? Account {
|
||||
account.addFeedIfNotInAnyFolder(feed)
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func removeFeed(for account: Account, from container: Container, with feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] {
|
||||
caller.deleteTagging(taggingID: feedTaggingID) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
folder.removeFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let account = container as? Account {
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
}
|
||||
|
||||
func restoreFeed(for account: Account, feed: Feed, folder: Folder?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
let editedName = feed.editedName
|
||||
|
||||
createFeed(for: account, url: feed.url) { result in
|
||||
createFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in
|
||||
switch result {
|
||||
case .success(let feed):
|
||||
self.processRestoredFeed(for: account, feed: feed, editedName: editedName, folder: folder, completion: completion)
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
@ -450,7 +418,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
||||
for feed in folder.topLevelFeeds {
|
||||
|
||||
group.enter()
|
||||
addFeed(for: account, to: folder, with: feed) { result in
|
||||
addFeed(for: account, with: feed, to: folder) { result in
|
||||
if account.topLevelFeeds.contains(feed) {
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
@ -572,7 +540,7 @@ private extension FeedbinAccountDelegate {
|
||||
account.addFeed(feed)
|
||||
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
}
|
||||
account.deleteFolder(folder)
|
||||
account.removeFolder(folder)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -824,74 +792,6 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func processRestoredFeed(for account: Account, feed: Feed, editedName: String?, folder: Folder?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
if let folder = folder {
|
||||
|
||||
addFeed(for: account, to: folder, with: feed) { result in
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
|
||||
if editedName != nil {
|
||||
DispatchQueue.main.async {
|
||||
account.removeFeed(feed)
|
||||
folder.addFeed(feed)
|
||||
}
|
||||
self.processRestoredFeedName(for: account, feed: feed, editedName: editedName!, completion: completion)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
account.removeFeed(feed)
|
||||
folder.addFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
DispatchQueue.main.async {
|
||||
account.addFeed(feed)
|
||||
}
|
||||
|
||||
if editedName != nil {
|
||||
processRestoredFeedName(for: account, feed: feed, editedName: editedName!, completion: completion)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func processRestoredFeedName(for account: Account, feed: Feed, editedName: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
renameFeed(for: account, with: feed, to: editedName) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
feed.editedName = editedName
|
||||
completion(.success(()))
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) {
|
||||
if var folderRelationship = feed.folderRelationship {
|
||||
folderRelationship[folderName] = nil
|
||||
@ -908,7 +808,7 @@ private extension FeedbinAccountDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
func decideBestFeedChoice(account: Account, url: String, choices: [FeedbinSubscriptionChoice], completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func decideBestFeedChoice(account: Account, url: String, name: String?, container: Container, choices: [FeedbinSubscriptionChoice], completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
|
||||
let feedSpecifiers: [FeedSpecifier] = choices.map { choice in
|
||||
let source = url == choice.url ? FeedSpecifier.Source.UserEntered : FeedSpecifier.Source.HTMLLink
|
||||
@ -918,7 +818,7 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
if let bestSpecifier = FeedSpecifier.bestFeed(in: Set(feedSpecifiers)) {
|
||||
if let bestSubscription = choices.filter({ bestSpecifier.urlString == $0.url }).first {
|
||||
createFeed(for: account, url: bestSubscription.url, completion: completion)
|
||||
createFeed(for: account, url: bestSubscription.url, name: name, container: container, completion: completion)
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
@ -932,44 +832,66 @@ private extension FeedbinAccountDelegate {
|
||||
|
||||
}
|
||||
|
||||
func createFeed( account: Account, subscription sub: FeedbinSubscription, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func createFeed( account: Account, subscription sub: FeedbinSubscription, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
||||
let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL)
|
||||
feed.subscriptionID = String(sub.subscriptionID)
|
||||
|
||||
// Download the initial articles
|
||||
self.caller.retrieveEntries(feedID: feed.feedID) { result in
|
||||
|
||||
account.addFeed(feed, to: container) { result in
|
||||
switch result {
|
||||
case .success(let (entries, page)):
|
||||
|
||||
self.processEntries(account: account, entries: entries) {
|
||||
self.refreshArticles(account, page: page) {
|
||||
self.refreshArticleStatus(for: account) {
|
||||
self.refreshMissingArticles(account) {
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
}
|
||||
}
|
||||
case .success:
|
||||
if let name = name {
|
||||
account.renameFeed(feed, to: name) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.initialFeedDownload(account: account, feed: feed, completion: completion)
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.initialFeedDownload(account: account, feed: feed, completion: completion)
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Initial articles download failed: %@.", error.localizedDescription)
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
}
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func initialFeedDownload( account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
|
||||
// Download the initial articles
|
||||
self.caller.retrieveEntries(feedID: feed.feedID) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let (entries, page)):
|
||||
|
||||
self.processEntries(account: account, entries: entries) {
|
||||
self.refreshArticles(account, page: page) {
|
||||
self.refreshArticleStatus(for: account) {
|
||||
self.refreshMissingArticles(account) {
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func refreshArticles(_ account: Account, completion: @escaping (() -> Void)) {
|
||||
|
||||
os_log(.debug, log: log, "Refreshing articles...")
|
||||
@ -1195,5 +1117,63 @@ private extension FeedbinAccountDelegate {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func deleteTagging(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] {
|
||||
caller.deleteTagging(taggingID: feedTaggingID) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||
folder.removeFeed(feed)
|
||||
account.addFeedIfNotInAnyFolder(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let account = container as? Account {
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func deleteSubscription(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
// This error should never happen
|
||||
guard let subscriptionID = feed.subscriptionID else {
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
caller.deleteSubscription(subscriptionID: subscriptionID) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
account.removeFeed(feed)
|
||||
if let folders = account.folders {
|
||||
for folder in folders {
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -95,20 +95,12 @@ public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCoun
|
||||
return topLevelFeeds.contains(feed)
|
||||
}
|
||||
|
||||
public func addFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
account?.addFeed(container: self, feed: feed, completion: completion)
|
||||
}
|
||||
|
||||
public func removeFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
account?.removeFeed(feed, from: self, completion: completion)
|
||||
}
|
||||
|
||||
func addFeed(_ feed: Feed) {
|
||||
public func addFeed(_ feed: Feed) {
|
||||
topLevelFeeds.insert(feed)
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
||||
func removeFeed(_ feed: Feed) {
|
||||
public func removeFeed(_ feed: Feed) {
|
||||
topLevelFeeds.remove(feed)
|
||||
postChildrenDidChangeNotification()
|
||||
}
|
||||
|
@ -19,16 +19,13 @@ public enum LocalAccountDelegateError: String, Error {
|
||||
final class LocalAccountDelegate: AccountDelegate {
|
||||
|
||||
let supportsSubFolders = false
|
||||
let usesTags = false
|
||||
let opmlImportInProgress = false
|
||||
|
||||
let server: String? = nil
|
||||
var credentials: Credentials?
|
||||
var accountMetadata: AccountMetadata?
|
||||
|
||||
private weak var account: Account?
|
||||
private var feedFinder: FeedFinder?
|
||||
private var createFeedCompletion: ((Result<Feed, Error>) -> Void)?
|
||||
|
||||
private let refresher = LocalAccountRefresher()
|
||||
|
||||
var refreshProgress: DownloadProgress {
|
||||
@ -88,28 +85,50 @@ final class LocalAccountDelegate: AccountDelegate {
|
||||
completion(.success(()))
|
||||
|
||||
}
|
||||
|
||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
folder.name = name
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func deleteFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
account.deleteFolder(folder)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func createFeed(for account: Account, url urlString: String, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
func createFeed(for account: Account, url urlString: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
|
||||
guard let url = URL(string: urlString) else {
|
||||
completion(.failure(LocalAccountDelegateError.invalidParameter))
|
||||
return
|
||||
}
|
||||
|
||||
self.account = account
|
||||
createFeedCompletion = completion
|
||||
|
||||
feedFinder = FeedFinder(url: url, delegate: self)
|
||||
FeedFinder.find(url: url) { result in
|
||||
|
||||
switch result {
|
||||
case .success(let feedSpecifiers):
|
||||
|
||||
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers),
|
||||
let url = URL(string: bestFeedSpecifier.urlString) else {
|
||||
completion(.failure(AccountError.createErrorNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
if account.hasFeed(withURL: bestFeedSpecifier.urlString) {
|
||||
completion(.failure(AccountError.createErrorAlreadySubscribed))
|
||||
return
|
||||
}
|
||||
|
||||
let feed = account.createFeed(with: nil, url: url.absoluteString, feedID: url.absoluteString, homePageURL: nil)
|
||||
|
||||
InitialFeedDownloader.download(url) { parsedFeed in
|
||||
|
||||
if let parsedFeed = parsedFeed {
|
||||
account.update(feed, with: parsedFeed, {})
|
||||
}
|
||||
|
||||
feed.editedName = name
|
||||
|
||||
container.addFeed(feed)
|
||||
completion(.success(feed))
|
||||
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -118,55 +137,42 @@ final class LocalAccountDelegate: AccountDelegate {
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func deleteFeed(for account: Account, from container: Container, feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
|
||||
if let account = container as? Account {
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
if let folder = container as? Folder {
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
completion(.success(()))
|
||||
|
||||
}
|
||||
|
||||
func deleteFeed(for account: Account, with feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
account.removeFeed(feed)
|
||||
if let folders = account.folders {
|
||||
for folder in folders {
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
}
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
container?.removeFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func addFeed(for account: Account, to container: Container, with feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if let folder = container as? Folder {
|
||||
folder.addFeed(feed)
|
||||
feed.account = folder.account
|
||||
} else if let account = container as? Account {
|
||||
account.addFeed(feed)
|
||||
feed.account = account
|
||||
}
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
from.removeFeed(feed)
|
||||
to.addFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func removeFeed(for account: Account, from container: Container, with feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if let account = container as? Account {
|
||||
account.removeFeed(feed)
|
||||
}
|
||||
if let folder = container as? Folder {
|
||||
folder.removeFeed(feed)
|
||||
}
|
||||
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
container.addFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func restoreFeed(for account: Account, feed: Feed, folder: Folder?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if let folder = folder {
|
||||
folder.addFeed(feed)
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
container.addFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func addFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
||||
if let folder = account.ensureFolder(with: name) {
|
||||
completion(.success(folder))
|
||||
} else {
|
||||
account.addFeed(feed)
|
||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||
}
|
||||
}
|
||||
|
||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
folder.name = name
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
account.removeFolder(folder)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
@ -187,42 +193,3 @@ final class LocalAccountDelegate: AccountDelegate {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension LocalAccountDelegate: FeedFinderDelegate {
|
||||
|
||||
// MARK: FeedFinderDelegate
|
||||
|
||||
public func feedFinder(_ feedFinder: FeedFinder, didFindFeeds feedSpecifiers: Set<FeedSpecifier>) {
|
||||
|
||||
if let error = feedFinder.initialDownloadError {
|
||||
if feedFinder.initialDownloadStatusCode == 404 {
|
||||
createFeedCompletion!(.failure(AccountError.createErrorNotFound))
|
||||
} else {
|
||||
createFeedCompletion!(.failure(error))
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
guard let bestFeedSpecifier = FeedSpecifier.bestFeed(in: feedSpecifiers),
|
||||
let url = URL(string: bestFeedSpecifier.urlString),
|
||||
let account = account else {
|
||||
createFeedCompletion!(.failure(AccountError.createErrorNotFound))
|
||||
return
|
||||
}
|
||||
|
||||
if account.hasFeed(withURL: bestFeedSpecifier.urlString) {
|
||||
createFeedCompletion!(.failure(AccountError.createErrorAlreadySubscribed))
|
||||
return
|
||||
}
|
||||
|
||||
let feed = account.createFeed(with: nil, url: url.absoluteString, feedID: url.absoluteString, homePageURL: nil)
|
||||
InitialFeedDownloader.download(url) { [weak self] parsedFeed in
|
||||
if let parsedFeed = parsedFeed {
|
||||
account.update(feed, with: parsedFeed, {})
|
||||
}
|
||||
self?.createFeedCompletion!(.success(feed))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -52,7 +52,6 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
return
|
||||
}
|
||||
let account = accountAndFolderSpecifier.account
|
||||
let folder = accountAndFolderSpecifier.folder
|
||||
|
||||
if account.hasFeed(withURL: url.absoluteString) {
|
||||
showAlreadySubscribedError(url.absoluteString)
|
||||
@ -61,20 +60,20 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
|
||||
account.createFeed(url: url.absoluteString) { [weak self] result in
|
||||
|
||||
self?.endShowingProgress()
|
||||
account.createFeed(url: url.absoluteString, name: title, container: container) { result in
|
||||
|
||||
self.endShowingProgress()
|
||||
BatchUpdate.shared.end()
|
||||
|
||||
switch result {
|
||||
case .success(let feed):
|
||||
self?.processFeed(feed, account: account, folder: folder, url: url, title: title)
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
|
||||
case .failure(let error):
|
||||
BatchUpdate.shared.end()
|
||||
switch error {
|
||||
case AccountError.createErrorAlreadySubscribed:
|
||||
self?.showAlreadySubscribedError(url.absoluteString)
|
||||
self.showAlreadySubscribedError(url.absoluteString)
|
||||
case AccountError.createErrorNotFound:
|
||||
self?.showNoFeedsErrorMessage()
|
||||
self.showNoFeedsErrorMessage()
|
||||
default:
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
@ -125,45 +124,6 @@ private extension AddFeedController {
|
||||
}
|
||||
}
|
||||
|
||||
func processFeed(_ feed: Feed, account: Account, folder: Folder?, url: URL, title: String?) {
|
||||
|
||||
if let title = title {
|
||||
account.renameFeed(feed, to: title) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let folder = folder {
|
||||
folder.addFeed(feed) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
BatchUpdate.shared.end()
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
|
||||
case .failure(let error):
|
||||
BatchUpdate.shared.end()
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
account.addFeed(feed) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
BatchUpdate.shared.end()
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
|
||||
case .failure(let error):
|
||||
BatchUpdate.shared.end()
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// MARK: Errors
|
||||
|
||||
func showAlreadySubscribedError(_ urlString: String) {
|
||||
|
@ -29,7 +29,7 @@ class AddFeedWindowController : NSWindowController {
|
||||
|
||||
private var urlString: String?
|
||||
private var initialName: String?
|
||||
private var initialAccount: Account?
|
||||
private weak var initialAccount: Account?
|
||||
private var initialFolder: Folder?
|
||||
private weak var delegate: AddFeedWindowControllerDelegate?
|
||||
private var folderTreeController: TreeController!
|
||||
|
@ -1,82 +0,0 @@
|
||||
//
|
||||
// FolderPasteboardWriter.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 2/11/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
extension Folder: PasteboardWriterOwner {
|
||||
|
||||
public var pasteboardWriter: NSPasteboardWriting {
|
||||
return FolderPasteboardWriter(folder: self)
|
||||
}
|
||||
}
|
||||
|
||||
@objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
|
||||
private let folder: Folder
|
||||
static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder"
|
||||
static let folderUTIInternalType = NSPasteboard.PasteboardType(rawValue: folderUTIInternal)
|
||||
|
||||
init(folder: Folder) {
|
||||
|
||||
self.folder = folder
|
||||
}
|
||||
|
||||
// MARK: - NSPasteboardWriting
|
||||
|
||||
func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
|
||||
|
||||
return [.string, FolderPasteboardWriter.folderUTIInternalType]
|
||||
}
|
||||
|
||||
func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
|
||||
|
||||
let plist: Any?
|
||||
|
||||
switch type {
|
||||
case .string:
|
||||
plist = folder.nameForDisplay
|
||||
case FolderPasteboardWriter.folderUTIInternalType:
|
||||
plist = internalDictionary()
|
||||
default:
|
||||
plist = nil
|
||||
}
|
||||
|
||||
return plist
|
||||
}
|
||||
}
|
||||
|
||||
private extension FolderPasteboardWriter {
|
||||
|
||||
private struct Key {
|
||||
|
||||
static let name = "name"
|
||||
|
||||
// Internal
|
||||
static let accountID = "accountID"
|
||||
static let folderID = "folderID"
|
||||
}
|
||||
|
||||
func internalDictionary() -> [String: Any] {
|
||||
|
||||
var d = [String: Any]()
|
||||
|
||||
d[Key.folderID] = folder.folderID
|
||||
if let name = folder.name {
|
||||
d[Key.name] = name
|
||||
}
|
||||
if let accountID = folder.account?.accountID {
|
||||
d[Key.accountID] = accountID
|
||||
}
|
||||
|
||||
return d
|
||||
|
||||
}
|
||||
}
|
||||
|
137
Mac/MainWindow/Sidebar/PasteboardFolder.swift
Normal file
137
Mac/MainWindow/Sidebar/PasteboardFolder.swift
Normal file
@ -0,0 +1,137 @@
|
||||
//
|
||||
// FolderPasteboardWriter.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Brent Simmons on 2/11/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Account
|
||||
import RSCore
|
||||
|
||||
typealias PasteboardFolderDictionary = [String: String]
|
||||
|
||||
struct PasteboardFolder: Hashable {
|
||||
|
||||
private struct Key {
|
||||
static let name = "name"
|
||||
// Internal
|
||||
static let folderID = "folderID"
|
||||
static let accountID = "accountID"
|
||||
}
|
||||
|
||||
|
||||
let name: String
|
||||
let folderID: String?
|
||||
let accountID: String?
|
||||
|
||||
init(name: String, folderID: String?, accountID: String?) {
|
||||
self.name = name
|
||||
self.folderID = folderID
|
||||
self.accountID = accountID
|
||||
}
|
||||
|
||||
// MARK: - Reading
|
||||
|
||||
init?(dictionary: PasteboardFolderDictionary) {
|
||||
guard let name = dictionary[Key.name] else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let folderID = dictionary[Key.folderID]
|
||||
let accountID = dictionary[Key.accountID]
|
||||
|
||||
self.init(name: name, folderID: folderID, accountID: accountID)
|
||||
}
|
||||
|
||||
init?(pasteboardItem: NSPasteboardItem) {
|
||||
var pasteboardType: NSPasteboard.PasteboardType?
|
||||
if pasteboardItem.types.contains(FolderPasteboardWriter.folderUTIInternalType) {
|
||||
pasteboardType = FolderPasteboardWriter.folderUTIInternalType
|
||||
}
|
||||
|
||||
if let foundType = pasteboardType {
|
||||
if let folderDictionary = pasteboardItem.propertyList(forType: foundType) as? PasteboardFeedDictionary {
|
||||
self.init(dictionary: folderDictionary)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
static func pasteboardFolders(with pasteboard: NSPasteboard) -> Set<PasteboardFolder>? {
|
||||
guard let items = pasteboard.pasteboardItems else {
|
||||
return nil
|
||||
}
|
||||
let folders = items.compactMap { PasteboardFolder(pasteboardItem: $0) }
|
||||
return folders.isEmpty ? nil : Set(folders)
|
||||
}
|
||||
|
||||
// MARK: - Writing
|
||||
|
||||
func internalDictionary() -> PasteboardFolderDictionary {
|
||||
var d = PasteboardFeedDictionary()
|
||||
d[PasteboardFolder.Key.name] = name
|
||||
if let folderID = folderID {
|
||||
d[PasteboardFolder.Key.folderID] = folderID
|
||||
}
|
||||
if let accountID = accountID {
|
||||
d[PasteboardFolder.Key.accountID] = accountID
|
||||
}
|
||||
return d
|
||||
}
|
||||
}
|
||||
|
||||
extension Folder: PasteboardWriterOwner {
|
||||
|
||||
public var pasteboardWriter: NSPasteboardWriting {
|
||||
return FolderPasteboardWriter(folder: self)
|
||||
}
|
||||
}
|
||||
|
||||
@objc final class FolderPasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
|
||||
private let folder: Folder
|
||||
static let folderUTIInternal = "com.ranchero.NetNewsWire-Evergreen.internal.folder"
|
||||
static let folderUTIInternalType = NSPasteboard.PasteboardType(rawValue: folderUTIInternal)
|
||||
|
||||
init(folder: Folder) {
|
||||
|
||||
self.folder = folder
|
||||
}
|
||||
|
||||
// MARK: - NSPasteboardWriting
|
||||
|
||||
func writableTypes(for pasteboard: NSPasteboard) -> [NSPasteboard.PasteboardType] {
|
||||
|
||||
return [.string, FolderPasteboardWriter.folderUTIInternalType]
|
||||
}
|
||||
|
||||
func pasteboardPropertyList(forType type: NSPasteboard.PasteboardType) -> Any? {
|
||||
|
||||
let plist: Any?
|
||||
|
||||
switch type {
|
||||
case .string:
|
||||
plist = folder.nameForDisplay
|
||||
case FolderPasteboardWriter.folderUTIInternalType:
|
||||
plist = internalDictionary
|
||||
default:
|
||||
plist = nil
|
||||
}
|
||||
|
||||
return plist
|
||||
}
|
||||
}
|
||||
|
||||
private extension FolderPasteboardWriter {
|
||||
var pasteboardFolder: PasteboardFolder {
|
||||
return PasteboardFolder(name: folder.name ?? "", folderID: String(folder.folderID), accountID: folder.account?.accountID)
|
||||
}
|
||||
|
||||
var internalDictionary: PasteboardFeedDictionary {
|
||||
return pasteboardFolder.internalDictionary()
|
||||
}
|
||||
}
|
@ -54,46 +54,70 @@ import Account
|
||||
}
|
||||
|
||||
func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
|
||||
guard let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard), !draggedFeeds.isEmpty else {
|
||||
let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)
|
||||
let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard)
|
||||
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
|
||||
let parentNode = nodeForItem(item)
|
||||
let contentsType = draggedFeedContentsType(draggedFeeds)
|
||||
|
||||
switch contentsType {
|
||||
case .singleNonLocal:
|
||||
let draggedNonLocalFeed = singleNonLocalFeed(from: draggedFeeds)!
|
||||
return validateSingleNonLocalFeedDrop(outlineView, draggedNonLocalFeed, parentNode, index)
|
||||
case .singleLocal:
|
||||
let draggedFeed = draggedFeeds.first!
|
||||
return validateSingleLocalFeedDrop(outlineView, draggedFeed, parentNode, index)
|
||||
case .multipleLocal:
|
||||
return validateLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
|
||||
case .multipleNonLocal, .mixed, .empty:
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
if let draggedFolders = draggedFolders {
|
||||
if draggedFolders.count == 1 {
|
||||
return validateLocalFolderDrop(outlineView, draggedFolders.first!, parentNode, index)
|
||||
} else {
|
||||
return validateLocalFoldersDrop(outlineView, draggedFolders, parentNode, index)
|
||||
}
|
||||
}
|
||||
|
||||
if let draggedFeeds = draggedFeeds {
|
||||
let contentsType = draggedFeedContentsType(draggedFeeds)
|
||||
|
||||
switch contentsType {
|
||||
case .singleNonLocal:
|
||||
let draggedNonLocalFeed = singleNonLocalFeed(from: draggedFeeds)!
|
||||
return validateSingleNonLocalFeedDrop(outlineView, draggedNonLocalFeed, parentNode, index)
|
||||
case .singleLocal:
|
||||
let draggedFeed = draggedFeeds.first!
|
||||
return validateSingleLocalFeedDrop(outlineView, draggedFeed, parentNode, index)
|
||||
case .multipleLocal:
|
||||
return validateLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
|
||||
case .multipleNonLocal, .mixed, .empty:
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
}
|
||||
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
|
||||
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
|
||||
guard let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard), !draggedFeeds.isEmpty else {
|
||||
let draggedFolders = PasteboardFolder.pasteboardFolders(with: info.draggingPasteboard)
|
||||
let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard)
|
||||
if (draggedFolders == nil && draggedFeeds == nil) || (draggedFolders != nil && draggedFeeds != nil) {
|
||||
return false
|
||||
}
|
||||
|
||||
let parentNode = nodeForItem(item)
|
||||
let contentsType = draggedFeedContentsType(draggedFeeds)
|
||||
|
||||
switch contentsType {
|
||||
case .singleNonLocal:
|
||||
let draggedNonLocalFeed = singleNonLocalFeed(from: draggedFeeds)!
|
||||
return acceptSingleNonLocalFeedDrop(outlineView, draggedNonLocalFeed, parentNode, index)
|
||||
case .singleLocal:
|
||||
return acceptLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
|
||||
case .multipleLocal:
|
||||
return acceptLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
|
||||
case .multipleNonLocal, .mixed, .empty:
|
||||
return false
|
||||
if let draggedFolders = draggedFolders {
|
||||
return acceptLocalFoldersDrop(outlineView, draggedFolders, parentNode, index)
|
||||
}
|
||||
|
||||
if let draggedFeeds = draggedFeeds {
|
||||
let contentsType = draggedFeedContentsType(draggedFeeds)
|
||||
|
||||
switch contentsType {
|
||||
case .singleNonLocal:
|
||||
let draggedNonLocalFeed = singleNonLocalFeed(from: draggedFeeds)!
|
||||
return acceptSingleNonLocalFeedDrop(outlineView, draggedNonLocalFeed, parentNode, index)
|
||||
case .singleLocal:
|
||||
return acceptLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
|
||||
case .multipleLocal:
|
||||
return acceptLocalFeedsDrop(outlineView, draggedFeeds, parentNode, index)
|
||||
case .multipleNonLocal, .mixed, .empty:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,11 +133,10 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
|
||||
func nodeRepresentsDraggableItem(_ node: Node) -> Bool {
|
||||
// Don’t allow PseudoFeed or Folder to be dragged.
|
||||
// Don’t allow PseudoFeed to be dragged.
|
||||
// This will have to be revisited later. For instance,
|
||||
// user-created smart feeds should be draggable, maybe.
|
||||
// And we might allow dragging folders between accounts.
|
||||
return node.representedObject is Feed
|
||||
return node.representedObject is Folder || node.representedObject is Feed
|
||||
}
|
||||
|
||||
// MARK: - Drag and Drop
|
||||
@ -173,21 +196,20 @@ private extension SidebarOutlineDataSource {
|
||||
guard let dropTargetNode = ancestorThatCanAcceptLocalFeed(parentNode) else {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
if !allParticipantsAreLocalAccounts(dropTargetNode, Set([draggedFeed])) {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
if nodeHasChildRepresentingDraggedFeed(dropTargetNode, draggedFeed) {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
let dragOperation: NSDragOperation = localFeedsDropOperation(dropTargetNode, Set([draggedFeed]))
|
||||
if violatesTagSpecificBehavior(dropTargetNode, draggedFeed) {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
if parentNode == dropTargetNode && index == NSOutlineViewDropOnItemIndex {
|
||||
return dragOperation
|
||||
return localDragOperation()
|
||||
}
|
||||
let updatedIndex = indexWhereDraggedFeedWouldAppear(dropTargetNode, draggedFeed)
|
||||
if parentNode !== dropTargetNode || index != updatedIndex {
|
||||
outlineView.setDropItem(dropTargetNode, dropChildIndex: updatedIndex)
|
||||
}
|
||||
return dragOperation
|
||||
return localDragOperation()
|
||||
}
|
||||
|
||||
func validateLocalFeedsDrop(_ outlineView: NSOutlineView, _ draggedFeeds: Set<PasteboardFeed>, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
@ -195,22 +217,19 @@ private extension SidebarOutlineDataSource {
|
||||
guard let dropTargetNode = ancestorThatCanAcceptLocalFeed(parentNode) else {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
if !allParticipantsAreLocalAccounts(dropTargetNode, draggedFeeds) {
|
||||
if nodeHasChildRepresentingAnyDraggedFeed(dropTargetNode, draggedFeeds) {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
if nodeHasChildRepresentingAnyDraggedFeed(dropTargetNode, draggedFeeds) {
|
||||
if violatesTagSpecificBehavior(dropTargetNode, draggedFeeds) {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
if parentNode !== dropTargetNode || index != NSOutlineViewDropOnItemIndex {
|
||||
outlineView.setDropItem(dropTargetNode, dropChildIndex: NSOutlineViewDropOnItemIndex)
|
||||
}
|
||||
return localFeedsDropOperation(dropTargetNode, draggedFeeds)
|
||||
return localDragOperation()
|
||||
}
|
||||
|
||||
func localFeedsDropOperation(_ dropTargetNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> NSDragOperation {
|
||||
if allParticipantsAreSameAccount(dropTargetNode, draggedFeeds) {
|
||||
return .move
|
||||
}
|
||||
func localDragOperation() -> NSDragOperation {
|
||||
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||
return .copy
|
||||
} else {
|
||||
@ -218,7 +237,7 @@ private extension SidebarOutlineDataSource {
|
||||
}
|
||||
}
|
||||
|
||||
private func accountForNode(_ node: Node) -> Account? {
|
||||
func accountForNode(_ node: Node) -> Account? {
|
||||
if let account = node.representedObject as? Account {
|
||||
return account
|
||||
}
|
||||
@ -231,7 +250,7 @@ private extension SidebarOutlineDataSource {
|
||||
return nil
|
||||
}
|
||||
|
||||
private func commonAccountsFor(_ nodes: Set<Node>) -> Set<Account> {
|
||||
func commonAccountsFor(_ nodes: Set<Node>) -> Set<Account> {
|
||||
|
||||
var accounts = Set<Account>()
|
||||
for node in nodes {
|
||||
@ -243,53 +262,156 @@ private extension SidebarOutlineDataSource {
|
||||
return accounts
|
||||
}
|
||||
|
||||
private func copy(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? Feed else {
|
||||
func accountHasFolderRepresentingAnyDraggedFolders(_ account: Account, _ draggedFolders: Set<PasteboardFolder>) -> Bool {
|
||||
for draggedFolder in draggedFolders {
|
||||
if account.existingFolder(with: draggedFolder.name) != nil {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func validateLocalFolderDrop(_ outlineView: NSOutlineView, _ draggedFolder: PasteboardFolder, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
guard let dropAccount = parentNode.representedObject as? Account, dropAccount.accountID != draggedFolder.accountID else {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
if accountHasFolderRepresentingAnyDraggedFolders(dropAccount, Set([draggedFolder])) {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
let updatedIndex = indexWhereDraggedFolderWouldAppear(parentNode, draggedFolder)
|
||||
if index != updatedIndex {
|
||||
outlineView.setDropItem(parentNode, dropChildIndex: updatedIndex)
|
||||
}
|
||||
return localDragOperation()
|
||||
}
|
||||
|
||||
func validateLocalFoldersDrop(_ outlineView: NSOutlineView, _ draggedFolders: Set<PasteboardFolder>, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
||||
guard let dropAccount = parentNode.representedObject as? Account else {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
if accountHasFolderRepresentingAnyDraggedFolders(dropAccount, draggedFolders) {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
for draggedFolder in draggedFolders {
|
||||
if dropAccount.accountID == draggedFolder.accountID {
|
||||
return SidebarOutlineDataSource.dragOperationNone
|
||||
}
|
||||
}
|
||||
if index != NSOutlineViewDropOnItemIndex {
|
||||
outlineView.setDropItem(parentNode, dropChildIndex: NSOutlineViewDropOnItemIndex)
|
||||
}
|
||||
return localDragOperation()
|
||||
}
|
||||
|
||||
func copyFeedInAccount(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? Feed, let destination = parentNode.representedObject as? Container else {
|
||||
return
|
||||
}
|
||||
|
||||
let destination = parentNode.representedObject as? Container
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
destination?.addFeed(feed) { result in
|
||||
|
||||
destination.account?.addFeed(feed, to: destination) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
BatchUpdate.shared.end()
|
||||
break
|
||||
case .failure(let error):
|
||||
BatchUpdate.shared.end()
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func move(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? Feed else {
|
||||
func moveFeedInAccount(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? Feed,
|
||||
let source = node.parent?.representedObject as? Container,
|
||||
let destination = parentNode.representedObject as? Container else {
|
||||
return
|
||||
}
|
||||
|
||||
let source = node.parent?.representedObject as? Container
|
||||
let destination = parentNode.representedObject as? Container
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
source?.removeFeed(feed) { result in
|
||||
source.account?.moveFeed(feed, from: source, to: destination) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
destination?.addFeed(feed) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
BatchUpdate.shared.end()
|
||||
break
|
||||
case .failure(let error):
|
||||
// If the second part of the move failed, try to put the feed back
|
||||
source?.addFeed(feed) { result in}
|
||||
BatchUpdate.shared.end()
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
BatchUpdate.shared.end()
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func copyFeedBetweenAccounts(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? Feed,
|
||||
let destinationAccount = nodeAccount(parentNode),
|
||||
let destinationContainer = parentNode.representedObject as? Container else {
|
||||
return
|
||||
}
|
||||
|
||||
if let existingFeed = destinationAccount.existingFeed(withURL: feed.url) {
|
||||
destinationAccount.addFeed(existingFeed, to: destinationContainer) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
destinationAccount.createFeed(url: feed.url, name: feed.editedName, container: destinationContainer) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func moveFeedBetweenAccounts(node: Node, to parentNode: Node) {
|
||||
guard let feed = node.representedObject as? Feed,
|
||||
let sourceAccount = nodeAccount(node),
|
||||
let sourceContainer = node.parent?.representedObject as? Container,
|
||||
let destinationAccount = nodeAccount(parentNode),
|
||||
let destinationContainer = parentNode.representedObject as? Container else {
|
||||
return
|
||||
}
|
||||
|
||||
if let existingFeed = destinationAccount.existingFeed(withURL: feed.url) {
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
destinationAccount.addFeed(existingFeed, to: destinationContainer) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
sourceAccount.removeFeed(feed, from: sourceContainer) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
destinationAccount.createFeed(url: feed.url, name: feed.editedName, container: destinationContainer) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
sourceAccount.removeFeed(feed, from: sourceContainer) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@ -299,24 +421,22 @@ private extension SidebarOutlineDataSource {
|
||||
return false
|
||||
}
|
||||
|
||||
BatchUpdate.shared.perform {
|
||||
|
||||
draggedNodes.forEach { node in
|
||||
if sameAccount(node, parentNode) {
|
||||
move(node: node, to: parentNode)
|
||||
} else if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||
copy(node: node, to: parentNode)
|
||||
draggedNodes.forEach { node in
|
||||
if sameAccount(node, parentNode) {
|
||||
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||
copyFeedInAccount(node: node, to: parentNode)
|
||||
} else {
|
||||
move(node: node, to: parentNode)
|
||||
moveFeedInAccount(node: node, to: parentNode)
|
||||
}
|
||||
} else {
|
||||
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||
copyFeedBetweenAccounts(node: node, to: parentNode)
|
||||
} else {
|
||||
moveFeedBetweenAccounts(node: node, to: parentNode)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
let allReferencedNodes = draggedNodes.union(Set([parentNode]))
|
||||
let accounts = commonAccountsFor(allReferencedNodes)
|
||||
accounts.forEach { $0.structureDidChange() }
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
@ -352,6 +472,94 @@ private extension SidebarOutlineDataSource {
|
||||
return ancestorThatCanAcceptNonLocalFeed(parentNode)
|
||||
}
|
||||
|
||||
func copyFolderBetweenAccounts(node: Node, to parentNode: Node) {
|
||||
guard let sourceFolder = node.representedObject as? Folder,
|
||||
let destinationAccount = nodeAccount(parentNode) else {
|
||||
return
|
||||
}
|
||||
replicateFolder(sourceFolder, destinationAccount: destinationAccount, completion: {})
|
||||
}
|
||||
|
||||
func moveFolderBetweenAccounts(node: Node, to parentNode: Node) {
|
||||
guard let sourceFolder = node.representedObject as? Folder,
|
||||
let sourceAccount = nodeAccount(node),
|
||||
let destinationAccount = nodeAccount(parentNode) else {
|
||||
return
|
||||
}
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
replicateFolder(sourceFolder, destinationAccount: destinationAccount) {
|
||||
sourceAccount.removeFolder(sourceFolder) { result in
|
||||
BatchUpdate.shared.end()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func replicateFolder(_ folder: Folder, destinationAccount: Account, completion: @escaping () -> Void) {
|
||||
destinationAccount.addFolder(folder.name ?? "") { result in
|
||||
switch result {
|
||||
case .success(let destinationFolder):
|
||||
let group = DispatchGroup()
|
||||
for feed in folder.topLevelFeeds {
|
||||
if let existingFeed = destinationAccount.existingFeed(withURL: feed.url) {
|
||||
group.enter()
|
||||
destinationAccount.addFeed(existingFeed, to: destinationFolder) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
group.enter()
|
||||
destinationAccount.createFeed(url: feed.url, name: feed.editedName, container: destinationFolder) { result in
|
||||
group.leave()
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
completion()
|
||||
}
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func acceptLocalFoldersDrop(_ outlineView: NSOutlineView, _ draggedFolders: Set<PasteboardFolder>, _ parentNode: Node, _ index: Int) -> Bool {
|
||||
guard let draggedNodes = draggedNodes else {
|
||||
return false
|
||||
}
|
||||
|
||||
draggedNodes.forEach { node in
|
||||
if !sameAccount(node, parentNode) {
|
||||
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||
copyFolderBetweenAccounts(node: node, to: parentNode)
|
||||
} else {
|
||||
moveFolderBetweenAccounts(node: node, to: parentNode)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func acceptSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardFeed, _ parentNode: Node, _ index: Int) -> Bool {
|
||||
guard nodeIsDropTarget(parentNode), index == NSOutlineViewDropOnItemIndex else {
|
||||
return false
|
||||
@ -385,44 +593,6 @@ private extension SidebarOutlineDataSource {
|
||||
return false
|
||||
}
|
||||
|
||||
func allParticipantsAreLocalAccounts(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
||||
|
||||
if let account = parentNode.representedObject as? Account {
|
||||
if account.type != .onMyMac {
|
||||
return false
|
||||
}
|
||||
} else if let folder = parentNode.representedObject as? Folder {
|
||||
if folder.account?.type != .onMyMac {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
|
||||
for draggedFeed in draggedFeeds {
|
||||
if draggedFeed.accountType != .onMyMac {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
}
|
||||
|
||||
func allParticipantsAreSameAccount(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
||||
guard let parentAccountID = nodeAccountID(parentNode) else {
|
||||
return false
|
||||
}
|
||||
|
||||
for draggedFeed in draggedFeeds {
|
||||
if draggedFeed.accountID != parentAccountID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func sameAccount(_ node: Node, _ parentNode: Node) -> Bool {
|
||||
if let accountID = nodeAccountID(node), let parentAccountID = nodeAccountID(parentNode) {
|
||||
if accountID == parentAccountID {
|
||||
@ -432,16 +602,21 @@ private extension SidebarOutlineDataSource {
|
||||
return false
|
||||
}
|
||||
|
||||
func nodeAccountID(_ node: Node) -> String? {
|
||||
func nodeAccount(_ node: Node) -> Account? {
|
||||
if let account = node.representedObject as? Account {
|
||||
return account.accountID
|
||||
return account
|
||||
} else if let folder = node.representedObject as? Folder {
|
||||
return folder.account?.accountID
|
||||
return folder.account
|
||||
} else if let feed = node.representedObject as? Feed {
|
||||
return feed.account?.accountID
|
||||
return feed.account
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func nodeAccountID(_ node: Node) -> String? {
|
||||
return nodeAccount(node)?.accountID
|
||||
}
|
||||
|
||||
func nodeHasChildRepresentingAnyDraggedFeed(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
||||
@ -453,6 +628,29 @@ private extension SidebarOutlineDataSource {
|
||||
return false
|
||||
}
|
||||
|
||||
func violatesTagSpecificBehavior(_ parentNode: Node, _ draggedFeed: PasteboardFeed) -> Bool {
|
||||
return violatesTagSpecificBehavior(parentNode, Set([draggedFeed]))
|
||||
}
|
||||
|
||||
func violatesTagSpecificBehavior(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
||||
guard let parentAccount = nodeAccount(parentNode), parentAccount.usesTags else {
|
||||
return false
|
||||
}
|
||||
|
||||
for draggedFeed in draggedFeeds {
|
||||
if parentAccount.accountID != draggedFeed.accountID {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Can't copy to the account when using tags
|
||||
if parentNode.representedObject is Account && (NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func indexWhereDraggedFeedWouldAppear(_ parentNode: Node, _ draggedFeed: PasteboardFeed) -> Int {
|
||||
let draggedFeedWrapper = PasteboardFeedObjectWrapper(pasteboardFeed: draggedFeed)
|
||||
let draggedFeedNode = Node(representedObject: draggedFeedWrapper, parent: nil)
|
||||
@ -463,6 +661,18 @@ private extension SidebarOutlineDataSource {
|
||||
let index = sortedNodes.firstIndex(of: draggedFeedNode)!
|
||||
return index
|
||||
}
|
||||
|
||||
func indexWhereDraggedFolderWouldAppear(_ parentNode: Node, _ draggedFolder: PasteboardFolder) -> Int {
|
||||
let draggedFolderWrapper = PasteboardFolderObjectWrapper(pasteboardFolder: draggedFolder)
|
||||
let draggedFolderNode = Node(representedObject: draggedFolderWrapper, parent: nil)
|
||||
draggedFolderNode.canHaveChildNodes = true
|
||||
let nodes = parentNode.childNodes + [draggedFolderNode]
|
||||
|
||||
// Revisit if the tree controller can ever be sorted in some other way.
|
||||
let sortedNodes = nodes.sortedAlphabeticallyWithFoldersAtEnd()
|
||||
let index = sortedNodes.firstIndex(of: draggedFolderNode)!
|
||||
return index
|
||||
}
|
||||
}
|
||||
|
||||
final class PasteboardFeedObjectWrapper: DisplayNameProvider {
|
||||
@ -476,3 +686,15 @@ final class PasteboardFeedObjectWrapper: DisplayNameProvider {
|
||||
self.pasteboardFeed = pasteboardFeed
|
||||
}
|
||||
}
|
||||
|
||||
final class PasteboardFolderObjectWrapper: DisplayNameProvider {
|
||||
|
||||
var nameForDisplay: String {
|
||||
return pasteboardFolder.name
|
||||
}
|
||||
let pasteboardFolder: PasteboardFolder
|
||||
|
||||
init(pasteboardFolder: PasteboardFolder) {
|
||||
self.pasteboardFolder = pasteboardFolder
|
||||
}
|
||||
}
|
||||
|
@ -60,6 +60,7 @@ protocol SidebarDelegate: class {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDidRequestSidebarSelection(_:)), name: .UserDidRequestSidebarSelection, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(calendarDayChanged(_:)), name: .NSCalendarDayChanged, object: nil)
|
||||
|
||||
outlineView.reloadData()
|
||||
|
||||
@ -165,6 +166,12 @@ protocol SidebarDelegate: class {
|
||||
revealAndSelectRepresentedObject(feed as AnyObject)
|
||||
}
|
||||
|
||||
@objc func calendarDayChanged(_ note: Notification) {
|
||||
DispatchQueue.main.async {
|
||||
SmartFeedsController.shared.todayFeed.fetchUnreadCounts()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Actions
|
||||
|
||||
@IBAction func delete(_ sender: AnyObject?) {
|
||||
|
@ -147,6 +147,7 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .AccountsDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(calendarDayChanged(_:)), name: .NSCalendarDayChanged, object: nil)
|
||||
|
||||
didRegisterForNotifications = true
|
||||
}
|
||||
@ -511,6 +512,14 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
|
||||
self.fontSize = AppDefaults.timelineFontSize
|
||||
self.sortDirection = AppDefaults.timelineSortDirection
|
||||
}
|
||||
|
||||
@objc func calendarDayChanged(_ note: Notification) {
|
||||
if representedObjectsContainsTodayFeed() {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.fetchArticles()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reloading Data
|
||||
|
||||
@ -966,6 +975,10 @@ private extension TimelineViewController {
|
||||
return representedObjects?.contains(where: { $0 is PseudoFeed}) ?? false
|
||||
}
|
||||
|
||||
func representedObjectsContainsTodayFeed() -> Bool {
|
||||
return representedObjects?.contains(where: { $0 === SmartFeedsController.shared.todayFeed }) ?? false
|
||||
}
|
||||
|
||||
func representedObjectsContainsAnyFeed(_ feeds: Set<Feed>) -> Bool {
|
||||
|
||||
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
||||
|
@ -50,12 +50,16 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
func deleteElement(_ element:ScriptingObject) {
|
||||
if let scriptableFolder = element as? ScriptableFolder {
|
||||
BatchUpdate.shared.perform {
|
||||
account.deleteFolder(scriptableFolder.folder) { result in
|
||||
account.removeFolder(scriptableFolder.folder) { result in
|
||||
}
|
||||
}
|
||||
} else if let scriptableFeed = element as? ScriptableFeed {
|
||||
BatchUpdate.shared.perform {
|
||||
account.deleteFeed(scriptableFeed.feed) { result in
|
||||
var container: Container? = nil
|
||||
if let scriptableFolder = scriptableFeed.container as? ScriptableFolder {
|
||||
container = scriptableFolder.folder
|
||||
}
|
||||
account.removeFeed(scriptableFeed.feed, from: container) { result in
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -91,7 +91,9 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
|
||||
if let existingFeed = account.existingFeed(withURL:url) {
|
||||
return self.scriptableFeed(existingFeed, account:account, folder:folder)
|
||||
}
|
||||
|
||||
|
||||
let container: Container = folder != nil ? folder! : account
|
||||
|
||||
// at this point, we need to download the feed and parse it.
|
||||
// RS Parser does the callback for the download on the main thread (which it probably shouldn't?)
|
||||
// because we can't wait here (on the main thread, maybe) for the callback, we have to return from this function
|
||||
@ -100,27 +102,12 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
|
||||
// suspendExecution(). When we get the callback, we can supply the event result and call resumeExecution()
|
||||
command.suspendExecution()
|
||||
|
||||
account.createFeed(url: url) { result in
|
||||
account.createFeed(url: url, name: titleFromArgs, container: container) { result in
|
||||
switch result {
|
||||
case .success(let feed):
|
||||
|
||||
if let editedName = titleFromArgs {
|
||||
account.renameFeed(feed, to: editedName) { result in
|
||||
}
|
||||
}
|
||||
|
||||
// add the feed, putting it in a folder if needed
|
||||
account.addFeed(feed) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
|
||||
let scriptableFeed = self.scriptableFeed(feed, account:account, folder:folder)
|
||||
command.resumeExecution(withResult:scriptableFeed.objectSpecifier)
|
||||
case .failure:
|
||||
command.resumeExecution(withResult:nil)
|
||||
}
|
||||
}
|
||||
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
|
||||
let scriptableFeed = self.scriptableFeed(feed, account:account, folder:folder)
|
||||
command.resumeExecution(withResult:scriptableFeed.objectSpecifier)
|
||||
case .failure:
|
||||
command.resumeExecution(withResult:nil)
|
||||
}
|
||||
|
@ -53,7 +53,7 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai
|
||||
func deleteElement(_ element:ScriptingObject) {
|
||||
if let scriptableFeed = element as? ScriptableFeed {
|
||||
BatchUpdate.shared.perform {
|
||||
folder.account?.deleteFeed(scriptableFeed.feed) { result in }
|
||||
folder.account?.removeFeed(scriptableFeed.feed, from: folder) { result in }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -235,7 +235,7 @@
|
||||
84A37CB5201ECD610087C5AF /* RenameWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A37CB4201ECD610087C5AF /* RenameWindowController.swift */; };
|
||||
84A3EE5F223B667F00557320 /* DefaultFeeds.opml in Resources */ = {isa = PBXBuildFile; fileRef = 84A3EE52223B667F00557320 /* DefaultFeeds.opml */; };
|
||||
84A3EE61223B667F00557320 /* DefaultFeeds.opml in Resources */ = {isa = PBXBuildFile; fileRef = 84A3EE52223B667F00557320 /* DefaultFeeds.opml */; };
|
||||
84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */; };
|
||||
84AD1EAA2031617300BC20B7 /* PasteboardFolder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EA92031617300BC20B7 /* PasteboardFolder.swift */; };
|
||||
84AD1EBA2031649C00BC20B7 /* SmartFeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */; };
|
||||
84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EBB2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift */; };
|
||||
84B7178C201E66580091657D /* SidebarViewController+ContextualMenus.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */; };
|
||||
@ -843,7 +843,7 @@
|
||||
84A1500420048DDF0046AD9A /* SendToMarsEditCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SendToMarsEditCommand.swift; sourceTree = "<group>"; };
|
||||
84A37CB4201ECD610087C5AF /* RenameWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RenameWindowController.swift; sourceTree = "<group>"; };
|
||||
84A3EE52223B667F00557320 /* DefaultFeeds.opml */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xml; path = DefaultFeeds.opml; sourceTree = "<group>"; };
|
||||
84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FolderPasteboardWriter.swift; sourceTree = "<group>"; };
|
||||
84AD1EA92031617300BC20B7 /* PasteboardFolder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteboardFolder.swift; sourceTree = "<group>"; };
|
||||
84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmartFeedPasteboardWriter.swift; sourceTree = "<group>"; };
|
||||
84AD1EBB2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarOutlineDataSource.swift; sourceTree = "<group>"; };
|
||||
84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SidebarViewController+ContextualMenus.swift"; sourceTree = "<group>"; };
|
||||
@ -1373,7 +1373,7 @@
|
||||
849A97601ED9EB96007D329B /* SidebarOutlineView.swift */,
|
||||
849A97631ED9EB96007D329B /* UnreadCountView.swift */,
|
||||
848D578D21543519005FFAD5 /* PasteboardFeed.swift */,
|
||||
84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */,
|
||||
84AD1EA92031617300BC20B7 /* PasteboardFolder.swift */,
|
||||
849A97821ED9EC63007D329B /* SidebarStatusBarView.swift */,
|
||||
844B5B6A1FEA224000C7C76A /* Keyboard */,
|
||||
845A29251FC928C7007B49E3 /* Cell */,
|
||||
@ -2476,7 +2476,7 @@
|
||||
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
|
||||
55E15BCC229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift in Sources */,
|
||||
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */,
|
||||
84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */,
|
||||
84AD1EAA2031617300BC20B7 /* PasteboardFolder.swift in Sources */,
|
||||
5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */,
|
||||
84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */,
|
||||
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */,
|
||||
|
@ -136,13 +136,13 @@ private struct SidebarItemSpecifier {
|
||||
|
||||
if let feed = feed {
|
||||
BatchUpdate.shared.start()
|
||||
account?.deleteFeed(feed) { result in
|
||||
account?.removeFeed(feed, from: path.resolveContainer()) { result in
|
||||
BatchUpdate.shared.end()
|
||||
self.checkResult(result)
|
||||
}
|
||||
} else if let folder = folder {
|
||||
BatchUpdate.shared.start()
|
||||
account?.deleteFolder(folder) { result in
|
||||
account?.removeFolder(folder) { result in
|
||||
BatchUpdate.shared.end()
|
||||
self.checkResult(result)
|
||||
}
|
||||
@ -161,12 +161,12 @@ private struct SidebarItemSpecifier {
|
||||
|
||||
private func restoreFeed() {
|
||||
|
||||
guard let account = account, let feed = feed else {
|
||||
guard let account = account, let feed = feed, let container = path.resolveContainer() else {
|
||||
return
|
||||
}
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
account.restoreFeed(feed, folder: resolvedFolder()) { result in
|
||||
account.restoreFeed(feed, container: container) { result in
|
||||
BatchUpdate.shared.end()
|
||||
self.checkResult(result)
|
||||
}
|
||||
@ -187,10 +187,6 @@ private struct SidebarItemSpecifier {
|
||||
|
||||
}
|
||||
|
||||
private func resolvedFolder() -> Folder? {
|
||||
return path.resolveContainer() as? Folder
|
||||
}
|
||||
|
||||
private func checkResult(_ result: Result<Void, Error>) {
|
||||
|
||||
switch result {
|
||||
|
@ -80,13 +80,10 @@ class AddFeedViewController: UITableViewController, AddContainerViewControllerCh
|
||||
let container = pickerData.containers[folderPickerView.selectedRow(inComponent: 0)]
|
||||
|
||||
var account: Account?
|
||||
var folder: Folder?
|
||||
if let containerAccount = container as? Account {
|
||||
account = containerAccount
|
||||
}
|
||||
if let containerFolder = container as? Folder, let containerAccount = containerFolder.account {
|
||||
} else if let containerFolder = container as? Folder, let containerAccount = containerFolder.account {
|
||||
account = containerAccount
|
||||
folder = containerFolder
|
||||
}
|
||||
|
||||
if account!.hasFeed(withURL: url.absoluteString) {
|
||||
@ -94,26 +91,28 @@ class AddFeedViewController: UITableViewController, AddContainerViewControllerCh
|
||||
return
|
||||
}
|
||||
|
||||
let title = nameTextField.text
|
||||
|
||||
delegate?.processingDidBegin()
|
||||
BatchUpdate.shared.start()
|
||||
|
||||
account!.createFeed(url: url.absoluteString, name: nameTextField.text, container: container) { result in
|
||||
|
||||
account!.createFeed(url: url.absoluteString) { [weak self] result in
|
||||
BatchUpdate.shared.end()
|
||||
|
||||
switch result {
|
||||
case .success(let feed):
|
||||
self?.processFeed(feed, account: account!, folder: folder, url: url, title: title)
|
||||
self.delegate?.processingDidEnd()
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
|
||||
case .failure(let error):
|
||||
switch error {
|
||||
case AccountError.createErrorAlreadySubscribed:
|
||||
self?.showAlreadySubscribedError()
|
||||
self?.delegate?.processingDidCancel()
|
||||
self.showAlreadySubscribedError()
|
||||
self.delegate?.processingDidCancel()
|
||||
case AccountError.createErrorNotFound:
|
||||
self?.showNoFeedsErrorMessage()
|
||||
self?.delegate?.processingDidCancel()
|
||||
self.showNoFeedsErrorMessage()
|
||||
self.delegate?.processingDidCancel()
|
||||
default:
|
||||
self?.presentError(error)
|
||||
self?.delegate?.processingDidCancel()
|
||||
self.presentError(error)
|
||||
self.delegate?.processingDidCancel()
|
||||
}
|
||||
}
|
||||
|
||||
@ -178,45 +177,6 @@ private extension AddFeedViewController {
|
||||
presentError(title: title, message: message as String)
|
||||
}
|
||||
|
||||
func processFeed(_ feed: Feed, account: Account, folder: Folder?, url: URL, title: String?) {
|
||||
|
||||
if let title = title {
|
||||
account.renameFeed(feed, to: title) { [weak self] result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self?.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let folder = folder {
|
||||
folder.addFeed(feed) { [weak self] result in
|
||||
switch result {
|
||||
case .success:
|
||||
self?.delegate?.processingDidEnd()
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
|
||||
case .failure(let error):
|
||||
self?.delegate?.processingDidEnd()
|
||||
self?.presentError(error)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
account.addFeed(feed) { [weak self] result in
|
||||
switch result {
|
||||
case .success:
|
||||
self?.delegate?.processingDidEnd()
|
||||
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
|
||||
case .failure(let error):
|
||||
self?.delegate?.processingDidEnd()
|
||||
self?.presentError(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension AddFeedViewController: UITextFieldDelegate {
|
||||
|
@ -373,22 +373,17 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
||||
}()
|
||||
|
||||
// Move the Feed
|
||||
let source = sourceNode.parent?.representedObject as? Container
|
||||
let destination = destParentNode?.representedObject as? Container
|
||||
source?.removeFeed(feed) { [weak self] result in
|
||||
guard let source = sourceNode.parent?.representedObject as? Container, let destination = destParentNode?.representedObject as? Container else {
|
||||
return
|
||||
}
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
source.account?.moveFeed(feed, from: source, to: destination) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
destination?.addFeed(feed) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
source?.addFeed(feed) { result in }
|
||||
self?.presentError(error)
|
||||
}
|
||||
}
|
||||
BatchUpdate.shared.end()
|
||||
case .failure(let error):
|
||||
self?.presentError(error)
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1 +1 @@
|
||||
Subproject commit 718f27db5016298a9cc650764d5d92ce54ce1e1a
|
||||
Subproject commit 93b481897d84849345daa965bd8e11860c9422e7
|
Loading…
x
Reference in New Issue
Block a user