Enable folders to be dropped in a move or copy between accounts

This commit is contained in:
Maurice Parker 2019-05-30 14:36:21 -05:00
parent 1352dda8aa
commit 30c21bb125
6 changed files with 135 additions and 61 deletions

View File

@ -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<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)
}

View File

@ -29,6 +29,7 @@ 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 removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)

View File

@ -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")

View File

@ -231,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
@ -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 {
let group = DispatchGroup()
for feed in folder.topLevelFeeds {
account.addFeed(feed)
self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
}
account.removeFolder(folder)
}
completion(.success(()))
}
self.syncTaggings(account, taggings)
group.enter()
removeFeed(for: account, with: feed, from: folder) { result in
group.leave()
switch result {
case .success:
break
case .failure(let error):
DispatchQueue.main.async {
let wrappedError = AccountError.wrappedError(error: error, account: account)
completion(.failure(wrappedError))
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<Feed, Error>) -> Void) {
@ -886,7 +889,6 @@ 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(()))

View File

@ -92,16 +92,6 @@ final class LocalAccountDelegate: AccountDelegate {
}
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(()))
}
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 {
@ -143,6 +133,24 @@ final class LocalAccountDelegate: AccountDelegate {
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 {
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(()))
}
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
account.addFolder(folder)
completion(.success(()))

View File

@ -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<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