mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2025-02-09 00:28:53 +01:00
Enable folders to be dropped in a move or copy between accounts
This commit is contained in:
parent
1352dda8aa
commit
30c21bb125
@ -411,6 +411,10 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
delegate.restoreFeed(for: self, feed: feed, container: container, completion: completion)
|
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) {
|
public func removeFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
delegate.removeFolder(for: self, with: folder, completion: completion)
|
delegate.removeFolder(for: self, with: folder, completion: completion)
|
||||||
}
|
}
|
||||||
|
@ -29,6 +29,7 @@ protocol AccountDelegate {
|
|||||||
|
|
||||||
func importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void)
|
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 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)
|
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
|
||||||
|
@ -143,25 +143,6 @@ final class FeedbinAPICaller: NSObject {
|
|||||||
transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion)
|
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) {
|
func retrieveSubscriptions(completion: @escaping (Result<[FeedbinSubscription]?, Error>) -> Void) {
|
||||||
|
|
||||||
let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json")
|
let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json")
|
||||||
|
@ -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) {
|
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
|
caller.renameTag(oldName: folder.name ?? "", newName: name) { result in
|
||||||
@ -258,31 +266,26 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// After we successfully delete at Feedbin, we add all the feeds to the account to save them. We then
|
let group = DispatchGroup()
|
||||||
// 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.
|
for feed in folder.topLevelFeeds {
|
||||||
caller.deleteTag(name: folder.name ?? "") { result in
|
group.enter()
|
||||||
switch result {
|
removeFeed(for: account, with: feed, from: folder) { result in
|
||||||
case .success(let taggings):
|
group.leave()
|
||||||
DispatchQueue.main.sync {
|
switch result {
|
||||||
BatchUpdate.shared.perform {
|
case .success:
|
||||||
for feed in folder.topLevelFeeds {
|
break
|
||||||
account.addFeed(feed)
|
case .failure(let error):
|
||||||
self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
os_log(.error, log: self.log, "Remove feed error: %@.", error.localizedDescription)
|
||||||
}
|
|
||||||
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))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
func createFeed(for account: Account, url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
@ -885,8 +888,7 @@ private extension FeedbinAccountDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func refreshArticles(_ account: Account, completion: @escaping (() -> Void)) {
|
func refreshArticles(_ account: Account, completion: @escaping (() -> Void)) {
|
||||||
@ -1122,7 +1124,7 @@ private extension FeedbinAccountDelegate {
|
|||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
feed.folderRelationship?.removeValue(forKey: folder.name ?? "")
|
self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
||||||
folder.removeFeed(feed)
|
folder.removeFeed(feed)
|
||||||
account.addFeedIfNotInAnyFolder(feed)
|
account.addFeedIfNotInAnyFolder(feed)
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
|
@ -91,16 +91,6 @@ final class LocalAccountDelegate: AccountDelegate {
|
|||||||
completion(.success(()))
|
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 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) {
|
func createFeed(for account: Account, url urlString: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
|
|
||||||
@ -143,6 +133,24 @@ final class LocalAccountDelegate: AccountDelegate {
|
|||||||
completion(.success(()))
|
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) {
|
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
account.addFolder(folder)
|
account.addFolder(folder)
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
|
@ -97,6 +97,10 @@ import Account
|
|||||||
}
|
}
|
||||||
let parentNode = nodeForItem(item)
|
let parentNode = nodeForItem(item)
|
||||||
|
|
||||||
|
if let draggedFolders = draggedFolders {
|
||||||
|
return acceptLocalFoldersDrop(outlineView, draggedFolders, parentNode, index)
|
||||||
|
}
|
||||||
|
|
||||||
if let draggedFeeds = draggedFeeds {
|
if let draggedFeeds = draggedFeeds {
|
||||||
let contentsType = draggedFeedContentsType(draggedFeeds)
|
let contentsType = draggedFeedContentsType(draggedFeeds)
|
||||||
|
|
||||||
@ -284,7 +288,7 @@ private extension SidebarOutlineDataSource {
|
|||||||
return localDragOperation()
|
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 {
|
guard let feed = node.representedObject as? Feed, let destination = parentNode.representedObject as? Container else {
|
||||||
return
|
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,
|
guard let feed = node.representedObject as? Feed,
|
||||||
let source = node.parent?.representedObject as? Container,
|
let source = node.parent?.representedObject as? Container,
|
||||||
let destination = parentNode.representedObject as? Container else {
|
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,
|
guard let feed = node.representedObject as? Feed,
|
||||||
let destinationAccount = nodeAccount(parentNode),
|
let destinationAccount = nodeAccount(parentNode),
|
||||||
let destinationContainer = parentNode.representedObject as? Container else {
|
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,
|
guard let feed = node.representedObject as? Feed,
|
||||||
let sourceAccount = nodeAccount(node),
|
let sourceAccount = nodeAccount(node),
|
||||||
let sourceContainer = node.parent?.representedObject as? Container,
|
let sourceContainer = node.parent?.representedObject as? Container,
|
||||||
@ -405,15 +409,15 @@ private extension SidebarOutlineDataSource {
|
|||||||
draggedNodes.forEach { node in
|
draggedNodes.forEach { node in
|
||||||
if sameAccount(node, parentNode) {
|
if sameAccount(node, parentNode) {
|
||||||
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||||
copyInAccount(node: node, to: parentNode)
|
copyFeedInAccount(node: node, to: parentNode)
|
||||||
} else {
|
} else {
|
||||||
moveInAccount(node: node, to: parentNode)
|
moveFeedInAccount(node: node, to: parentNode)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||||
copyBetweenAccounts(node: node, to: parentNode)
|
copyFeedBetweenAccounts(node: node, to: parentNode)
|
||||||
} else {
|
} else {
|
||||||
moveBetweenAccounts(node: node, to: parentNode)
|
moveFeedBetweenAccounts(node: node, to: parentNode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -453,6 +457,80 @@ private extension SidebarOutlineDataSource {
|
|||||||
return ancestorThatCanAcceptNonLocalFeed(parentNode)
|
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 {
|
func acceptSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardFeed, _ parentNode: Node, _ index: Int) -> Bool {
|
||||||
guard nodeIsDropTarget(parentNode), index == NSOutlineViewDropOnItemIndex else {
|
guard nodeIsDropTarget(parentNode), index == NSOutlineViewDropOnItemIndex else {
|
||||||
return false
|
return false
|
||||||
|
Loading…
x
Reference in New Issue
Block a user