Merge branch 'master' into google_reader_compatible_syncing

This commit is contained in:
Jeremy Beker 2019-05-31 09:54:25 -04:00
commit 98c32b9987
No known key found for this signature in database
GPG Key ID: CD5EE767A4A34FD0
24 changed files with 917 additions and 824 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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 theres an existing feed specifier, merge the two so that we have the best data. If one has a title and one doesnt, 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)
}
}

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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()
}
}

View File

@ -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 {
// Dont allow PseudoFeed or Folder to be dragged.
// Dont 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
}
}

View File

@ -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?) {

View File

@ -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 theres a match or if a folder contains (recursively) one of feeds

View File

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

View File

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

View File

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

View File

@ -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 */,

View File

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

View File

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

View File

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