From 30c21bb1259a58e4a4ab034f46c3c79d658c2995 Mon Sep 17 00:00:00 2001 From: Maurice Parker Date: Thu, 30 May 2019 14:36:21 -0500 Subject: [PATCH] Enable folders to be dropped in a move or copy between accounts --- Frameworks/Account/Account.swift | 4 + Frameworks/Account/AccountDelegate.swift | 1 + .../Account/Feedbin/FeedbinAPICaller.swift | 19 ---- .../Feedbin/FeedbinAccountDelegate.swift | 50 +++++----- .../LocalAccount/LocalAccountDelegate.swift | 28 ++++-- .../Sidebar/SidebarOutlineDataSource.swift | 94 +++++++++++++++++-- 6 files changed, 135 insertions(+), 61 deletions(-) diff --git a/Frameworks/Account/Account.swift b/Frameworks/Account/Account.swift index 71e6d67bd..5bc4cdfd0 100644 --- a/Frameworks/Account/Account.swift +++ b/Frameworks/Account/Account.swift @@ -411,6 +411,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container, delegate.restoreFeed(for: self, feed: feed, container: container, completion: completion) } + public func addFolder(_ name: String, completion: @escaping (Result) -> Void) { + delegate.addFolder(for: self, name: name, completion: completion) + } + public func removeFolder(_ folder: Folder, completion: @escaping (Result) -> Void) { delegate.removeFolder(for: self, with: folder, completion: completion) } diff --git a/Frameworks/Account/AccountDelegate.swift b/Frameworks/Account/AccountDelegate.swift index 34b41db8f..56005273a 100644 --- a/Frameworks/Account/AccountDelegate.swift +++ b/Frameworks/Account/AccountDelegate.swift @@ -29,6 +29,7 @@ protocol AccountDelegate { func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result) -> Void) + func addFolder(for account: Account, name: String, completion: @escaping (Result) -> Void) func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) diff --git a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift index ba1e0fa94..d8a8bc555 100644 --- a/Frameworks/Account/Feedbin/FeedbinAPICaller.swift +++ b/Frameworks/Account/Feedbin/FeedbinAPICaller.swift @@ -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") diff --git a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift index 1ec25ca63..fbd830e9e 100644 --- a/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift +++ b/Frameworks/Account/Feedbin/FeedbinAccountDelegate.swift @@ -231,6 +231,14 @@ final class FeedbinAccountDelegate: AccountDelegate { } + func addFolder(for account: Account, name: String, completion: @escaping (Result) -> 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) { caller.renameTag(oldName: folder.name ?? "", newName: name) { result in @@ -258,31 +266,26 @@ final class FeedbinAccountDelegate: AccountDelegate { 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.removeFolder(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, name: String?, container: Container, completion: @escaping (Result) -> Void) { @@ -885,8 +888,7 @@ private extension FeedbinAccountDelegate { } } - - + } func refreshArticles(_ account: Account, completion: @escaping (() -> Void)) { @@ -1122,7 +1124,7 @@ private extension FeedbinAccountDelegate { switch result { case .success: DispatchQueue.main.async { - feed.folderRelationship?.removeValue(forKey: folder.name ?? "") + self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "") folder.removeFeed(feed) account.addFeedIfNotInAnyFolder(feed) completion(.success(())) diff --git a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift index 759c411b0..d0d4c5435 100644 --- a/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift +++ b/Frameworks/Account/LocalAccount/LocalAccountDelegate.swift @@ -91,16 +91,6 @@ final class LocalAccountDelegate: AccountDelegate { completion(.success(())) } - - func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result) -> Void) { - folder.name = name - completion(.success(())) - } - - func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { - account.removeFolder(folder) - completion(.success(())) - } func createFeed(for account: Account, url urlString: String, name: String?, container: Container, completion: @escaping (Result) -> Void) { @@ -143,6 +133,24 @@ final class LocalAccountDelegate: AccountDelegate { completion(.success(())) } + func addFolder(for account: Account, name: String, completion: @escaping (Result) -> 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) { + folder.name = name + completion(.success(())) + } + + func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result) -> Void) { + account.removeFolder(folder) + completion(.success(())) + } + func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result) -> Void) { account.addFolder(folder) completion(.success(())) diff --git a/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift b/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift index 00eb7711a..1eff773aa 100644 --- a/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift +++ b/Mac/MainWindow/Sidebar/SidebarOutlineDataSource.swift @@ -97,6 +97,10 @@ import Account } let parentNode = nodeForItem(item) + if let draggedFolders = draggedFolders { + return acceptLocalFoldersDrop(outlineView, draggedFolders, parentNode, index) + } + if let draggedFeeds = draggedFeeds { let contentsType = draggedFeedContentsType(draggedFeeds) @@ -284,7 +288,7 @@ private extension SidebarOutlineDataSource { return localDragOperation() } - func copyInAccount(node: Node, to parentNode: Node) { + func copyFeedInAccount(node: Node, to parentNode: Node) { guard let feed = node.representedObject as? Feed, let destination = parentNode.representedObject as? Container else { return } @@ -299,7 +303,7 @@ private extension SidebarOutlineDataSource { } } - func moveInAccount(node: Node, to parentNode: Node) { + 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 { @@ -317,7 +321,7 @@ private extension SidebarOutlineDataSource { } } - func copyBetweenAccounts(node: Node, to parentNode: Node) { + 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 { @@ -345,7 +349,7 @@ private extension SidebarOutlineDataSource { } } - func moveBetweenAccounts(node: Node, to parentNode: Node) { + 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, @@ -405,15 +409,15 @@ private extension SidebarOutlineDataSource { draggedNodes.forEach { node in if sameAccount(node, parentNode) { if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false { - copyInAccount(node: node, to: parentNode) + copyFeedInAccount(node: node, to: parentNode) } else { - moveInAccount(node: node, to: parentNode) + moveFeedInAccount(node: node, to: parentNode) } } else { if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false { - copyBetweenAccounts(node: node, to: parentNode) + copyFeedBetweenAccounts(node: node, to: parentNode) } else { - moveBetweenAccounts(node: node, to: parentNode) + moveFeedBetweenAccounts(node: node, to: parentNode) } } } @@ -453,6 +457,80 @@ 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 { + 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) + } + } + + } + + func acceptLocalFoldersDrop(_ outlineView: NSOutlineView, _ draggedFolders: Set, _ 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