Merge branch 'main' of https://github.com/Ranchero-Software/NetNewsWire into main
This commit is contained in:
commit
e9f7cdb0c6
|
@ -28,6 +28,11 @@ public enum AccountBehavior: Equatable {
|
||||||
*/
|
*/
|
||||||
case disallowFeedInRootFolder
|
case disallowFeedInRootFolder
|
||||||
|
|
||||||
|
/**
|
||||||
|
Account doesn't support a feed being in more than one folder.
|
||||||
|
*/
|
||||||
|
case disallowFeedInMultipleFolders
|
||||||
|
|
||||||
/**
|
/**
|
||||||
Account doesn't support folders
|
Account doesn't support folders
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -29,7 +29,7 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ReaderAPI")
|
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "ReaderAPI")
|
||||||
|
|
||||||
var behaviors: AccountBehaviors {
|
var behaviors: AccountBehaviors {
|
||||||
var behaviors: AccountBehaviors = [.disallowOPMLImports]
|
var behaviors: AccountBehaviors = [.disallowOPMLImports, .disallowFeedInMultipleFolders]
|
||||||
if variant == .freshRSS {
|
if variant == .freshRSS {
|
||||||
behaviors.append(.disallowFeedInRootFolder)
|
behaviors.append(.disallowFeedInRootFolder)
|
||||||
}
|
}
|
||||||
|
@ -186,8 +186,9 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||||
caller.retrieveItemIDs(type: .unread) { result in
|
caller.retrieveItemIDs(type: .unread) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let articleIDs):
|
case .success(let articleIDs):
|
||||||
self.syncArticleReadState(account: account, articleIDs: articleIDs)
|
self.syncArticleReadState(account: account, articleIDs: articleIDs) {
|
||||||
group.leave()
|
group.leave()
|
||||||
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log(.info, log: self.log, "Retrieving unread entries failed: %@.", error.localizedDescription)
|
os_log(.info, log: self.log, "Retrieving unread entries failed: %@.", error.localizedDescription)
|
||||||
group.leave()
|
group.leave()
|
||||||
|
@ -199,8 +200,9 @@ final class ReaderAPIAccountDelegate: AccountDelegate {
|
||||||
caller.retrieveItemIDs(type: .starred) { result in
|
caller.retrieveItemIDs(type: .starred) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let articleIDs):
|
case .success(let articleIDs):
|
||||||
self.syncArticleStarredState(account: account, articleIDs: articleIDs)
|
self.syncArticleStarredState(account: account, articleIDs: articleIDs) {
|
||||||
group.leave()
|
group.leave()
|
||||||
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription)
|
os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription)
|
||||||
group.leave()
|
group.leave()
|
||||||
|
@ -918,7 +920,7 @@ private extension ReaderAPIAccountDelegate {
|
||||||
let group = DispatchGroup()
|
let group = DispatchGroup()
|
||||||
|
|
||||||
let articleIDs = Array(fetchedArticleIDs)
|
let articleIDs = Array(fetchedArticleIDs)
|
||||||
let chunkedArticleIDs = articleIDs.chunked(into: 100)
|
let chunkedArticleIDs = articleIDs.chunked(into: 150)
|
||||||
|
|
||||||
self.refreshProgress.addToNumberOfTasksAndRemaining(chunkedArticleIDs.count - 1)
|
self.refreshProgress.addToNumberOfTasksAndRemaining(chunkedArticleIDs.count - 1)
|
||||||
|
|
||||||
|
@ -1001,7 +1003,7 @@ private extension ReaderAPIAccountDelegate {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncArticleReadState(account: Account, articleIDs: [String]?) {
|
func syncArticleReadState(account: Account, articleIDs: [String]?, completion: @escaping (() -> Void)) {
|
||||||
guard let articleIDs = articleIDs else {
|
guard let articleIDs = articleIDs else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1016,13 +1018,25 @@ private extension ReaderAPIAccountDelegate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
|
||||||
// Mark articles as unread
|
// Mark articles as unread
|
||||||
let deltaUnreadArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
|
let deltaUnreadArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentUnreadArticleIDs)
|
||||||
account.markAsUnread(deltaUnreadArticleIDs)
|
group.enter()
|
||||||
|
account.markAsUnread(deltaUnreadArticleIDs) { _ in
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
|
||||||
// Mark articles as read
|
// Mark articles as read
|
||||||
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
|
let deltaReadArticleIDs = currentUnreadArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
|
||||||
account.markAsRead(deltaReadArticleIDs)
|
group.enter()
|
||||||
|
account.markAsRead(deltaReadArticleIDs) { _ in
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
|
||||||
|
group.notify(queue: DispatchQueue.main) {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1037,7 +1051,7 @@ private extension ReaderAPIAccountDelegate {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func syncArticleStarredState(account: Account, articleIDs: [String]?) {
|
func syncArticleStarredState(account: Account, articleIDs: [String]?, completion: @escaping (() -> Void)) {
|
||||||
guard let articleIDs = articleIDs else {
|
guard let articleIDs = articleIDs else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -1052,13 +1066,25 @@ private extension ReaderAPIAccountDelegate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let group = DispatchGroup()
|
||||||
|
|
||||||
// Mark articles as starred
|
// Mark articles as starred
|
||||||
let deltaStarredArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentStarredArticleIDs)
|
let deltaStarredArticleIDs = updatableReaderUnreadArticleIDs.subtracting(currentStarredArticleIDs)
|
||||||
account.markAsStarred(deltaStarredArticleIDs)
|
group.enter()
|
||||||
|
account.markAsStarred(deltaStarredArticleIDs) { _ in
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
|
||||||
// Mark articles as unstarred
|
// Mark articles as unstarred
|
||||||
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
|
let deltaUnstarredArticleIDs = currentStarredArticleIDs.subtracting(updatableReaderUnreadArticleIDs)
|
||||||
account.markAsUnstarred(deltaUnstarredArticleIDs)
|
group.enter()
|
||||||
|
account.markAsUnstarred(deltaUnstarredArticleIDs) { _ in
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
|
||||||
|
group.notify(queue: DispatchQueue.main) {
|
||||||
|
completion()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -631,52 +631,76 @@ private extension SidebarOutlineDataSource {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func violatesAccountSpecificBehavior(_ parentNode: Node, _ draggedFeed: PasteboardWebFeed) -> Bool {
|
func violatesAccountSpecificBehavior(_ dropTargetNode: Node, _ draggedFeed: PasteboardWebFeed) -> Bool {
|
||||||
return violatesAccountSpecificBehavior(parentNode, Set([draggedFeed]))
|
return violatesAccountSpecificBehavior(dropTargetNode, Set([draggedFeed]))
|
||||||
}
|
}
|
||||||
|
|
||||||
func violatesAccountSpecificBehavior(_ parentNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
|
func violatesAccountSpecificBehavior(_ dropTargetNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
|
||||||
if violatesDisallowFeedInRootFolder(parentNode) {
|
if violatesDisallowFeedInRootFolder(dropTargetNode) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
if violatesDisallowFeedCopyInRootFolder(parentNode, draggedFeeds) {
|
if violatesDisallowFeedCopyInRootFolder(dropTargetNode, draggedFeeds) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if violatesDisallowFeedInMultipleFolders(dropTargetNode, draggedFeeds) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func violatesDisallowFeedInRootFolder(_ parentNode: Node) -> Bool {
|
func violatesDisallowFeedInRootFolder(_ dropTargetNode: Node) -> Bool {
|
||||||
guard let parentAccount = nodeAccount(parentNode), parentAccount.behaviors.contains(.disallowFeedInRootFolder) else {
|
guard let parentAccount = nodeAccount(dropTargetNode), parentAccount.behaviors.contains(.disallowFeedInRootFolder) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if parentNode.representedObject is Account {
|
if dropTargetNode.representedObject is Account {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func violatesDisallowFeedCopyInRootFolder(_ parentNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
|
func violatesDisallowFeedCopyInRootFolder(_ dropTargetNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
|
||||||
guard let parentAccount = nodeAccount(parentNode), parentAccount.behaviors.contains(.disallowFeedCopyInRootFolder) else {
|
guard let dropTargetAccount = nodeAccount(dropTargetNode), dropTargetAccount.behaviors.contains(.disallowFeedCopyInRootFolder) else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
for draggedFeed in draggedFeeds {
|
for draggedFeed in draggedFeeds {
|
||||||
if parentAccount.accountID != draggedFeed.accountID {
|
if dropTargetAccount.accountID != draggedFeed.accountID {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if parentNode.representedObject is Account && (NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false) {
|
if dropTargetNode.representedObject is Account && (NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func violatesDisallowFeedInMultipleFolders(_ dropTargetNode: Node, _ draggedFeeds: Set<PasteboardWebFeed>) -> Bool {
|
||||||
|
guard let dropTargetAccount = nodeAccount(dropTargetNode), dropTargetAccount.behaviors.contains(.disallowFeedInMultipleFolders) else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for draggedFeed in draggedFeeds {
|
||||||
|
if dropTargetAccount.accountID == draggedFeed.accountID {
|
||||||
|
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if dropTargetAccount.hasWebFeed(withURL: draggedFeed.url) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func indexWhereDraggedFeedWouldAppear(_ parentNode: Node, _ draggedFeed: PasteboardWebFeed) -> Int {
|
func indexWhereDraggedFeedWouldAppear(_ parentNode: Node, _ draggedFeed: PasteboardWebFeed) -> Int {
|
||||||
let draggedFeedWrapper = PasteboardFeedObjectWrapper(pasteboardFeed: draggedFeed)
|
let draggedFeedWrapper = PasteboardFeedObjectWrapper(pasteboardFeed: draggedFeed)
|
||||||
let draggedFeedNode = Node(representedObject: draggedFeedWrapper, parent: nil)
|
let draggedFeedNode = Node(representedObject: draggedFeedWrapper, parent: nil)
|
||||||
|
|
|
@ -18,6 +18,7 @@ final class MasterFeedTableViewIdentifier: NSObject, NSCopying {
|
||||||
|
|
||||||
let isEditable: Bool
|
let isEditable: Bool
|
||||||
let isPsuedoFeed: Bool
|
let isPsuedoFeed: Bool
|
||||||
|
let isAccount: Bool
|
||||||
let isFolder: Bool
|
let isFolder: Bool
|
||||||
let isWebFeed: Bool
|
let isWebFeed: Bool
|
||||||
|
|
||||||
|
@ -26,6 +27,19 @@ final class MasterFeedTableViewIdentifier: NSObject, NSCopying {
|
||||||
let unreadCount: Int
|
let unreadCount: Int
|
||||||
let childCount: Int
|
let childCount: Int
|
||||||
|
|
||||||
|
var account: Account? {
|
||||||
|
if isAccount, let containerID = containerID {
|
||||||
|
return AccountManager.shared.existingContainer(with: containerID) as? Account
|
||||||
|
}
|
||||||
|
if isFolder, let parentContainerID = parentContainerID {
|
||||||
|
return AccountManager.shared.existingContainer(with: parentContainerID) as? Account
|
||||||
|
}
|
||||||
|
if isWebFeed, let feedID = feedID {
|
||||||
|
return (AccountManager.shared.existingFeed(with: feedID) as? WebFeed)?.account
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
init(node: Node, unreadCount: Int) {
|
init(node: Node, unreadCount: Int) {
|
||||||
let feed = node.representedObject as! Feed
|
let feed = node.representedObject as! Feed
|
||||||
self.feedID = feed.feedID
|
self.feedID = feed.feedID
|
||||||
|
@ -34,6 +48,7 @@ final class MasterFeedTableViewIdentifier: NSObject, NSCopying {
|
||||||
|
|
||||||
self.isEditable = !(node.representedObject is PseudoFeed)
|
self.isEditable = !(node.representedObject is PseudoFeed)
|
||||||
self.isPsuedoFeed = node.representedObject is PseudoFeed
|
self.isPsuedoFeed = node.representedObject is PseudoFeed
|
||||||
|
self.isAccount = node.representedObject is Account
|
||||||
self.isFolder = node.representedObject is Folder
|
self.isFolder = node.representedObject is Folder
|
||||||
self.isWebFeed = node.representedObject is WebFeed
|
self.isWebFeed = node.representedObject is WebFeed
|
||||||
self.nameForDisplay = feed.nameForDisplay
|
self.nameForDisplay = feed.nameForDisplay
|
||||||
|
|
|
@ -22,10 +22,24 @@ extension MasterFeedViewController: UITableViewDropDelegate {
|
||||||
destIndexPath.section > 0,
|
destIndexPath.section > 0,
|
||||||
tableView.hasActiveDrag,
|
tableView.hasActiveDrag,
|
||||||
let destIdentifier = dataSource.itemIdentifier(for: destIndexPath),
|
let destIdentifier = dataSource.itemIdentifier(for: destIndexPath),
|
||||||
|
let destAccount = destIdentifier.account,
|
||||||
let destCell = tableView.cellForRow(at: destIndexPath) else {
|
let destCell = tableView.cellForRow(at: destIndexPath) else {
|
||||||
return UITableViewDropProposal(operation: .forbidden)
|
return UITableViewDropProposal(operation: .forbidden)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate account specific behaviors...
|
||||||
|
if destAccount.behaviors.contains(.disallowFeedInRootFolder) && destIdentifier.isAccount {
|
||||||
|
return UITableViewDropProposal(operation: .forbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
if destAccount.behaviors.contains(.disallowFeedInMultipleFolders),
|
||||||
|
let sourceFeedID = (session.localDragSession?.items.first?.localObject as? MasterFeedTableViewIdentifier)?.feedID,
|
||||||
|
let sourceWebFeed = AccountManager.shared.existingFeed(with: sourceFeedID) as? WebFeed,
|
||||||
|
sourceWebFeed.account?.accountID != destAccount.accountID && destAccount.hasWebFeed(withURL: sourceWebFeed.url) {
|
||||||
|
return UITableViewDropProposal(operation: .forbidden)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine the correct drop proposal
|
||||||
if destIdentifier.isFolder {
|
if destIdentifier.isFolder {
|
||||||
if session.location(in: destCell).y >= 0 {
|
if session.location(in: destCell).y >= 0 {
|
||||||
return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath)
|
return UITableViewDropProposal(operation: .move, intent: .insertIntoDestinationIndexPath)
|
||||||
|
|
Loading…
Reference in New Issue