mirror of
https://github.com/Ranchero-Software/NetNewsWire.git
synced 2025-02-09 08:39:00 +01:00
Merge branch 'master' into google_reader_compatible_syncing
This commit is contained in:
commit
98c32b9987
@ -62,6 +62,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
return defaultName
|
return defaultName
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
public var isDeleted = false
|
||||||
|
|
||||||
|
public var account: Account? {
|
||||||
|
return self
|
||||||
|
}
|
||||||
public let accountID: String
|
public let accountID: String
|
||||||
public let type: AccountType
|
public let type: AccountType
|
||||||
public var nameForDisplay: String {
|
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 {
|
var refreshInProgress = false {
|
||||||
didSet {
|
didSet {
|
||||||
if refreshInProgress != oldValue {
|
if refreshInProgress != oldValue {
|
||||||
@ -406,16 +415,12 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
return feed
|
return feed
|
||||||
}
|
}
|
||||||
|
|
||||||
func addFeed(container: Container, feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
public func addFeed(_ feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
delegate.addFeed(for: self, to: container, with: feed, completion: completion)
|
delegate.addFeed(for: self, with: feed, to: container, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFeed(_ feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
public func createFeed(url: String, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
delegate.removeFeed(for: self, from: container, with: feed, completion: completion)
|
delegate.createFeed(for: self, url: url, name: name, container: container, completion: completion)
|
||||||
}
|
|
||||||
|
|
||||||
public func createFeed(url: String, completion: @escaping (Result<Feed, Error>) -> Void) {
|
|
||||||
delegate.createFeed(for: self, url: url, completion: completion)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createFeed(with name: String?, url: String, feedID: String, homePageURL: String?) -> Feed {
|
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) {
|
public func removeFeed(_ feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
feedMetadata[feed.url] = nil
|
delegate.removeFeed(for: self, with: feed, from: container, completion: completion)
|
||||||
delegate.deleteFeed(for: self, with: feed, 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) {
|
public func renameFeed(_ feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
delegate.renameFeed(for: self, with: feed, to: name, completion: completion)
|
delegate.renameFeed(for: self, with: feed, to: name, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func restoreFeed(_ feed: Feed, folder: Folder?, completion: @escaping (Result<Void, Error>) -> Void) {
|
public func restoreFeed(_ feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
delegate.restoreFeed(for: self, feed: feed, folder: folder, completion: completion)
|
delegate.restoreFeed(for: self, feed: feed, container: container, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func deleteFolder(_ folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
public func addFolder(_ name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
||||||
delegate.deleteFolder(for: self, with: folder, completion: completion)
|
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) {
|
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)
|
delegate.restoreFolder(for: self, folder: folder, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func clearFeedMetadata(_ feed: Feed) {
|
||||||
|
feedMetadata[feed.url] = nil
|
||||||
|
}
|
||||||
|
|
||||||
func addFolder(_ folder: Folder) {
|
func addFolder(_ folder: Folder) {
|
||||||
folders!.insert(folder)
|
folders!.insert(folder)
|
||||||
postChildrenDidChangeNotification()
|
postChildrenDidChangeNotification()
|
||||||
@ -692,27 +708,25 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
return _flattenedFeeds
|
return _flattenedFeeds
|
||||||
}
|
}
|
||||||
|
|
||||||
public func removeFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
public func removeFeed(_ feed: Feed) {
|
||||||
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) {
|
|
||||||
topLevelFeeds.remove(feed)
|
topLevelFeeds.remove(feed)
|
||||||
structureDidChange()
|
structureDidChange()
|
||||||
postChildrenDidChangeNotification()
|
postChildrenDidChangeNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
func addFeed(_ feed: Feed) {
|
public func addFeed(_ feed: Feed) {
|
||||||
topLevelFeeds.insert(feed)
|
topLevelFeeds.insert(feed)
|
||||||
structureDidChange()
|
structureDidChange()
|
||||||
postChildrenDidChangeNotification()
|
postChildrenDidChangeNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteFolder(_ folder: Folder) {
|
func addFeedIfNotInAnyFolder(_ feed: Feed) {
|
||||||
|
if !flattenedFeeds().contains(feed) {
|
||||||
|
addFeed(feed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeFolder(_ folder: Folder) {
|
||||||
folders?.remove(folder)
|
folders?.remove(folder)
|
||||||
structureDidChange()
|
structureDidChange()
|
||||||
postChildrenDidChangeNotification()
|
postChildrenDidChangeNotification()
|
||||||
@ -785,19 +799,19 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||||||
|
|
||||||
@objc func saveToDiskIfNeeded() {
|
@objc func saveToDiskIfNeeded() {
|
||||||
|
|
||||||
if dirty {
|
if dirty && !isDeleted {
|
||||||
saveToDisk()
|
saveToDisk()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func saveFeedMetadataIfNeeded() {
|
@objc func saveFeedMetadataIfNeeded() {
|
||||||
if feedMetadataDirty {
|
if feedMetadataDirty && !isDeleted {
|
||||||
saveFeedMetadata()
|
saveFeedMetadata()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@objc func saveAccountMetadataIfNeeded() {
|
@objc func saveAccountMetadataIfNeeded() {
|
||||||
if metadataDirty {
|
if metadataDirty && !isDeleted {
|
||||||
saveAccountMetadata()
|
saveAccountMetadata()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ protocol AccountDelegate {
|
|||||||
|
|
||||||
// Local account does not; some synced accounts might.
|
// Local account does not; some synced accounts might.
|
||||||
var supportsSubFolders: Bool { get }
|
var supportsSubFolders: Bool { get }
|
||||||
|
var usesTags: Bool { get }
|
||||||
var opmlImportInProgress: Bool { get }
|
var opmlImportInProgress: Bool { get }
|
||||||
|
|
||||||
var server: String? { 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 importOPML(for account:Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
|
||||||
|
func addFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> Void)
|
||||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
|
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
func 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 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, 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 addFeed(for account: Account, to container: Container, with: Feed, completion: @escaping (Result<Void, Error>) -> Void)
|
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
func removeFeed(for account: Account, from container: Container, with: Feed, completion: @escaping (Result<Void, Error>) -> Void)
|
|
||||||
|
|
||||||
func restoreFeed(for account: Account, feed: Feed, folder: Folder?, completion: @escaping (Result<Void, Error>) -> Void)
|
|
||||||
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
|
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void)
|
||||||
|
|
||||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>?
|
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool) -> Set<Article>?
|
||||||
|
@ -127,6 +127,7 @@ public final class AccountManager: UnreadCountProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
accountsDictionary.removeValue(forKey: account.accountID)
|
accountsDictionary.removeValue(forKey: account.accountID)
|
||||||
|
account.isDeleted = true
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try FileManager.default.removeItem(atPath: account.dataFolder)
|
try FileManager.default.removeItem(atPath: account.dataFolder)
|
||||||
|
@ -18,6 +18,7 @@ extension Notification.Name {
|
|||||||
|
|
||||||
public protocol Container: class {
|
public protocol Container: class {
|
||||||
|
|
||||||
|
var account: Account? { get }
|
||||||
var topLevelFeeds: Set<Feed> { get set }
|
var topLevelFeeds: Set<Feed> { get set }
|
||||||
var folders: Set<Folder>? { get set }
|
var folders: Set<Folder>? { get set }
|
||||||
|
|
||||||
@ -27,8 +28,8 @@ public protocol Container: class {
|
|||||||
func hasChildFolder(with: String) -> Bool
|
func hasChildFolder(with: String) -> Bool
|
||||||
func childFolder(with: String) -> Folder?
|
func childFolder(with: String) -> Folder?
|
||||||
|
|
||||||
func removeFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void)
|
func removeFeed(_ feed: Feed)
|
||||||
func addFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void)
|
func addFeed(_ feed: Feed)
|
||||||
|
|
||||||
//Recursive — checks subfolders
|
//Recursive — checks subfolders
|
||||||
func flattenedFeeds() -> Set<Feed>
|
func flattenedFeeds() -> Set<Feed>
|
||||||
|
@ -11,38 +11,54 @@ import RSParser
|
|||||||
import RSWeb
|
import RSWeb
|
||||||
import RSCore
|
import RSCore
|
||||||
|
|
||||||
protocol FeedFinderDelegate: class {
|
|
||||||
|
|
||||||
func feedFinder(_: FeedFinder, didFindFeeds: Set<FeedSpecifier>)
|
|
||||||
}
|
|
||||||
|
|
||||||
class FeedFinder {
|
class FeedFinder {
|
||||||
|
|
||||||
private weak var delegate: FeedFinderDelegate?
|
static func find(url: URL, completion: @escaping (Result<Set<FeedSpecifier>, Error>) -> Void) {
|
||||||
private var feedSpecifiers = [String: FeedSpecifier]()
|
|
||||||
private var didNotifyDelegate = false
|
|
||||||
|
|
||||||
var initialDownloadError: Error?
|
downloadUsingCache(url) { (data, response, error) in
|
||||||
var initialDownloadStatusCode = -1
|
|
||||||
|
|
||||||
init(url: URL, delegate: FeedFinderDelegate) {
|
if response?.forcedStatusCode == 404 {
|
||||||
|
completion(.failure(AccountError.createErrorNotFound))
|
||||||
self.delegate = delegate
|
return
|
||||||
|
|
||||||
DispatchQueue.main.async() { () -> Void in
|
|
||||||
|
|
||||||
self.findFeeds(url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deinit {
|
if let error = error {
|
||||||
notifyDelegateIfNeeded()
|
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)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private extension FeedFinder {
|
private extension FeedFinder {
|
||||||
|
|
||||||
func addFeedSpecifier(_ feedSpecifier: FeedSpecifier) {
|
static func addFeedSpecifier(_ feedSpecifier: FeedSpecifier, feedSpecifiers: inout [String: FeedSpecifier]) {
|
||||||
|
|
||||||
// If there’s an existing feed specifier, merge the two so that we have the best data. If one has a title and one doesn’t, use that non-nil title. Use the better source.
|
// If there’s an existing feed specifier, merge the two so that we have the best data. If one has a title and one doesn’t, 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.
|
// Feeds in the <head> section we automatically assume are feeds.
|
||||||
// If there are none from the <head> section,
|
// If there are none from the <head> section,
|
||||||
@ -63,31 +79,35 @@ private extension FeedFinder {
|
|||||||
// and added once we determine they are feeds.
|
// and added once we determine they are feeds.
|
||||||
|
|
||||||
let possibleFeedSpecifiers = possibleFeedsInHTMLPage(htmlData: htmlData, urlString: urlString)
|
let possibleFeedSpecifiers = possibleFeedsInHTMLPage(htmlData: htmlData, urlString: urlString)
|
||||||
|
var feedSpecifiers = [String: FeedSpecifier]()
|
||||||
var feedSpecifiersToDownload = Set<FeedSpecifier>()
|
var feedSpecifiersToDownload = Set<FeedSpecifier>()
|
||||||
|
|
||||||
var didFindFeedInHTMLHead = false
|
var didFindFeedInHTMLHead = false
|
||||||
|
|
||||||
for oneFeedSpecifier in possibleFeedSpecifiers {
|
for oneFeedSpecifier in possibleFeedSpecifiers {
|
||||||
if oneFeedSpecifier.source == .HTMLHead {
|
if oneFeedSpecifier.source == .HTMLHead {
|
||||||
addFeedSpecifier(oneFeedSpecifier)
|
addFeedSpecifier(oneFeedSpecifier, feedSpecifiers: &feedSpecifiers)
|
||||||
didFindFeedInHTMLHead = true
|
didFindFeedInHTMLHead = true
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
if !feedSpecifiersContainsURLString(oneFeedSpecifier.urlString) {
|
if feedSpecifiers[oneFeedSpecifier.urlString] == nil {
|
||||||
feedSpecifiersToDownload.insert(oneFeedSpecifier)
|
feedSpecifiersToDownload.insert(oneFeedSpecifier)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if didFindFeedInHTMLHead || feedSpecifiersToDownload.isEmpty {
|
if didFindFeedInHTMLHead {
|
||||||
stopFinding()
|
completion(.success(Set(feedSpecifiers.values)))
|
||||||
}
|
return
|
||||||
else {
|
} else if feedSpecifiersToDownload.isEmpty {
|
||||||
downloadFeedSpecifiers(feedSpecifiersToDownload)
|
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)
|
let parserData = ParserData(url: urlString, data: htmlData)
|
||||||
var feedSpecifiers = HTMLFeedFinder(parserData: parserData).feedSpecifiers
|
var feedSpecifiers = HTMLFeedFinder(parserData: parserData).feedSpecifiers
|
||||||
@ -109,105 +129,42 @@ private extension FeedFinder {
|
|||||||
return feedSpecifiers
|
return feedSpecifiers
|
||||||
}
|
}
|
||||||
|
|
||||||
func feedSpecifiersContainsURLString(_ urlString: String) -> Bool {
|
static func isHTML(_ data: Data) -> Bool {
|
||||||
|
|
||||||
if let _ = feedSpecifiers[urlString] {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func isHTML(_ data: Data) -> Bool {
|
|
||||||
|
|
||||||
return (data as NSData).rs_dataIsProbablyHTML()
|
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()
|
||||||
|
|
||||||
func downloadInitialFeed(_ initialURL: URL) {
|
for downloadFeedSpecifier in downloadFeedSpecifiers {
|
||||||
|
|
||||||
downloadUsingCache(initialURL) { (data, response, error) in
|
guard let url = URL(string: downloadFeedSpecifier.urlString) else {
|
||||||
|
|
||||||
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)
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
group.enter()
|
||||||
downloadUsingCache(url) { (data, response, error) in
|
downloadUsingCache(url) { (data, response, error) in
|
||||||
|
|
||||||
pendingDownloads.remove(oneFeedSpecifier)
|
|
||||||
|
|
||||||
if let data = data, let response = response, response.statusIsOK, error == nil {
|
if let data = data, let response = response, response.statusIsOK, error == nil {
|
||||||
if self.isFeed(data, oneFeedSpecifier.urlString) {
|
if self.isFeed(data, downloadFeedSpecifier.urlString) {
|
||||||
self.addFeedSpecifier(oneFeedSpecifier)
|
addFeedSpecifier(downloadFeedSpecifier, feedSpecifiers: &resultFeedSpecifiers)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
group.leave()
|
||||||
|
}
|
||||||
|
|
||||||
if pendingDownloads.isEmpty {
|
|
||||||
self.stopFinding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func stopFinding() {
|
group.notify(queue: DispatchQueue.main) {
|
||||||
|
completion(.success(Set(resultFeedSpecifiers.values)))
|
||||||
notifyDelegateIfNeeded()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func notifyDelegateIfNeeded() {
|
|
||||||
|
|
||||||
if !didNotifyDelegate {
|
|
||||||
delegate?.feedFinder(self, didFindFeeds: Set(feedSpecifiers.values))
|
|
||||||
didNotifyDelegate = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func isFeed(_ data: Data, _ urlString: String) -> Bool {
|
static func isFeed(_ data: Data, _ urlString: String) -> Bool {
|
||||||
|
|
||||||
let parserData = ParserData(url: urlString, data: data)
|
let parserData = ParserData(url: urlString, data: data)
|
||||||
return FeedParser.canParse(parserData)
|
return FeedParser.canParse(parserData)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -143,25 +143,6 @@ final class FeedbinAPICaller: NSObject {
|
|||||||
transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion)
|
transport.send(request: request, method: HTTPMethod.post, payload: payload, completion: completion)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteTag(name: String, completion: @escaping (Result<[FeedbinTagging]?, Error>) -> Void) {
|
|
||||||
|
|
||||||
let callURL = feedbinBaseURL.appendingPathComponent("tags.json")
|
|
||||||
let request = URLRequest(url: callURL, credentials: credentials)
|
|
||||||
let payload = FeedbinDeleteTag(name: name)
|
|
||||||
|
|
||||||
transport.send(request: request, method: HTTPMethod.delete, payload: payload, resultType: [FeedbinTagging].self) { result in
|
|
||||||
|
|
||||||
switch result {
|
|
||||||
case .success(let (_, taggings)):
|
|
||||||
completion(.success(taggings))
|
|
||||||
case .failure(let error):
|
|
||||||
completion(.failure(error))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func retrieveSubscriptions(completion: @escaping (Result<[FeedbinSubscription]?, Error>) -> Void) {
|
func retrieveSubscriptions(completion: @escaping (Result<[FeedbinSubscription]?, Error>) -> Void) {
|
||||||
|
|
||||||
let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json")
|
let callURL = feedbinBaseURL.appendingPathComponent("subscriptions.json")
|
||||||
@ -438,13 +419,12 @@ final class FeedbinAPICaller: NSObject {
|
|||||||
|
|
||||||
func retrieveEntries(page: String, completion: @escaping (Result<([FeedbinEntry]?, String?), Error>) -> Void) {
|
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)))
|
completion(.success((nil, nil)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
callComponents.queryItems?.append(URLQueryItem(name: "mode", value: "extended"))
|
let request = URLRequest(url: url, credentials: credentials)
|
||||||
let request = URLRequest(url: callComponents.url!, credentials: credentials)
|
|
||||||
|
|
||||||
transport.send(request: request, resultType: [FeedbinEntry].self) { result in
|
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 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])
|
return Int(link[lowerBound..<upperBound])
|
||||||
}
|
}
|
||||||
if let upperBound = link.range(of: ">")?.lowerBound {
|
if let upperBound = partialLink.range(of: ">")?.lowerBound {
|
||||||
return Int(link[lowerBound..<upperBound])
|
return Int(link[lowerBound..<upperBound])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -31,6 +31,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||||||
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Feedbin")
|
private var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Feedbin")
|
||||||
|
|
||||||
let supportsSubFolders = false
|
let supportsSubFolders = false
|
||||||
|
let usesTags = true
|
||||||
let server: String? = "api.feedbin.com"
|
let server: String? = "api.feedbin.com"
|
||||||
var opmlImportInProgress = false
|
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) {
|
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
caller.renameTag(oldName: folder.name ?? "", newName: name) { result in
|
caller.renameTag(oldName: folder.name ?? "", newName: name) { result in
|
||||||
@ -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
|
// 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 {
|
guard folder.hasAtLeastOneFeed() else {
|
||||||
account.deleteFolder(folder)
|
account.removeFolder(folder)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// After we successfully delete at Feedbin, we add all the feeds to the account to save them. We then
|
let group = DispatchGroup()
|
||||||
// delete the folder. We then sync the taggings we received on the delete to remove any feeds from
|
|
||||||
// the account that might be in another folder.
|
|
||||||
caller.deleteTag(name: folder.name ?? "") { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let taggings):
|
|
||||||
DispatchQueue.main.sync {
|
|
||||||
BatchUpdate.shared.perform {
|
|
||||||
for feed in folder.topLevelFeeds {
|
for feed in folder.topLevelFeeds {
|
||||||
account.addFeed(feed)
|
group.enter()
|
||||||
self.clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
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)
|
||||||
}
|
}
|
||||||
account.deleteFolder(folder)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.notify(queue: DispatchQueue.main) {
|
||||||
|
account.removeFolder(folder)
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
self.syncTaggings(account, taggings)
|
|
||||||
case .failure(let error):
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
|
||||||
completion(.failure(wrappedError))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
caller.createSubscription(url: url) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let subResult):
|
case .success(let subResult):
|
||||||
switch subResult {
|
switch subResult {
|
||||||
case .created(let subscription):
|
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):
|
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:
|
case .alreadySubscribed:
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
completion(.failure(AccountError.createErrorAlreadySubscribed))
|
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) {
|
func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
if feed.folderRelationship?.count ?? 0 > 1 {
|
||||||
// This error should never happen
|
deleteTagging(for: account, with: feed, from: container, completion: completion)
|
||||||
guard let subscriptionID = feed.subscriptionID else {
|
} else {
|
||||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
account.clearFeedMetadata(feed)
|
||||||
return
|
deleteSubscription(for: account, with: feed, from: container, completion: completion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
caller.deleteSubscription(subscriptionID: subscriptionID) { result in
|
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 {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
DispatchQueue.main.async {
|
self.addFeed(for: account, with: feed, to: to, completion: completion)
|
||||||
account.removeFeed(feed)
|
|
||||||
if let folders = account.folders {
|
|
||||||
for folder in folders {
|
|
||||||
folder.removeFeed(feed)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completion(.success(()))
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
DispatchQueue.main.async {
|
completion(.failure(error))
|
||||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
}
|
||||||
completion(.failure(wrappedError))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
func addFeed(for account: Account, with feed: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
func addFeed(for account: Account, to container: Container, with feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
|
|
||||||
if let folder = container as? Folder, let feedID = Int(feed.feedID) {
|
if let folder = container as? Folder, let feedID = Int(feed.feedID) {
|
||||||
caller.createTagging(feedID: feedID, name: folder.name ?? "") { result in
|
caller.createTagging(feedID: feedID, name: folder.name ?? "") { result in
|
||||||
@ -377,6 +375,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||||||
case .success(let taggingID):
|
case .success(let taggingID):
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: String(taggingID))
|
self.saveFolderRelationship(for: feed, withFolderName: folder.name ?? "", id: String(taggingID))
|
||||||
|
account.removeFeed(feed)
|
||||||
folder.addFeed(feed)
|
folder.addFeed(feed)
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
@ -388,55 +387,24 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if let account = container as? Account {
|
|
||||||
account.addFeed(feed)
|
|
||||||
}
|
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
|
if let account = container as? Account {
|
||||||
|
account.addFeedIfNotInAnyFolder(feed)
|
||||||
|
}
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFeed(for account: Account, from container: Container, with feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
|
||||||
if let folder = container as? Folder, let feedTaggingID = feed.folderRelationship?[folder.name ?? ""] {
|
createFeed(for: account, url: feed.url, name: feed.editedName, container: container) { result in
|
||||||
caller.deleteTagging(taggingID: feedTaggingID) { result in
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
DispatchQueue.main.async {
|
|
||||||
folder.removeFeed(feed)
|
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
DispatchQueue.main.async {
|
completion(.failure(error))
|
||||||
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, folder: Folder?, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
||||||
|
|
||||||
let editedName = feed.editedName
|
|
||||||
|
|
||||||
createFeed(for: account, url: feed.url) { result in
|
|
||||||
switch result {
|
|
||||||
case .success(let feed):
|
|
||||||
self.processRestoredFeed(for: account, feed: feed, editedName: editedName, folder: folder, completion: completion)
|
|
||||||
case .failure(let error):
|
|
||||||
DispatchQueue.main.async {
|
|
||||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
|
||||||
completion(.failure(wrappedError))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -450,7 +418,7 @@ final class FeedbinAccountDelegate: AccountDelegate {
|
|||||||
for feed in folder.topLevelFeeds {
|
for feed in folder.topLevelFeeds {
|
||||||
|
|
||||||
group.enter()
|
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) {
|
if account.topLevelFeeds.contains(feed) {
|
||||||
account.removeFeed(feed)
|
account.removeFeed(feed)
|
||||||
}
|
}
|
||||||
@ -572,7 +540,7 @@ private extension FeedbinAccountDelegate {
|
|||||||
account.addFeed(feed)
|
account.addFeed(feed)
|
||||||
clearFolderRelationship(for: feed, withFolderName: folder.name ?? "")
|
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) {
|
func clearFolderRelationship(for feed: Feed, withFolderName folderName: String) {
|
||||||
if var folderRelationship = feed.folderRelationship {
|
if var folderRelationship = feed.folderRelationship {
|
||||||
folderRelationship[folderName] = nil
|
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 feedSpecifiers: [FeedSpecifier] = choices.map { choice in
|
||||||
let source = url == choice.url ? FeedSpecifier.Source.UserEntered : FeedSpecifier.Source.HTMLLink
|
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 bestSpecifier = FeedSpecifier.bestFeed(in: Set(feedSpecifiers)) {
|
||||||
if let bestSubscription = choices.filter({ bestSpecifier.urlString == $0.url }).first {
|
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 {
|
} else {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
completion(.failure(FeedbinAccountDelegateError.invalidParameter))
|
||||||
@ -932,13 +832,40 @@ 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 {
|
DispatchQueue.main.async {
|
||||||
|
|
||||||
let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL)
|
let feed = account.createFeed(with: sub.name, url: sub.url, feedID: String(sub.feedID), homePageURL: sub.homePageURL)
|
||||||
feed.subscriptionID = String(sub.subscriptionID)
|
feed.subscriptionID = String(sub.subscriptionID)
|
||||||
|
|
||||||
|
account.addFeed(feed, to: container) { result in
|
||||||
|
switch result {
|
||||||
|
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):
|
||||||
|
completion(.failure(error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func initialFeedDownload( account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||||
|
|
||||||
// Download the initial articles
|
// Download the initial articles
|
||||||
self.caller.retrieveEntries(feedID: feed.feedID) { result in
|
self.caller.retrieveEntries(feedID: feed.feedID) { result in
|
||||||
|
|
||||||
@ -958,12 +885,7 @@ private extension FeedbinAccountDelegate {
|
|||||||
}
|
}
|
||||||
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
os_log(.error, log: self.log, "Initial articles download failed: %@.", error.localizedDescription)
|
completion(.failure(error))
|
||||||
DispatchQueue.main.async {
|
|
||||||
completion(.success(feed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -1196,4 +1118,62 @@ 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -95,20 +95,12 @@ public final class Folder: DisplayNameProvider, Renamable, Container, UnreadCoun
|
|||||||
return topLevelFeeds.contains(feed)
|
return topLevelFeeds.contains(feed)
|
||||||
}
|
}
|
||||||
|
|
||||||
public func addFeed(_ feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
public func addFeed(_ feed: Feed) {
|
||||||
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) {
|
|
||||||
topLevelFeeds.insert(feed)
|
topLevelFeeds.insert(feed)
|
||||||
postChildrenDidChangeNotification()
|
postChildrenDidChangeNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFeed(_ feed: Feed) {
|
public func removeFeed(_ feed: Feed) {
|
||||||
topLevelFeeds.remove(feed)
|
topLevelFeeds.remove(feed)
|
||||||
postChildrenDidChangeNotification()
|
postChildrenDidChangeNotification()
|
||||||
}
|
}
|
||||||
|
@ -19,16 +19,13 @@ public enum LocalAccountDelegateError: String, Error {
|
|||||||
final class LocalAccountDelegate: AccountDelegate {
|
final class LocalAccountDelegate: AccountDelegate {
|
||||||
|
|
||||||
let supportsSubFolders = false
|
let supportsSubFolders = false
|
||||||
|
let usesTags = false
|
||||||
let opmlImportInProgress = false
|
let opmlImportInProgress = false
|
||||||
|
|
||||||
let server: String? = nil
|
let server: String? = nil
|
||||||
var credentials: Credentials?
|
var credentials: Credentials?
|
||||||
var accountMetadata: AccountMetadata?
|
var accountMetadata: AccountMetadata?
|
||||||
|
|
||||||
private weak var account: Account?
|
|
||||||
private var feedFinder: FeedFinder?
|
|
||||||
private var createFeedCompletion: ((Result<Feed, Error>) -> Void)?
|
|
||||||
|
|
||||||
private let refresher = LocalAccountRefresher()
|
private let refresher = LocalAccountRefresher()
|
||||||
|
|
||||||
var refreshProgress: DownloadProgress {
|
var refreshProgress: DownloadProgress {
|
||||||
@ -89,27 +86,49 @@ final class LocalAccountDelegate: AccountDelegate {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
func createFeed(for account: Account, url urlString: String, name: String?, container: Container, completion: @escaping (Result<Feed, 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) {
|
|
||||||
|
|
||||||
guard let url = URL(string: urlString) else {
|
guard let url = URL(string: urlString) else {
|
||||||
completion(.failure(LocalAccountDelegateError.invalidParameter))
|
completion(.failure(LocalAccountDelegateError.invalidParameter))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
self.account = account
|
FeedFinder.find(url: url) { result in
|
||||||
createFeedCompletion = completion
|
|
||||||
|
|
||||||
feedFinder = FeedFinder(url: url, delegate: self)
|
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(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteFeed(for account: Account, from container: Container, feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
func removeFeed(for account: Account, with feed: Feed, from container: Container?, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
|
container?.removeFeed(feed)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addFeed(for account: Account, to container: Container, with feed: Feed, completion: @escaping (Result<Void, Error>) -> Void) {
|
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||||
if let folder = container as? Folder {
|
from.removeFeed(feed)
|
||||||
folder.addFeed(feed)
|
to.addFeed(feed)
|
||||||
feed.account = folder.account
|
|
||||||
} else if let account = container as? Account {
|
|
||||||
account.addFeed(feed)
|
|
||||||
feed.account = account
|
|
||||||
}
|
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
func removeFeed(for account: Account, from 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 account = container as? Account {
|
container.addFeed(feed)
|
||||||
account.removeFeed(feed)
|
|
||||||
}
|
|
||||||
if let folder = container as? Folder {
|
|
||||||
folder.removeFeed(feed)
|
|
||||||
}
|
|
||||||
completion(.success(()))
|
completion(.success(()))
|
||||||
}
|
}
|
||||||
|
|
||||||
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) {
|
||||||
if let folder = folder {
|
container.addFeed(feed)
|
||||||
folder.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 {
|
} 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(()))
|
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))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
@ -52,7 +52,6 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
let account = accountAndFolderSpecifier.account
|
let account = accountAndFolderSpecifier.account
|
||||||
let folder = accountAndFolderSpecifier.folder
|
|
||||||
|
|
||||||
if account.hasFeed(withURL: url.absoluteString) {
|
if account.hasFeed(withURL: url.absoluteString) {
|
||||||
showAlreadySubscribedError(url.absoluteString)
|
showAlreadySubscribedError(url.absoluteString)
|
||||||
@ -61,20 +60,20 @@ class AddFeedController: AddFeedWindowControllerDelegate {
|
|||||||
|
|
||||||
BatchUpdate.shared.start()
|
BatchUpdate.shared.start()
|
||||||
|
|
||||||
account.createFeed(url: url.absoluteString) { [weak self] result in
|
account.createFeed(url: url.absoluteString, name: title, container: container) { result in
|
||||||
|
|
||||||
self?.endShowingProgress()
|
self.endShowingProgress()
|
||||||
|
BatchUpdate.shared.end()
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let feed):
|
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):
|
case .failure(let error):
|
||||||
BatchUpdate.shared.end()
|
|
||||||
switch error {
|
switch error {
|
||||||
case AccountError.createErrorAlreadySubscribed:
|
case AccountError.createErrorAlreadySubscribed:
|
||||||
self?.showAlreadySubscribedError(url.absoluteString)
|
self.showAlreadySubscribedError(url.absoluteString)
|
||||||
case AccountError.createErrorNotFound:
|
case AccountError.createErrorNotFound:
|
||||||
self?.showNoFeedsErrorMessage()
|
self.showNoFeedsErrorMessage()
|
||||||
default:
|
default:
|
||||||
NSApplication.shared.presentError(error)
|
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
|
// MARK: Errors
|
||||||
|
|
||||||
func showAlreadySubscribedError(_ urlString: String) {
|
func showAlreadySubscribedError(_ urlString: String) {
|
||||||
|
@ -29,7 +29,7 @@ class AddFeedWindowController : NSWindowController {
|
|||||||
|
|
||||||
private var urlString: String?
|
private var urlString: String?
|
||||||
private var initialName: String?
|
private var initialName: String?
|
||||||
private var initialAccount: Account?
|
private weak var initialAccount: Account?
|
||||||
private var initialFolder: Folder?
|
private var initialFolder: Folder?
|
||||||
private weak var delegate: AddFeedWindowControllerDelegate?
|
private weak var delegate: AddFeedWindowControllerDelegate?
|
||||||
private var folderTreeController: TreeController!
|
private var folderTreeController: TreeController!
|
||||||
|
@ -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
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
137
Mac/MainWindow/Sidebar/PasteboardFolder.swift
Normal file
137
Mac/MainWindow/Sidebar/PasteboardFolder.swift
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
@ -54,11 +54,22 @@ import Account
|
|||||||
}
|
}
|
||||||
|
|
||||||
func outlineView(_ outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: Any?, proposedChildIndex index: Int) -> NSDragOperation {
|
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
|
return SidebarOutlineDataSource.dragOperationNone
|
||||||
}
|
}
|
||||||
|
|
||||||
let parentNode = nodeForItem(item)
|
let parentNode = nodeForItem(item)
|
||||||
|
|
||||||
|
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)
|
let contentsType = draggedFeedContentsType(draggedFeeds)
|
||||||
|
|
||||||
switch contentsType {
|
switch contentsType {
|
||||||
@ -75,12 +86,22 @@ import Account
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
|
return SidebarOutlineDataSource.dragOperationNone
|
||||||
guard let draggedFeeds = PasteboardFeed.pasteboardFeeds(with: info.draggingPasteboard), !draggedFeeds.isEmpty else {
|
|
||||||
return false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func outlineView(_ outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: Any?, childIndex index: Int) -> Bool {
|
||||||
|
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 parentNode = nodeForItem(item)
|
||||||
|
|
||||||
|
if let draggedFolders = draggedFolders {
|
||||||
|
return acceptLocalFoldersDrop(outlineView, draggedFolders, parentNode, index)
|
||||||
|
}
|
||||||
|
|
||||||
|
if let draggedFeeds = draggedFeeds {
|
||||||
let contentsType = draggedFeedContentsType(draggedFeeds)
|
let contentsType = draggedFeedContentsType(draggedFeeds)
|
||||||
|
|
||||||
switch contentsType {
|
switch contentsType {
|
||||||
@ -95,6 +116,9 @@ import Account
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
@ -109,11 +133,10 @@ private extension SidebarOutlineDataSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func nodeRepresentsDraggableItem(_ node: Node) -> Bool {
|
func nodeRepresentsDraggableItem(_ node: Node) -> Bool {
|
||||||
// Don’t allow PseudoFeed or Folder to be dragged.
|
// Don’t allow PseudoFeed to be dragged.
|
||||||
// This will have to be revisited later. For instance,
|
// This will have to be revisited later. For instance,
|
||||||
// user-created smart feeds should be draggable, maybe.
|
// user-created smart feeds should be draggable, maybe.
|
||||||
// And we might allow dragging folders between accounts.
|
return node.representedObject is Folder || node.representedObject is Feed
|
||||||
return node.representedObject is Feed
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Drag and Drop
|
// MARK: - Drag and Drop
|
||||||
@ -173,21 +196,20 @@ private extension SidebarOutlineDataSource {
|
|||||||
guard let dropTargetNode = ancestorThatCanAcceptLocalFeed(parentNode) else {
|
guard let dropTargetNode = ancestorThatCanAcceptLocalFeed(parentNode) else {
|
||||||
return SidebarOutlineDataSource.dragOperationNone
|
return SidebarOutlineDataSource.dragOperationNone
|
||||||
}
|
}
|
||||||
if !allParticipantsAreLocalAccounts(dropTargetNode, Set([draggedFeed])) {
|
|
||||||
return SidebarOutlineDataSource.dragOperationNone
|
|
||||||
}
|
|
||||||
if nodeHasChildRepresentingDraggedFeed(dropTargetNode, draggedFeed) {
|
if nodeHasChildRepresentingDraggedFeed(dropTargetNode, draggedFeed) {
|
||||||
return SidebarOutlineDataSource.dragOperationNone
|
return SidebarOutlineDataSource.dragOperationNone
|
||||||
}
|
}
|
||||||
let dragOperation: NSDragOperation = localFeedsDropOperation(dropTargetNode, Set([draggedFeed]))
|
if violatesTagSpecificBehavior(dropTargetNode, draggedFeed) {
|
||||||
|
return SidebarOutlineDataSource.dragOperationNone
|
||||||
|
}
|
||||||
if parentNode == dropTargetNode && index == NSOutlineViewDropOnItemIndex {
|
if parentNode == dropTargetNode && index == NSOutlineViewDropOnItemIndex {
|
||||||
return dragOperation
|
return localDragOperation()
|
||||||
}
|
}
|
||||||
let updatedIndex = indexWhereDraggedFeedWouldAppear(dropTargetNode, draggedFeed)
|
let updatedIndex = indexWhereDraggedFeedWouldAppear(dropTargetNode, draggedFeed)
|
||||||
if parentNode !== dropTargetNode || index != updatedIndex {
|
if parentNode !== dropTargetNode || index != updatedIndex {
|
||||||
outlineView.setDropItem(dropTargetNode, dropChildIndex: updatedIndex)
|
outlineView.setDropItem(dropTargetNode, dropChildIndex: updatedIndex)
|
||||||
}
|
}
|
||||||
return dragOperation
|
return localDragOperation()
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateLocalFeedsDrop(_ outlineView: NSOutlineView, _ draggedFeeds: Set<PasteboardFeed>, _ parentNode: Node, _ index: Int) -> NSDragOperation {
|
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 {
|
guard let dropTargetNode = ancestorThatCanAcceptLocalFeed(parentNode) else {
|
||||||
return SidebarOutlineDataSource.dragOperationNone
|
return SidebarOutlineDataSource.dragOperationNone
|
||||||
}
|
}
|
||||||
if !allParticipantsAreLocalAccounts(dropTargetNode, draggedFeeds) {
|
if nodeHasChildRepresentingAnyDraggedFeed(dropTargetNode, draggedFeeds) {
|
||||||
return SidebarOutlineDataSource.dragOperationNone
|
return SidebarOutlineDataSource.dragOperationNone
|
||||||
}
|
}
|
||||||
if nodeHasChildRepresentingAnyDraggedFeed(dropTargetNode, draggedFeeds) {
|
if violatesTagSpecificBehavior(dropTargetNode, draggedFeeds) {
|
||||||
return SidebarOutlineDataSource.dragOperationNone
|
return SidebarOutlineDataSource.dragOperationNone
|
||||||
}
|
}
|
||||||
if parentNode !== dropTargetNode || index != NSOutlineViewDropOnItemIndex {
|
if parentNode !== dropTargetNode || index != NSOutlineViewDropOnItemIndex {
|
||||||
outlineView.setDropItem(dropTargetNode, dropChildIndex: NSOutlineViewDropOnItemIndex)
|
outlineView.setDropItem(dropTargetNode, dropChildIndex: NSOutlineViewDropOnItemIndex)
|
||||||
}
|
}
|
||||||
return localFeedsDropOperation(dropTargetNode, draggedFeeds)
|
return localDragOperation()
|
||||||
}
|
}
|
||||||
|
|
||||||
func localFeedsDropOperation(_ dropTargetNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> NSDragOperation {
|
func localDragOperation() -> NSDragOperation {
|
||||||
if allParticipantsAreSameAccount(dropTargetNode, draggedFeeds) {
|
|
||||||
return .move
|
|
||||||
}
|
|
||||||
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||||
return .copy
|
return .copy
|
||||||
} else {
|
} 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 {
|
if let account = node.representedObject as? Account {
|
||||||
return account
|
return account
|
||||||
}
|
}
|
||||||
@ -231,7 +250,7 @@ private extension SidebarOutlineDataSource {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
private func commonAccountsFor(_ nodes: Set<Node>) -> Set<Account> {
|
func commonAccountsFor(_ nodes: Set<Node>) -> Set<Account> {
|
||||||
|
|
||||||
var accounts = Set<Account>()
|
var accounts = Set<Account>()
|
||||||
for node in nodes {
|
for node in nodes {
|
||||||
@ -243,53 +262,156 @@ private extension SidebarOutlineDataSource {
|
|||||||
return accounts
|
return accounts
|
||||||
}
|
}
|
||||||
|
|
||||||
private func copy(node: Node, to parentNode: Node) {
|
func accountHasFolderRepresentingAnyDraggedFolders(_ account: Account, _ draggedFolders: Set<PasteboardFolder>) -> Bool {
|
||||||
guard let feed = node.representedObject as? Feed else {
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let destination = parentNode.representedObject as? Container
|
destination.account?.addFeed(feed, to: destination) { result in
|
||||||
|
|
||||||
BatchUpdate.shared.start()
|
|
||||||
destination?.addFeed(feed) { result in
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
BatchUpdate.shared.end()
|
|
||||||
break
|
break
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
BatchUpdate.shared.end()
|
|
||||||
NSApplication.shared.presentError(error)
|
NSApplication.shared.presentError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func move(node: Node, to parentNode: Node) {
|
func moveFeedInAccount(node: Node, to parentNode: Node) {
|
||||||
guard let feed = node.representedObject as? Feed else {
|
guard let feed = node.representedObject as? Feed,
|
||||||
|
let source = node.parent?.representedObject as? Container,
|
||||||
|
let destination = parentNode.representedObject as? Container else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let source = node.parent?.representedObject as? Container
|
|
||||||
let destination = parentNode.representedObject as? Container
|
|
||||||
|
|
||||||
BatchUpdate.shared.start()
|
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 {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
BatchUpdate.shared.end()
|
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
|
break
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
// If the second part of the move failed, try to put the feed back
|
NSApplication.shared.presentError(error)
|
||||||
source?.addFeed(feed) { result in}
|
}
|
||||||
|
}
|
||||||
|
} 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()
|
BatchUpdate.shared.end()
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
break
|
||||||
|
case .failure(let error):
|
||||||
NSApplication.shared.presentError(error)
|
NSApplication.shared.presentError(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
NSApplication.shared.presentError(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
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
BatchUpdate.shared.perform {
|
|
||||||
|
|
||||||
draggedNodes.forEach { node in
|
draggedNodes.forEach { node in
|
||||||
if sameAccount(node, parentNode) {
|
if sameAccount(node, parentNode) {
|
||||||
move(node: node, to: parentNode)
|
if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
||||||
} else if NSApplication.shared.currentEvent?.modifierFlags.contains(.option) ?? false {
|
copyFeedInAccount(node: node, to: parentNode)
|
||||||
copy(node: node, to: parentNode)
|
|
||||||
} else {
|
} 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
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -352,6 +472,94 @@ private extension SidebarOutlineDataSource {
|
|||||||
return ancestorThatCanAcceptNonLocalFeed(parentNode)
|
return ancestorThatCanAcceptNonLocalFeed(parentNode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func copyFolderBetweenAccounts(node: Node, to parentNode: Node) {
|
||||||
|
guard let sourceFolder = node.representedObject as? Folder,
|
||||||
|
let destinationAccount = nodeAccount(parentNode) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
replicateFolder(sourceFolder, destinationAccount: destinationAccount, completion: {})
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveFolderBetweenAccounts(node: Node, to parentNode: Node) {
|
||||||
|
guard let sourceFolder = node.representedObject as? Folder,
|
||||||
|
let sourceAccount = nodeAccount(node),
|
||||||
|
let destinationAccount = nodeAccount(parentNode) else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
BatchUpdate.shared.start()
|
||||||
|
replicateFolder(sourceFolder, destinationAccount: destinationAccount) {
|
||||||
|
sourceAccount.removeFolder(sourceFolder) { result in
|
||||||
|
BatchUpdate.shared.end()
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
break
|
||||||
|
case .failure(let error):
|
||||||
|
NSApplication.shared.presentError(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func replicateFolder(_ folder: Folder, destinationAccount: Account, completion: @escaping () -> Void) {
|
||||||
|
destinationAccount.addFolder(folder.name ?? "") { result in
|
||||||
|
switch result {
|
||||||
|
case .success(let destinationFolder):
|
||||||
|
let group = DispatchGroup()
|
||||||
|
for feed in folder.topLevelFeeds {
|
||||||
|
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 {
|
func acceptSingleNonLocalFeedDrop(_ outlineView: NSOutlineView, _ draggedFeed: PasteboardFeed, _ parentNode: Node, _ index: Int) -> Bool {
|
||||||
guard nodeIsDropTarget(parentNode), index == NSOutlineViewDropOnItemIndex else {
|
guard nodeIsDropTarget(parentNode), index == NSOutlineViewDropOnItemIndex else {
|
||||||
return false
|
return false
|
||||||
@ -385,44 +593,6 @@ private extension SidebarOutlineDataSource {
|
|||||||
return false
|
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 {
|
func sameAccount(_ node: Node, _ parentNode: Node) -> Bool {
|
||||||
if let accountID = nodeAccountID(node), let parentAccountID = nodeAccountID(parentNode) {
|
if let accountID = nodeAccountID(node), let parentAccountID = nodeAccountID(parentNode) {
|
||||||
if accountID == parentAccountID {
|
if accountID == parentAccountID {
|
||||||
@ -432,16 +602,21 @@ private extension SidebarOutlineDataSource {
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func nodeAccountID(_ node: Node) -> String? {
|
func nodeAccount(_ node: Node) -> Account? {
|
||||||
if let account = node.representedObject as? Account {
|
if let account = node.representedObject as? Account {
|
||||||
return account.accountID
|
return account
|
||||||
} else if let folder = node.representedObject as? Folder {
|
} else if let folder = node.representedObject as? Folder {
|
||||||
return folder.account?.accountID
|
return folder.account
|
||||||
} else if let feed = node.representedObject as? Feed {
|
} else if let feed = node.representedObject as? Feed {
|
||||||
return feed.account?.accountID
|
return feed.account
|
||||||
} else {
|
} else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func nodeAccountID(_ node: Node) -> String? {
|
||||||
|
return nodeAccount(node)?.accountID
|
||||||
}
|
}
|
||||||
|
|
||||||
func nodeHasChildRepresentingAnyDraggedFeed(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
func nodeHasChildRepresentingAnyDraggedFeed(_ parentNode: Node, _ draggedFeeds: Set<PasteboardFeed>) -> Bool {
|
||||||
@ -453,6 +628,29 @@ private extension SidebarOutlineDataSource {
|
|||||||
return false
|
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 {
|
func indexWhereDraggedFeedWouldAppear(_ parentNode: Node, _ draggedFeed: PasteboardFeed) -> 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)
|
||||||
@ -463,6 +661,18 @@ private extension SidebarOutlineDataSource {
|
|||||||
let index = sortedNodes.firstIndex(of: draggedFeedNode)!
|
let index = sortedNodes.firstIndex(of: draggedFeedNode)!
|
||||||
return index
|
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 {
|
final class PasteboardFeedObjectWrapper: DisplayNameProvider {
|
||||||
@ -476,3 +686,15 @@ final class PasteboardFeedObjectWrapper: DisplayNameProvider {
|
|||||||
self.pasteboardFeed = pasteboardFeed
|
self.pasteboardFeed = pasteboardFeed
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final class PasteboardFolderObjectWrapper: DisplayNameProvider {
|
||||||
|
|
||||||
|
var nameForDisplay: String {
|
||||||
|
return pasteboardFolder.name
|
||||||
|
}
|
||||||
|
let pasteboardFolder: PasteboardFolder
|
||||||
|
|
||||||
|
init(pasteboardFolder: PasteboardFolder) {
|
||||||
|
self.pasteboardFolder = pasteboardFolder
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -60,6 +60,7 @@ protocol SidebarDelegate: class {
|
|||||||
NotificationCenter.default.addObserver(self, selector: #selector(feedSettingDidChange(_:)), name: .FeedSettingDidChange, object: nil)
|
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(displayNameDidChange(_:)), name: .DisplayNameDidChange, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(userDidRequestSidebarSelection(_:)), name: .UserDidRequestSidebarSelection, 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()
|
outlineView.reloadData()
|
||||||
|
|
||||||
@ -165,6 +166,12 @@ protocol SidebarDelegate: class {
|
|||||||
revealAndSelectRepresentedObject(feed as AnyObject)
|
revealAndSelectRepresentedObject(feed as AnyObject)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func calendarDayChanged(_ note: Notification) {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
SmartFeedsController.shared.todayFeed.fetchUnreadCounts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Actions
|
// MARK: - Actions
|
||||||
|
|
||||||
@IBAction func delete(_ sender: AnyObject?) {
|
@IBAction func delete(_ sender: AnyObject?) {
|
||||||
|
@ -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(accountStateDidChange(_:)), name: .AccountStateDidChange, object: nil)
|
||||||
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange(_:)), name: .AccountsDidChange, 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(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil)
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(calendarDayChanged(_:)), name: .NSCalendarDayChanged, object: nil)
|
||||||
|
|
||||||
didRegisterForNotifications = true
|
didRegisterForNotifications = true
|
||||||
}
|
}
|
||||||
@ -512,6 +513,14 @@ final class TimelineViewController: NSViewController, UndoableCommandRunner {
|
|||||||
self.sortDirection = AppDefaults.timelineSortDirection
|
self.sortDirection = AppDefaults.timelineSortDirection
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc func calendarDayChanged(_ note: Notification) {
|
||||||
|
if representedObjectsContainsTodayFeed() {
|
||||||
|
DispatchQueue.main.async { [weak self] in
|
||||||
|
self?.fetchArticles()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Reloading Data
|
// MARK: - Reloading Data
|
||||||
|
|
||||||
private func cellForRowView(_ rowView: NSView) -> NSView? {
|
private func cellForRowView(_ rowView: NSView) -> NSView? {
|
||||||
@ -966,6 +975,10 @@ private extension TimelineViewController {
|
|||||||
return representedObjects?.contains(where: { $0 is PseudoFeed}) ?? false
|
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 {
|
func representedObjectsContainsAnyFeed(_ feeds: Set<Feed>) -> Bool {
|
||||||
|
|
||||||
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
||||||
|
@ -50,12 +50,16 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
|||||||
func deleteElement(_ element:ScriptingObject) {
|
func deleteElement(_ element:ScriptingObject) {
|
||||||
if let scriptableFolder = element as? ScriptableFolder {
|
if let scriptableFolder = element as? ScriptableFolder {
|
||||||
BatchUpdate.shared.perform {
|
BatchUpdate.shared.perform {
|
||||||
account.deleteFolder(scriptableFolder.folder) { result in
|
account.removeFolder(scriptableFolder.folder) { result in
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if let scriptableFeed = element as? ScriptableFeed {
|
} else if let scriptableFeed = element as? ScriptableFeed {
|
||||||
BatchUpdate.shared.perform {
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -92,6 +92,8 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
|
|||||||
return self.scriptableFeed(existingFeed, account:account, folder:folder)
|
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.
|
// 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?)
|
// 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
|
// because we can't wait here (on the main thread, maybe) for the callback, we have to return from this function
|
||||||
@ -100,30 +102,15 @@ class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContaine
|
|||||||
// suspendExecution(). When we get the callback, we can supply the event result and call resumeExecution()
|
// suspendExecution(). When we get the callback, we can supply the event result and call resumeExecution()
|
||||||
command.suspendExecution()
|
command.suspendExecution()
|
||||||
|
|
||||||
account.createFeed(url: url) { result in
|
account.createFeed(url: url, name: titleFromArgs, container: container) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let feed):
|
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])
|
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
|
||||||
let scriptableFeed = self.scriptableFeed(feed, account:account, folder:folder)
|
let scriptableFeed = self.scriptableFeed(feed, account:account, folder:folder)
|
||||||
command.resumeExecution(withResult:scriptableFeed.objectSpecifier)
|
command.resumeExecution(withResult:scriptableFeed.objectSpecifier)
|
||||||
case .failure:
|
case .failure:
|
||||||
command.resumeExecution(withResult:nil)
|
command.resumeExecution(withResult:nil)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
case .failure:
|
|
||||||
command.resumeExecution(withResult:nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -53,7 +53,7 @@ class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContai
|
|||||||
func deleteElement(_ element:ScriptingObject) {
|
func deleteElement(_ element:ScriptingObject) {
|
||||||
if let scriptableFeed = element as? ScriptableFeed {
|
if let scriptableFeed = element as? ScriptableFeed {
|
||||||
BatchUpdate.shared.perform {
|
BatchUpdate.shared.perform {
|
||||||
folder.account?.deleteFeed(scriptableFeed.feed) { result in }
|
folder.account?.removeFeed(scriptableFeed.feed, from: folder) { result in }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -235,7 +235,7 @@
|
|||||||
84A37CB5201ECD610087C5AF /* RenameWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A37CB4201ECD610087C5AF /* RenameWindowController.swift */; };
|
84A37CB5201ECD610087C5AF /* RenameWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84A37CB4201ECD610087C5AF /* RenameWindowController.swift */; };
|
||||||
84A3EE5F223B667F00557320 /* DefaultFeeds.opml in Resources */ = {isa = PBXBuildFile; fileRef = 84A3EE52223B667F00557320 /* DefaultFeeds.opml */; };
|
84A3EE5F223B667F00557320 /* DefaultFeeds.opml in Resources */ = {isa = PBXBuildFile; fileRef = 84A3EE52223B667F00557320 /* DefaultFeeds.opml */; };
|
||||||
84A3EE61223B667F00557320 /* 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 */; };
|
84AD1EBA2031649C00BC20B7 /* SmartFeedPasteboardWriter.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EB92031649C00BC20B7 /* SmartFeedPasteboardWriter.swift */; };
|
||||||
84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84AD1EBB2032AF5C00BC20B7 /* SidebarOutlineDataSource.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 */; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
84B7178B201E66580091657D /* SidebarViewController+ContextualMenus.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "SidebarViewController+ContextualMenus.swift"; sourceTree = "<group>"; };
|
||||||
@ -1373,7 +1373,7 @@
|
|||||||
849A97601ED9EB96007D329B /* SidebarOutlineView.swift */,
|
849A97601ED9EB96007D329B /* SidebarOutlineView.swift */,
|
||||||
849A97631ED9EB96007D329B /* UnreadCountView.swift */,
|
849A97631ED9EB96007D329B /* UnreadCountView.swift */,
|
||||||
848D578D21543519005FFAD5 /* PasteboardFeed.swift */,
|
848D578D21543519005FFAD5 /* PasteboardFeed.swift */,
|
||||||
84AD1EA92031617300BC20B7 /* FolderPasteboardWriter.swift */,
|
84AD1EA92031617300BC20B7 /* PasteboardFolder.swift */,
|
||||||
849A97821ED9EC63007D329B /* SidebarStatusBarView.swift */,
|
849A97821ED9EC63007D329B /* SidebarStatusBarView.swift */,
|
||||||
844B5B6A1FEA224000C7C76A /* Keyboard */,
|
844B5B6A1FEA224000C7C76A /* Keyboard */,
|
||||||
845A29251FC928C7007B49E3 /* Cell */,
|
845A29251FC928C7007B49E3 /* Cell */,
|
||||||
@ -2476,7 +2476,7 @@
|
|||||||
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
|
8472058120142E8900AD578B /* FeedInspectorViewController.swift in Sources */,
|
||||||
55E15BCC229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift in Sources */,
|
55E15BCC229D65A900D6602A /* AccountsGoogleReaderCompatibleWindowController.swift in Sources */,
|
||||||
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */,
|
5144EA382279FC6200D19003 /* AccountsAddLocalWindowController.swift in Sources */,
|
||||||
84AD1EAA2031617300BC20B7 /* FolderPasteboardWriter.swift in Sources */,
|
84AD1EAA2031617300BC20B7 /* PasteboardFolder.swift in Sources */,
|
||||||
5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */,
|
5144EA51227B8E4500D19003 /* AccountsFeedbinWindowController.swift in Sources */,
|
||||||
84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */,
|
84AD1EBC2032AF5C00BC20B7 /* SidebarOutlineDataSource.swift in Sources */,
|
||||||
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */,
|
845A29241FC9255E007B49E3 /* SidebarCellAppearance.swift in Sources */,
|
||||||
|
@ -136,13 +136,13 @@ private struct SidebarItemSpecifier {
|
|||||||
|
|
||||||
if let feed = feed {
|
if let feed = feed {
|
||||||
BatchUpdate.shared.start()
|
BatchUpdate.shared.start()
|
||||||
account?.deleteFeed(feed) { result in
|
account?.removeFeed(feed, from: path.resolveContainer()) { result in
|
||||||
BatchUpdate.shared.end()
|
BatchUpdate.shared.end()
|
||||||
self.checkResult(result)
|
self.checkResult(result)
|
||||||
}
|
}
|
||||||
} else if let folder = folder {
|
} else if let folder = folder {
|
||||||
BatchUpdate.shared.start()
|
BatchUpdate.shared.start()
|
||||||
account?.deleteFolder(folder) { result in
|
account?.removeFolder(folder) { result in
|
||||||
BatchUpdate.shared.end()
|
BatchUpdate.shared.end()
|
||||||
self.checkResult(result)
|
self.checkResult(result)
|
||||||
}
|
}
|
||||||
@ -161,12 +161,12 @@ private struct SidebarItemSpecifier {
|
|||||||
|
|
||||||
private func restoreFeed() {
|
private func restoreFeed() {
|
||||||
|
|
||||||
guard let account = account, let feed = feed else {
|
guard let account = account, let feed = feed, let container = path.resolveContainer() else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
BatchUpdate.shared.start()
|
BatchUpdate.shared.start()
|
||||||
account.restoreFeed(feed, folder: resolvedFolder()) { result in
|
account.restoreFeed(feed, container: container) { result in
|
||||||
BatchUpdate.shared.end()
|
BatchUpdate.shared.end()
|
||||||
self.checkResult(result)
|
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>) {
|
private func checkResult(_ result: Result<Void, Error>) {
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
|
@ -80,13 +80,10 @@ class AddFeedViewController: UITableViewController, AddContainerViewControllerCh
|
|||||||
let container = pickerData.containers[folderPickerView.selectedRow(inComponent: 0)]
|
let container = pickerData.containers[folderPickerView.selectedRow(inComponent: 0)]
|
||||||
|
|
||||||
var account: Account?
|
var account: Account?
|
||||||
var folder: Folder?
|
|
||||||
if let containerAccount = container as? Account {
|
if let containerAccount = container as? Account {
|
||||||
account = containerAccount
|
account = containerAccount
|
||||||
}
|
} else if let containerFolder = container as? Folder, let containerAccount = containerFolder.account {
|
||||||
if let containerFolder = container as? Folder, let containerAccount = containerFolder.account {
|
|
||||||
account = containerAccount
|
account = containerAccount
|
||||||
folder = containerFolder
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if account!.hasFeed(withURL: url.absoluteString) {
|
if account!.hasFeed(withURL: url.absoluteString) {
|
||||||
@ -94,26 +91,28 @@ class AddFeedViewController: UITableViewController, AddContainerViewControllerCh
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let title = nameTextField.text
|
|
||||||
|
|
||||||
delegate?.processingDidBegin()
|
delegate?.processingDidBegin()
|
||||||
|
BatchUpdate.shared.start()
|
||||||
|
|
||||||
account!.createFeed(url: url.absoluteString) { [weak self] result in
|
account!.createFeed(url: url.absoluteString, name: nameTextField.text, container: container) { result in
|
||||||
|
|
||||||
|
BatchUpdate.shared.end()
|
||||||
|
|
||||||
switch result {
|
switch result {
|
||||||
case .success(let feed):
|
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):
|
case .failure(let error):
|
||||||
switch error {
|
switch error {
|
||||||
case AccountError.createErrorAlreadySubscribed:
|
case AccountError.createErrorAlreadySubscribed:
|
||||||
self?.showAlreadySubscribedError()
|
self.showAlreadySubscribedError()
|
||||||
self?.delegate?.processingDidCancel()
|
self.delegate?.processingDidCancel()
|
||||||
case AccountError.createErrorNotFound:
|
case AccountError.createErrorNotFound:
|
||||||
self?.showNoFeedsErrorMessage()
|
self.showNoFeedsErrorMessage()
|
||||||
self?.delegate?.processingDidCancel()
|
self.delegate?.processingDidCancel()
|
||||||
default:
|
default:
|
||||||
self?.presentError(error)
|
self.presentError(error)
|
||||||
self?.delegate?.processingDidCancel()
|
self.delegate?.processingDidCancel()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -178,45 +177,6 @@ private extension AddFeedViewController {
|
|||||||
presentError(title: title, message: message as String)
|
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 {
|
extension AddFeedViewController: UITextFieldDelegate {
|
||||||
|
@ -373,22 +373,17 @@ class MasterFeedViewController: ProgressTableViewController, UndoableCommandRunn
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
// Move the Feed
|
// Move the Feed
|
||||||
let source = sourceNode.parent?.representedObject as? Container
|
guard let source = sourceNode.parent?.representedObject as? Container, let destination = destParentNode?.representedObject as? Container else {
|
||||||
let destination = destParentNode?.representedObject as? Container
|
return
|
||||||
source?.removeFeed(feed) { [weak self] result in
|
}
|
||||||
|
|
||||||
|
BatchUpdate.shared.start()
|
||||||
|
source.account?.moveFeed(feed, from: source, to: destination) { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .success:
|
case .success:
|
||||||
destination?.addFeed(feed) { result in
|
BatchUpdate.shared.end()
|
||||||
switch result {
|
|
||||||
case .success:
|
|
||||||
break
|
|
||||||
case .failure(let error):
|
case .failure(let error):
|
||||||
source?.addFeed(feed) { result in }
|
self.presentError(error)
|
||||||
self?.presentError(error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case .failure(let error):
|
|
||||||
self?.presentError(error)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1 +1 @@
|
|||||||
Subproject commit 718f27db5016298a9cc650764d5d92ce54ce1e1a
|
Subproject commit 93b481897d84849345daa965bd8e11860c9422e7
|
Loading…
x
Reference in New Issue
Block a user