Remove FeedWrangler support.
@ -39,7 +39,6 @@ public enum AccountType: Int, Codable {
|
||||
case cloudKit = 2
|
||||
case feedly = 16
|
||||
case feedbin = 17
|
||||
case feedWrangler = 18
|
||||
case newsBlur = 19
|
||||
case freshRSS = 20
|
||||
case inoreader = 21
|
||||
@ -47,7 +46,7 @@ public enum AccountType: Int, Codable {
|
||||
case theOldReader = 23
|
||||
|
||||
public var isDeveloperRestricted: Bool {
|
||||
return self == .cloudKit || self == .feedbin || self == .feedly || self == .feedWrangler || self == .inoreader
|
||||
return self == .cloudKit || self == .feedbin || self == .feedly || self == .inoreader
|
||||
}
|
||||
|
||||
}
|
||||
@ -269,8 +268,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
self.delegate = FeedbinAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||
case .feedly:
|
||||
self.delegate = FeedlyAccountDelegate(dataFolder: dataFolder, transport: transport, api: FeedlyAccountDelegate.environment)
|
||||
case .feedWrangler:
|
||||
self.delegate = FeedWranglerAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||
case .newsBlur:
|
||||
self.delegate = NewsBlurAccountDelegate(dataFolder: dataFolder, transport: transport)
|
||||
case .freshRSS:
|
||||
@ -302,8 +299,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
defaultName = NSLocalizedString("Feedly", comment: "Feedly")
|
||||
case .feedbin:
|
||||
defaultName = NSLocalizedString("Feedbin", comment: "Feedbin")
|
||||
case .feedWrangler:
|
||||
defaultName = NSLocalizedString("FeedWrangler", comment: "FeedWrangler")
|
||||
case .newsBlur:
|
||||
defaultName = NSLocalizedString("NewsBlur", comment: "NewsBlur")
|
||||
case .freshRSS:
|
||||
@ -364,8 +359,6 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
||||
switch type {
|
||||
case .feedbin:
|
||||
FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
|
||||
case .feedWrangler:
|
||||
FeedWranglerAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
|
||||
case .newsBlur:
|
||||
NewsBlurAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
|
||||
case .freshRSS, .inoreader, .bazQux, .theOldReader:
|
||||
|
@ -1,280 +0,0 @@
|
||||
//
|
||||
// FeedWranglerAPICaller.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-08-29.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
import Foundation
|
||||
import SyncDatabase
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
enum FeedWranglerError : Error {
|
||||
case general(message: String)
|
||||
}
|
||||
|
||||
final class FeedWranglerAPICaller: NSObject {
|
||||
|
||||
private var transport: Transport!
|
||||
|
||||
var credentials: Credentials?
|
||||
weak var accountMetadata: AccountMetadata?
|
||||
|
||||
init(transport: Transport) {
|
||||
super.init()
|
||||
self.transport = transport
|
||||
}
|
||||
|
||||
func cancelAll() {
|
||||
transport.cancelAll()
|
||||
}
|
||||
|
||||
func logout(completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let url = FeedWranglerConfig.clientURL.appendingPathComponent("users/logout")
|
||||
let request = URLRequest(url: url, credentials: credentials)
|
||||
|
||||
transport.send(request: request) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
|
||||
let url = FeedWranglerConfig.clientURL.appendingPathComponent("users/authorize")
|
||||
let username = self.credentials?.username ?? ""
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerAuthorizationResult.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
if let accessToken = results?.accessToken {
|
||||
let authCredentials = Credentials(type: .feedWranglerToken, username: username, secret: accessToken)
|
||||
completion(.success(authCredentials))
|
||||
} else {
|
||||
completion(.success(nil))
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveSubscriptions(completion: @escaping (Result<[FeedWranglerSubscription], Error>) -> Void) {
|
||||
let url = FeedWranglerConfig.clientURL.appendingPathComponent("subscriptions/list")
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerSubscriptionsRequest.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
completion(.success(results?.feeds ?? []))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addSubscription(url: String, completion: @escaping (Result<FeedWranglerSubscription, Error>) -> Void) {
|
||||
let url = FeedWranglerConfig
|
||||
.clientURL
|
||||
.appendingPathComponent("subscriptions/add_feed_and_wait")
|
||||
.appendingQueryItems([
|
||||
URLQueryItem(name: "feed_url", value: url),
|
||||
URLQueryItem(name: "choose_first", value: "true")
|
||||
])
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerSubscriptionResult.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
if let results = results {
|
||||
if let error = results.error {
|
||||
completion(.failure(FeedWranglerError.general(message: error)))
|
||||
} else {
|
||||
completion(.success(results.feed))
|
||||
}
|
||||
} else {
|
||||
completion(.failure(FeedWranglerError.general(message: "No feed found")))
|
||||
}
|
||||
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renameSubscription(feedID: String, newName: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
var postData = URLComponents(url: FeedWranglerConfig.clientURL, resolvingAgainstBaseURL: false)
|
||||
postData?.path += "subscriptions/rename_feed"
|
||||
postData?.queryItems = [
|
||||
URLQueryItem(name: "feed_id", value: feedID),
|
||||
URLQueryItem(name: "feed_name", value: newName),
|
||||
]
|
||||
|
||||
guard let url = postData?.urlWithEnhancedPercentEncodedQuery else {
|
||||
completion(.failure(FeedWranglerError.general(message: "Could not encode name")))
|
||||
return
|
||||
}
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerSubscriptionsRequest.self) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func removeSubscription(feedID: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
let url = FeedWranglerConfig.clientURL
|
||||
.appendingPathComponent("subscriptions/remove_feed")
|
||||
.appendingQueryItem(URLQueryItem(name: "feed_id", value: feedID))
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerGenericResult.self) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: FeedItems
|
||||
func retrieveEntries(articleIDs: [String], completion: @escaping (Result<[FeedWranglerFeedItem], Error>) -> Void) {
|
||||
let IDs = articleIDs.joined(separator: ",")
|
||||
let url = FeedWranglerConfig.clientURL
|
||||
.appendingPathComponent("feed_items/get")
|
||||
.appendingQueryItem(URLQueryItem(name: "feed_item_ids", value: IDs))
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerFeedItemsRequest.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
completion(.success(results?.feedItems ?? []))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func retrieveFeedItems(page: Int = 0, feed: Feed? = nil, completion: @escaping (Result<[FeedWranglerFeedItem], Error>) -> Void) {
|
||||
let queryItems = [
|
||||
URLQueryItem(name: "read", value: "false"),
|
||||
URLQueryItem(name: "offset", value: String(page * FeedWranglerConfig.pageSize)),
|
||||
feed.map { URLQueryItem(name: "feed_id", value: $0.feedID) }
|
||||
].compactMap { $0 }
|
||||
let url = FeedWranglerConfig.clientURL
|
||||
.appendingPathComponent("feed_items/list")
|
||||
.appendingQueryItems(queryItems)
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerFeedItemsRequest.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
completion(.success(results?.feedItems ?? []))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func retrieveUnreadFeedItemIds(completion: @escaping (Result<[FeedWranglerFeedItemId], Error>) -> Void) {
|
||||
retrieveAllFeedItemIds(filters: [URLQueryItem(name: "read", value: "false")], completion: completion)
|
||||
}
|
||||
|
||||
func retrieveStarredFeedItemIds(completion: @escaping (Result<[FeedWranglerFeedItemId], Error>) -> Void) {
|
||||
retrieveAllFeedItemIds(filters: [URLQueryItem(name: "starred", value: "true")], completion: completion)
|
||||
}
|
||||
|
||||
private func retrieveAllFeedItemIds(filters: [URLQueryItem] = [], foundItems: [FeedWranglerFeedItemId] = [], page: Int = 0, completion: @escaping (Result<[FeedWranglerFeedItemId], Error>) -> Void) {
|
||||
retrieveFeedItemIds(filters: filters, page: page) { result in
|
||||
switch result {
|
||||
case .success(let newItems):
|
||||
if newItems.count > 0 {
|
||||
self.retrieveAllFeedItemIds(filters: filters, foundItems: foundItems + newItems, page: (page + 1), completion: completion)
|
||||
} else {
|
||||
completion(.success(foundItems + newItems))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func retrieveFeedItemIds(filters: [URLQueryItem] = [], page: Int = 0, completion: @escaping (Result<[FeedWranglerFeedItemId], Error>) -> Void) {
|
||||
let url = FeedWranglerConfig.clientURL
|
||||
.appendingPathComponent("feed_items/list_ids")
|
||||
.appendingQueryItems(filters + [URLQueryItem(name: "offset", value: String(page * FeedWranglerConfig.idsPageSize))])
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerFeedItemIdsRequest.self) { result in
|
||||
switch result {
|
||||
case .success(let (_, results)):
|
||||
completion(.success(results?.feedItems ?? []))
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateArticleStatus(_ articleID: String, _ statuses: [SyncStatus], completion: @escaping () -> Void) {
|
||||
|
||||
var queryItems = statuses.compactMap { status -> URLQueryItem? in
|
||||
switch status.key {
|
||||
case .read:
|
||||
return URLQueryItem(name: "read", value: status.flag.description)
|
||||
case .starred:
|
||||
return URLQueryItem(name: "starred", value: status.flag.description)
|
||||
case .deleted:
|
||||
return nil
|
||||
case .new:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
queryItems.append(URLQueryItem(name: "feed_item_id", value: articleID))
|
||||
let url = FeedWranglerConfig.clientURL
|
||||
.appendingPathComponent("feed_items/update")
|
||||
.appendingQueryItems(queryItems)
|
||||
|
||||
standardSend(url: url, resultType: FeedWranglerGenericResult.self) { result in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
private func standardSend<R: Decodable>(url: URL?, resultType: R.Type, completion: @escaping (Result<(HTTPURLResponse, R?), Error>) -> Void) {
|
||||
guard let callURL = url else {
|
||||
completion(.failure(TransportError.noURL))
|
||||
return
|
||||
}
|
||||
let request = URLRequest(url: callURL, credentials: credentials)
|
||||
|
||||
transport.send(request: request, resultType: resultType, completion: completion)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private extension URLComponents {
|
||||
|
||||
var urlWithEnhancedPercentEncodedQuery: URL? {
|
||||
guard let tempQueryItems = self.queryItems, !tempQueryItems.isEmpty else {
|
||||
return self.url
|
||||
}
|
||||
|
||||
var tempComponents = self
|
||||
tempComponents.percentEncodedQuery = self.enhancedPercentEncodedQuery
|
||||
return tempComponents.url
|
||||
}
|
||||
}
|
@ -1,608 +0,0 @@
|
||||
//
|
||||
// FeedWranglerAccountDelegate.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-08-29.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Articles
|
||||
import RSCore
|
||||
import RSParser
|
||||
import RSWeb
|
||||
import SyncDatabase
|
||||
import os.log
|
||||
import Secrets
|
||||
|
||||
final class FeedWranglerAccountDelegate: AccountDelegate {
|
||||
|
||||
var behaviors: AccountBehaviors = [.disallowFolderManagement]
|
||||
|
||||
var isOPMLImportInProgress = false
|
||||
var server: String? = FeedWranglerConfig.clientPath
|
||||
var credentials: Credentials? {
|
||||
didSet {
|
||||
caller.credentials = credentials
|
||||
}
|
||||
}
|
||||
|
||||
var accountMetadata: AccountMetadata?
|
||||
var refreshProgress = DownloadProgress(numberOfTasks: 0)
|
||||
|
||||
private let caller: FeedWranglerAPICaller
|
||||
private let log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Feed Wrangler")
|
||||
private let database: SyncDatabase
|
||||
|
||||
init(dataFolder: String, transport: Transport?) {
|
||||
if let transport = transport {
|
||||
caller = FeedWranglerAPICaller(transport: transport)
|
||||
} else {
|
||||
let sessionConfiguration = URLSessionConfiguration.default
|
||||
sessionConfiguration.requestCachePolicy = .reloadIgnoringLocalCacheData
|
||||
sessionConfiguration.timeoutIntervalForRequest = 60.0
|
||||
sessionConfiguration.httpShouldSetCookies = false
|
||||
sessionConfiguration.httpCookieAcceptPolicy = .never
|
||||
sessionConfiguration.httpMaximumConnectionsPerHost = 1
|
||||
sessionConfiguration.httpCookieStorage = nil
|
||||
sessionConfiguration.urlCache = nil
|
||||
|
||||
if let userAgentHeaders = UserAgent.headers() {
|
||||
sessionConfiguration.httpAdditionalHeaders = userAgentHeaders
|
||||
}
|
||||
|
||||
let session = URLSession(configuration: sessionConfiguration)
|
||||
caller = FeedWranglerAPICaller(transport: session)
|
||||
}
|
||||
|
||||
database = SyncDatabase(databaseFilePath: dataFolder.appending("/DB.sqlite3"))
|
||||
}
|
||||
|
||||
func accountWillBeDeleted(_ account: Account) {
|
||||
caller.logout() { _ in }
|
||||
}
|
||||
|
||||
func receiveRemoteNotification(for account: Account, userInfo: [AnyHashable : Any], completion: @escaping () -> Void) {
|
||||
completion()
|
||||
}
|
||||
|
||||
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(6)
|
||||
|
||||
self.refreshCredentials(for: account) {
|
||||
self.refreshProgress.completeTask()
|
||||
self.refreshSubscriptions(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.sendArticleStatus(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticles(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshMissingArticles(for: account) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
account.metadata.lastArticleFetchEndTime = Date()
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshCredentials(for account: Account, completion: @escaping (() -> Void)) {
|
||||
os_log(.debug, log: log, "Refreshing credentials...")
|
||||
// MARK: TODO
|
||||
credentials = try? account.retrieveCredentials(type: .feedWranglerToken)
|
||||
completion()
|
||||
}
|
||||
|
||||
func refreshSubscriptions(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
os_log(.debug, log: log, "Refreshing subscriptions...")
|
||||
caller.retrieveSubscriptions { result in
|
||||
switch result {
|
||||
case .success(let subscriptions):
|
||||
self.syncFeeds(account, subscriptions)
|
||||
completion(.success(()))
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.debug, log: self.log, "Failed to refresh subscriptions: %@", error.localizedDescription)
|
||||
completion(.failure(error))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func syncArticleStatus(for account: Account, completion: ((Result<Void, Error>) -> Void)? = nil) {
|
||||
sendArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
self.refreshArticleStatus(for: account) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion?(.success(()))
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
completion?(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshArticles(for account: Account, page: Int = 0, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
os_log(.debug, log: log, "Refreshing articles, page: %d...", page)
|
||||
|
||||
caller.retrieveFeedItems(page: page) { result in
|
||||
switch result {
|
||||
case .success(let items):
|
||||
self.syncFeedItems(account, items) {
|
||||
if items.count == 0 {
|
||||
completion(.success(()))
|
||||
} else {
|
||||
self.refreshArticles(for: account, page: (page + 1), completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshMissingArticles(for account: Account, completion: @escaping ((Result<Void, Error>)-> Void)) {
|
||||
account.fetchArticleIDsForStatusesWithoutArticlesNewerThanCutoffDate { articleIDsResult in
|
||||
|
||||
func process(_ fetchedArticleIDs: Set<String>) {
|
||||
os_log(.debug, log: self.log, "Refreshing missing articles...")
|
||||
let group = DispatchGroup()
|
||||
|
||||
let articleIDs = Array(fetchedArticleIDs)
|
||||
let chunkedArticleIDs = articleIDs.chunked(into: 100)
|
||||
|
||||
for chunk in chunkedArticleIDs {
|
||||
group.enter()
|
||||
self.caller.retrieveEntries(articleIDs: chunk) { result in
|
||||
switch result {
|
||||
case .success(let entries):
|
||||
self.syncFeedItems(account, entries) {
|
||||
group.leave()
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.error, log: self.log, "Refresh missing articles failed: %@", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
self.refreshProgress.completeTask()
|
||||
os_log(.debug, log: self.log, "Done refreshing missing articles.")
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
switch articleIDsResult {
|
||||
case .success(let articleIDs):
|
||||
process(articleIDs)
|
||||
case .failure(let databaseError):
|
||||
self.refreshProgress.completeTask()
|
||||
completion(.failure(databaseError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func sendArticleStatus(for account: Account, completion: @escaping VoidResultCompletionBlock) {
|
||||
os_log(.debug, log: log, "Sending article status...")
|
||||
|
||||
database.selectForProcessing { result in
|
||||
|
||||
func processStatuses(_ syncStatuses: [SyncStatus]) {
|
||||
let articleStatuses = Dictionary(grouping: syncStatuses, by: { $0.articleID })
|
||||
let group = DispatchGroup()
|
||||
|
||||
articleStatuses.forEach { articleID, statuses in
|
||||
group.enter()
|
||||
self.caller.updateArticleStatus(articleID, statuses) {
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done sending article statuses.")
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
switch result {
|
||||
case .success(let syncStatuses):
|
||||
processStatuses(syncStatuses)
|
||||
case .failure(let databaseError):
|
||||
completion(.failure(databaseError))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func refreshArticleStatus(for account: Account, completion: @escaping ((Result<Void, Error>) -> Void)) {
|
||||
os_log(.debug, log: log, "Refreshing article status...")
|
||||
let group = DispatchGroup()
|
||||
|
||||
group.enter()
|
||||
caller.retrieveUnreadFeedItemIds { result in
|
||||
switch result {
|
||||
case .success(let items):
|
||||
self.syncArticleReadState(account, items)
|
||||
group.leave()
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.info, log: self.log, "Retrieving unread entries failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
// starred
|
||||
group.enter()
|
||||
caller.retrieveStarredFeedItemIds { result in
|
||||
switch result {
|
||||
case .success(let items):
|
||||
self.syncArticleStarredState(account, items)
|
||||
group.leave()
|
||||
|
||||
case .failure(let error):
|
||||
os_log(.info, log: self.log, "Retrieving starred entries failed: %@.", error.localizedDescription)
|
||||
group.leave()
|
||||
}
|
||||
}
|
||||
|
||||
group.notify(queue: DispatchQueue.main) {
|
||||
os_log(.debug, log: self.log, "Done refreshing article statuses.")
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func createFolder(for account: Account, name: String, completion: @escaping (Result<Folder, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func renameFolder(for account: Account, with folder: Folder, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func removeFolder(for account: Account, with folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func createFeed(for account: Account, url: String, name: String?, container: Container, validateFeed: Bool, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
|
||||
self.refreshCredentials(for: account) {
|
||||
self.refreshProgress.completeTask()
|
||||
self.caller.addSubscription(url: url) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success(let subscription):
|
||||
self.addFeedWranglerSubscription(account: account, subscription: subscription, name: name, container: container, completion: completion)
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func addFeedWranglerSubscription(account: Account, subscription sub: FeedWranglerSubscription, name: String?, container: Container, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
DispatchQueue.main.async {
|
||||
let feed = account.createFeed(with: sub.title, url: sub.feedURL, feedID: String(sub.feedID), homePageURL: sub.siteURL)
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func initialFeedDownload(account: Account, feed: Feed, completion: @escaping (Result<Feed, Error>) -> Void) {
|
||||
|
||||
self.caller.retrieveFeedItems(page: 0, feed: feed) { results in
|
||||
switch results {
|
||||
case .success(let entries):
|
||||
self.syncFeedItems(account, entries) {
|
||||
DispatchQueue.main.async {
|
||||
completion(.success(feed))
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
|
||||
self.refreshCredentials(for: account) {
|
||||
self.refreshProgress.completeTask()
|
||||
self.caller.renameSubscription(feedID: feed.feedID, newName: name) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
feed.editedName = name
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
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) {
|
||||
// just add to account, folders are not supported
|
||||
DispatchQueue.main.async {
|
||||
account.addFeedIfNotInAnyFolder(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
|
||||
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
refreshProgress.addToNumberOfTasksAndRemaining(2)
|
||||
|
||||
self.refreshCredentials(for: account) {
|
||||
self.refreshProgress.completeTask()
|
||||
self.caller.removeSubscription(feedID: feed.feedID) { result in
|
||||
self.refreshProgress.completeTask()
|
||||
|
||||
switch result {
|
||||
case .success:
|
||||
DispatchQueue.main.async {
|
||||
account.clearFeedMetadata(feed)
|
||||
account.removeFeed(feed)
|
||||
completion(.success(()))
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
DispatchQueue.main.async {
|
||||
let wrappedError = AccountError.wrappedError(error: error, account: account)
|
||||
completion(.failure(wrappedError))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func moveFeed(for account: Account, with feed: Feed, from: Container, to: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func restoreFeed(for account: Account, feed: Feed, container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
if let existingFeed = account.existingFeed(withURL: feed.url) {
|
||||
account.addFeed(existingFeed, to: container) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
createFeed(for: account, url: feed.url, name: feed.editedName, container: container, validateFeed: true) { result in
|
||||
switch result {
|
||||
case .success:
|
||||
completion(.success(()))
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restoreFolder(for account: Account, folder: Folder, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
fatalError()
|
||||
}
|
||||
|
||||
func markArticles(for account: Account, articles: Set<Article>, statusKey: ArticleStatus.Key, flag: Bool, completion: @escaping (Result<Void, Error>) -> Void) {
|
||||
account.update(articles, statusKey: statusKey, flag: flag) { result in
|
||||
switch result {
|
||||
case .success(let articles):
|
||||
let syncStatuses = articles.map { article in
|
||||
return SyncStatus(articleID: article.articleID, key: SyncStatus.Key(statusKey), flag: flag)
|
||||
}
|
||||
|
||||
self.database.insertStatuses(syncStatuses) { _ in
|
||||
self.database.selectPendingCount { result in
|
||||
if let count = try? result.get(), count > 100 {
|
||||
self.sendArticleStatus(for: account) { _ in }
|
||||
}
|
||||
completion(.success(()))
|
||||
}
|
||||
}
|
||||
case .failure(let error):
|
||||
completion(.failure(error))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func accountDidInitialize(_ account: Account) {
|
||||
credentials = try? account.retrieveCredentials(type: .feedWranglerToken)
|
||||
}
|
||||
|
||||
static func validateCredentials(transport: Transport, credentials: Credentials, endpoint: URL? = nil, completion: @escaping (Result<Credentials?, Error>) -> Void) {
|
||||
let caller = FeedWranglerAPICaller(transport: transport)
|
||||
caller.credentials = credentials
|
||||
caller.validateCredentials() { result in
|
||||
DispatchQueue.main.async {
|
||||
completion(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Suspend and Resume (for iOS)
|
||||
|
||||
/// Suspend all network activity
|
||||
func suspendNetwork() {
|
||||
caller.cancelAll()
|
||||
}
|
||||
|
||||
/// Suspend the SQLLite databases
|
||||
func suspendDatabase() {
|
||||
database.suspend()
|
||||
}
|
||||
|
||||
/// Make sure no SQLite databases are open and we are ready to issue network requests.
|
||||
func resume() {
|
||||
database.resume()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
private extension FeedWranglerAccountDelegate {
|
||||
|
||||
func syncFeeds(_ account: Account, _ subscriptions: [FeedWranglerSubscription]) {
|
||||
assert(Thread.isMainThread)
|
||||
let feedIds = subscriptions.map { String($0.feedID) }
|
||||
|
||||
let feedsToRemove = account.topLevelFeeds.filter { !feedIds.contains($0.feedID) }
|
||||
account.removeFeeds(feedsToRemove)
|
||||
|
||||
var subscriptionsToAdd = Set<FeedWranglerSubscription>()
|
||||
subscriptions.forEach { subscription in
|
||||
let subscriptionId = String(subscription.feedID)
|
||||
|
||||
if let feed = account.existingFeed(withFeedID: subscriptionId) {
|
||||
feed.name = subscription.title
|
||||
feed.editedName = nil
|
||||
feed.homePageURL = subscription.siteURL
|
||||
feed.externalID = nil // MARK: TODO What should this be?
|
||||
} else {
|
||||
subscriptionsToAdd.insert(subscription)
|
||||
}
|
||||
}
|
||||
|
||||
subscriptionsToAdd.forEach { subscription in
|
||||
let feedId = String(subscription.feedID)
|
||||
let feed = account.createFeed(with: subscription.title, url: subscription.feedURL, feedID: feedId, homePageURL: subscription.siteURL)
|
||||
feed.externalID = nil
|
||||
account.addFeed(feed)
|
||||
}
|
||||
}
|
||||
|
||||
func syncFeedItems(_ account: Account, _ feedItems: [FeedWranglerFeedItem], completion: @escaping VoidCompletionBlock) {
|
||||
let parsedItems = feedItems.map { (item: FeedWranglerFeedItem) -> ParsedItem in
|
||||
let itemID = String(item.feedItemID)
|
||||
// let authors = ...
|
||||
let parsedItem = ParsedItem(syncServiceID: itemID, uniqueID: itemID, feedURL: String(item.feedID), url: nil, externalURL: item.url, title: item.title, language: nil, contentHTML: item.body, contentText: nil, summary: nil, imageURL: nil, bannerImageURL: nil, datePublished: item.publishedDate, dateModified: item.updatedDate, authors: nil, tags: nil, attachments: nil)
|
||||
|
||||
return parsedItem
|
||||
}
|
||||
|
||||
let feedIDsAndItems = Dictionary(grouping: parsedItems, by: { $0.feedURL }).mapValues { Set($0) }
|
||||
account.update(feedIDsAndItems: feedIDsAndItems, defaultRead: true) { _ in
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
func syncArticleReadState(_ account: Account, _ unreadFeedItems: [FeedWranglerFeedItemId]) {
|
||||
let unreadServerItemIDs = Set(unreadFeedItems.map { String($0.feedItemID) })
|
||||
account.fetchUnreadArticleIDs { articleIDsResult in
|
||||
guard let unreadLocalItemIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
account.markAsUnread(unreadServerItemIDs)
|
||||
|
||||
let readItemIDs = unreadLocalItemIDs.subtracting(unreadServerItemIDs)
|
||||
account.markAsRead(readItemIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func syncArticleStarredState(_ account: Account, _ starredFeedItems: [FeedWranglerFeedItemId]) {
|
||||
let starredServerItemIDs = Set(starredFeedItems.map { String($0.feedItemID) })
|
||||
account.fetchStarredArticleIDs { articleIDsResult in
|
||||
guard let starredLocalItemIDs = try? articleIDsResult.get() else {
|
||||
return
|
||||
}
|
||||
|
||||
account.markAsStarred(starredServerItemIDs)
|
||||
|
||||
let unstarredItemIDs = starredLocalItemIDs.subtracting(starredServerItemIDs)
|
||||
account.markAsUnstarred(unstarredItemIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func syncArticleState(_ account: Account, key: ArticleStatus.Key, flag: Bool, serverFeedItems: [FeedWranglerFeedItem]) {
|
||||
let _ /*serverFeedItemIDs*/ = serverFeedItems.map { String($0.feedID) }
|
||||
|
||||
// todo generalize this logic
|
||||
}
|
||||
}
|
@ -1,23 +0,0 @@
|
||||
//
|
||||
// FeedWranglerAuthorizationResult.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-11-20.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerAuthorizationResult: Hashable, Codable {
|
||||
|
||||
let accessToken: String?
|
||||
let error: String?
|
||||
let result: String
|
||||
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case accessToken = "access_token"
|
||||
case error = "error"
|
||||
case result = "result"
|
||||
}
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
//
|
||||
// FeedWranglerConfig.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Jonathan Bennett on 9/27/19.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Secrets
|
||||
|
||||
enum FeedWranglerConfig {
|
||||
static let pageSize = 100
|
||||
static let idsPageSize = 1000
|
||||
static let clientPath = "https://feedwrangler.net/api/v2/"
|
||||
static let clientURL = {
|
||||
URL(string: FeedWranglerConfig.clientPath)!
|
||||
}()
|
||||
}
|
@ -1,62 +0,0 @@
|
||||
//
|
||||
// FeedWranglerFeedItem.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-10-16.4// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerFeedItem: Hashable, Codable {
|
||||
|
||||
let feedItemID: Int
|
||||
let publishedAt: Int
|
||||
let createdAt: Int
|
||||
let versionKey: Int
|
||||
let updatedAt: Int
|
||||
let url: String
|
||||
let title: String
|
||||
let starred: Bool
|
||||
let read: Bool
|
||||
let readLater: Bool
|
||||
let body: String
|
||||
let author: String?
|
||||
let feedID: Int
|
||||
let feedName: String
|
||||
|
||||
var publishedDate: Date {
|
||||
get {
|
||||
Date(timeIntervalSince1970: Double(publishedAt))
|
||||
}
|
||||
}
|
||||
|
||||
var createdDate: Date {
|
||||
get {
|
||||
Date(timeIntervalSince1970: Double(createdAt))
|
||||
}
|
||||
}
|
||||
|
||||
var updatedDate: Date {
|
||||
get {
|
||||
Date(timeIntervalSince1970: Double(updatedAt))
|
||||
}
|
||||
}
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case feedItemID = "feed_item_id"
|
||||
case publishedAt = "published_at"
|
||||
case createdAt = "created_at"
|
||||
case versionKey = "version_key"
|
||||
case updatedAt = "updated_at"
|
||||
case url = "url"
|
||||
case title = "title"
|
||||
case starred = "starred"
|
||||
case read = "read"
|
||||
case readLater = "read_later"
|
||||
case body = "body"
|
||||
case author = "author"
|
||||
case feedID = "feed_id"
|
||||
case feedName = "feed_name"
|
||||
}
|
||||
|
||||
}
|
@ -1,19 +0,0 @@
|
||||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Jonathan Bennett on 2021-01-14.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerFeedItemId: Hashable, Codable {
|
||||
|
||||
let feedItemID: Int
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case feedItemID = "feed_item_id"
|
||||
}
|
||||
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
//
|
||||
// FeedWranglerFeedItemsRequest.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2021-01-14.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerFeedItemIdsRequest: Hashable, Codable {
|
||||
|
||||
let count: Int
|
||||
let feedItems: [FeedWranglerFeedItemId]
|
||||
let error: String?
|
||||
let result: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case count = "count"
|
||||
case feedItems = "feed_items"
|
||||
case error = "error"
|
||||
case result = "result"
|
||||
}
|
||||
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
//
|
||||
// FeedWranglerFeedItemsRequest.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-10-16.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerFeedItemsRequest: Hashable, Codable {
|
||||
|
||||
let count: Int
|
||||
let feedItems: [FeedWranglerFeedItem]
|
||||
let error: String?
|
||||
let result: String
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case count = "count"
|
||||
case feedItems = "feed_items"
|
||||
case error = "error"
|
||||
case result = "result"
|
||||
}
|
||||
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
//
|
||||
// FeedWranglerGenericResult.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-10-16.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerGenericResult: Hashable, Codable {
|
||||
|
||||
let error: String?
|
||||
let result: String
|
||||
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
//
|
||||
// FeedWranglerSubscription.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-10-16.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
import Foundation
|
||||
import RSCore
|
||||
import RSParser
|
||||
|
||||
struct FeedWranglerSubscription: Hashable, Codable {
|
||||
|
||||
let title: String
|
||||
let feedID: Int
|
||||
let feedURL: String
|
||||
let siteURL: String?
|
||||
|
||||
enum CodingKeys: String, CodingKey {
|
||||
case title = "title"
|
||||
case feedID = "feed_id"
|
||||
case feedURL = "feed_url"
|
||||
case siteURL = "site_url"
|
||||
}
|
||||
|
||||
}
|
@ -1,18 +0,0 @@
|
||||
//
|
||||
// FeedWranglerSubscriptionResult.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-11-20.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerSubscriptionResult: Hashable, Codable {
|
||||
|
||||
let feed: FeedWranglerSubscription
|
||||
let error: String?
|
||||
let result: String
|
||||
|
||||
}
|
||||
|
@ -1,17 +0,0 @@
|
||||
//
|
||||
// FeedWranglerSubscriptionsRequest.swift
|
||||
// Account
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-10-16.
|
||||
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
struct FeedWranglerSubscriptionsRequest: Hashable, Codable {
|
||||
|
||||
let feeds: [FeedWranglerSubscription]
|
||||
let error: String?
|
||||
let result: String
|
||||
|
||||
}
|
@ -26,14 +26,6 @@ public extension URLRequest {
|
||||
let base64 = data?.base64EncodedString()
|
||||
let auth = "Basic \(base64 ?? "")"
|
||||
setValue(auth, forHTTPHeaderField: HTTPRequestHeader.authorization)
|
||||
case .feedWranglerBasic:
|
||||
self.url = url.appendingQueryItems([
|
||||
URLQueryItem(name: "email", value: credentials.username),
|
||||
URLQueryItem(name: "password", value: credentials.secret),
|
||||
URLQueryItem(name: "client_key", value: SecretsManager.provider.feedWranglerKey)
|
||||
])
|
||||
case .feedWranglerToken:
|
||||
self.url = url.appendingQueryItem(URLQueryItem(name: "access_token", value: credentials.secret))
|
||||
case .newsBlurBasic:
|
||||
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: HTTPRequestHeader.contentType)
|
||||
httpMethod = "POST"
|
||||
|
@ -9,7 +9,6 @@ import Foundation
|
||||
import Secrets
|
||||
|
||||
struct FeedlyTestSecrets: SecretsProvider {
|
||||
var feedWranglerKey = ""
|
||||
var mercuryClientId = ""
|
||||
var mercuryClientSecret = ""
|
||||
var feedlyClientId = ""
|
||||
|
@ -28,10 +28,6 @@ struct AppAssets {
|
||||
return RSImage(named: "accountFeedly")
|
||||
}()
|
||||
|
||||
static var accountFeedWrangler: RSImage! = {
|
||||
return RSImage(named: "accountFeedWrangler")
|
||||
}()
|
||||
|
||||
static var accountFreshRSS: RSImage! = {
|
||||
return RSImage(named: "accountFreshRSS")
|
||||
}()
|
||||
@ -272,8 +268,6 @@ struct AppAssets {
|
||||
return AppAssets.accountFeedbin
|
||||
case .feedly:
|
||||
return AppAssets.accountFeedly
|
||||
case .feedWrangler:
|
||||
return AppAssets.accountFeedWrangler
|
||||
case .freshRSS:
|
||||
return AppAssets.accountFreshRSS
|
||||
case .inoreader:
|
||||
|
@ -89,11 +89,6 @@ final class AccountsDetailViewController: NSViewController, NSTextFieldDelegate
|
||||
accountsReaderAPIWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsWindowController = accountsReaderAPIWindowController
|
||||
break
|
||||
case .feedWrangler:
|
||||
let accountsFeedWranglerWindowController = AccountsFeedWranglerWindowController()
|
||||
accountsFeedWranglerWindowController.account = account
|
||||
accountsFeedWranglerWindowController.runSheetOnWindow(self.view.window!)
|
||||
accountsWindowController = accountsFeedWranglerWindowController
|
||||
case .newsBlur:
|
||||
let accountsNewsBlurWindowController = AccountsNewsBlurWindowController()
|
||||
accountsNewsBlurWindowController.account = account
|
||||
|
@ -1,234 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="17701" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
|
||||
<dependencies>
|
||||
<deployment identifier="macosx"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="17701"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
</dependencies>
|
||||
<objects>
|
||||
<customObject id="-2" userLabel="File's Owner" customClass="AccountsFeedWranglerWindowController" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<connections>
|
||||
<outlet property="actionButton" destination="9mz-D9-krh" id="ozu-6Q-9Lb"/>
|
||||
<outlet property="createNewAccountButton" destination="pPT-Cj-3vI" id="KAL-Y7-XQK"/>
|
||||
<outlet property="errorMessageLabel" destination="zwG-Ag-z8o" id="7a1-iJ-URN"/>
|
||||
<outlet property="noAccountTextField" destination="xEl-Ae-5r8" id="dU3-Jv-Aq8"/>
|
||||
<outlet property="passwordTextField" destination="JSa-LY-zNQ" id="5cF-bM-CJE"/>
|
||||
<outlet property="progressIndicator" destination="B0W-bh-Evv" id="Tiq-gx-s3F"/>
|
||||
<outlet property="signInTextField" destination="lti-yM-8LV" id="ZgR-2i-RXB"/>
|
||||
<outlet property="usernameTextField" destination="78p-Cf-f55" id="Gg5-Ce-RJv"/>
|
||||
<outlet property="window" destination="F0z-JX-Cv5" id="gIp-Ho-8D9"/>
|
||||
</connections>
|
||||
</customObject>
|
||||
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
|
||||
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
|
||||
<window title="Window" allowsToolTipsWhenApplicationIsInactive="NO" autorecalculatesKeyViewLoop="NO" releasedWhenClosed="NO" visibleAtLaunch="NO" animationBehavior="default" id="F0z-JX-Cv5">
|
||||
<windowStyleMask key="styleMask" titled="YES" closable="YES" miniaturizable="YES" resizable="YES"/>
|
||||
<windowPositionMask key="initialPositionMask" leftStrut="YES" rightStrut="YES" topStrut="YES" bottomStrut="YES"/>
|
||||
<rect key="contentRect" x="196" y="240" width="433" height="249"/>
|
||||
<rect key="screenRect" x="0.0" y="0.0" width="2560" height="1417"/>
|
||||
<view key="contentView" id="se5-gp-TjO">
|
||||
<rect key="frame" x="0.0" y="0.0" width="433" height="249"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<subviews>
|
||||
<stackView distribution="fill" orientation="horizontal" alignment="bottom" spacing="19" horizontalStackHuggingPriority="249.99998474121094" verticalStackHuggingPriority="249.99998474121094" detachesHiddenViews="YES" translatesAutoresizingMaskIntoConstraints="NO" id="7Ht-Fn-0Ya">
|
||||
<rect key="frame" x="217" y="229" width="0.0" height="0.0"/>
|
||||
</stackView>
|
||||
<gridView xPlacement="trailing" yPlacement="center" rowAlignment="none" rowSpacing="7" columnSpacing="8" translatesAutoresizingMaskIntoConstraints="NO" id="zBB-JH-huI">
|
||||
<rect key="frame" x="80" y="126" width="270" height="49"/>
|
||||
<rows>
|
||||
<gridRow id="DRl-lC-vUc"/>
|
||||
<gridRow id="eW8-uH-txq"/>
|
||||
</rows>
|
||||
<columns>
|
||||
<gridColumn id="fCQ-jY-Mts"/>
|
||||
<gridColumn xPlacement="leading" id="7CY-bX-6x4"/>
|
||||
</columns>
|
||||
<gridCells>
|
||||
<gridCell row="DRl-lC-vUc" column="fCQ-jY-Mts" id="4DI-01-jGD">
|
||||
<textField key="contentView" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Zy6-9c-8TI">
|
||||
<rect key="frame" x="23" y="31" width="41" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Email:" id="DqN-SV-v35">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</gridCell>
|
||||
<gridCell row="DRl-lC-vUc" column="7CY-bX-6x4" id="Z0b-qS-MUJ">
|
||||
<textField key="contentView" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="78p-Cf-f55">
|
||||
<rect key="frame" x="70" y="28" width="200" height="21"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="200" id="Qin-jm-4zt"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="me@email.com" drawsBackground="YES" id="fCk-Tf-q01">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</gridCell>
|
||||
<gridCell row="eW8-uH-txq" column="fCQ-jY-Mts" id="Hqa-3w-cQv">
|
||||
<textField key="contentView" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="wEx-TM-rPM">
|
||||
<rect key="frame" x="-2" y="3" width="66" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Password:" id="7g8-Kk-ISg">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</gridCell>
|
||||
<gridCell row="eW8-uH-txq" column="7CY-bX-6x4" id="m16-3v-9pf">
|
||||
<secureTextField key="contentView" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="JSa-LY-zNQ">
|
||||
<rect key="frame" x="70" y="0.0" width="200" height="21"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="200" id="eal-wa-1nU"/>
|
||||
</constraints>
|
||||
<secureTextFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" selectable="YES" editable="YES" sendsActionOnEndEditing="YES" borderStyle="bezel" placeholderString="•••••••••" drawsBackground="YES" usesSingleLineMode="YES" id="trK-OG-tBe">
|
||||
<font key="font" metaFont="system"/>
|
||||
<color key="textColor" name="controlTextColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
<allowedInputSourceLocales>
|
||||
<string>NSAllRomanInputSourcesLocaleIdentifier</string>
|
||||
</allowedInputSourceLocales>
|
||||
</secureTextFieldCell>
|
||||
</secureTextField>
|
||||
</gridCell>
|
||||
</gridCells>
|
||||
</gridView>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9mz-D9-krh">
|
||||
<rect key="frame" x="345" y="13" width="74" height="32"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="62" id="KMy-Qk-maN"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="push" title="Create" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="IMO-YT-k9Z">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
DQ
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="action:" target="-2" id="Kix-5a-5Og"/>
|
||||
</connections>
|
||||
</button>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="XAM-Hb-0Hw">
|
||||
<rect key="frame" x="263" y="13" width="82" height="32"/>
|
||||
<buttonCell key="cell" type="push" title="Cancel" bezelStyle="rounded" alignment="center" borderStyle="border" imageScaling="proportionallyDown" inset="2" id="ufs-ar-BAY">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" metaFont="system"/>
|
||||
<string key="keyEquivalent" base64-UTF8="YES">
|
||||
Gw
|
||||
</string>
|
||||
</buttonCell>
|
||||
<connections>
|
||||
<action selector="cancel:" target="-2" id="WAD-ES-hpq"/>
|
||||
</connections>
|
||||
</button>
|
||||
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Ssh-Dh-xbg">
|
||||
<rect key="frame" x="20" y="179" width="50" height="50"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="50" id="Ern-Kk-8LX"/>
|
||||
<constraint firstAttribute="width" constant="50" id="PLS-68-NMc"/>
|
||||
</constraints>
|
||||
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyUpOrDown" image="accountFeedWrangler" id="y38-YL-woC"/>
|
||||
</imageView>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="lti-yM-8LV">
|
||||
<rect key="frame" x="78" y="213" width="337" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Sign in to your Feed Wrangler account." id="ras-dj-nP8">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="xEl-Ae-5r8">
|
||||
<rect key="frame" x="78" y="192" width="231" height="16"/>
|
||||
<textFieldCell key="cell" lineBreakMode="clipping" title="Don’t have a Feed Wrangler account?" id="DFR-20-sjO">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="pPT-Cj-3vI">
|
||||
<rect key="frame" x="308" y="192" width="105" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="105" id="Ez2-gz-Wqf"/>
|
||||
</constraints>
|
||||
<buttonCell key="cell" type="roundRect" title="Create one here." bezelStyle="roundedRect" alignment="center" state="on" imageScaling="proportionallyDown" inset="2" id="tlF-nc-ZOr">
|
||||
<behavior key="behavior" pushIn="YES" lightByBackground="YES" lightByGray="YES"/>
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
</buttonCell>
|
||||
<color key="contentTintColor" name="AccentColor"/>
|
||||
<connections>
|
||||
<action selector="createAccountWithProvider:" target="-2" id="bp5-3n-RLW"/>
|
||||
</connections>
|
||||
</button>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="Uzn-QS-o4p">
|
||||
<rect key="frame" x="78" y="72" width="337" height="39"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="39" id="99q-po-zLV"/>
|
||||
<constraint firstAttribute="width" constant="333" id="oSi-jz-DZ8"/>
|
||||
</constraints>
|
||||
<textFieldCell key="cell" title="Your username and password will be encrypted and stored in Keychain. " id="83j-VH-GgC">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
<progressIndicator hidden="YES" wantsLayer="YES" horizontalHuggingPriority="750" verticalHuggingPriority="750" maxValue="100" displayedWhenStopped="NO" bezeled="NO" indeterminate="YES" controlSize="small" style="spinning" translatesAutoresizingMaskIntoConstraints="NO" id="B0W-bh-Evv">
|
||||
<rect key="frame" x="245" y="22" width="16" height="16"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="width" constant="16" id="ggl-Gq-PUV"/>
|
||||
<constraint firstAttribute="height" constant="16" id="m1z-4y-g41"/>
|
||||
</constraints>
|
||||
</progressIndicator>
|
||||
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="zwG-Ag-z8o">
|
||||
<rect key="frame" x="78" y="57" width="337" height="16"/>
|
||||
<textFieldCell key="cell" id="b2G-2g-1KR">
|
||||
<font key="font" usesAppearanceFont="YES"/>
|
||||
<color key="textColor" name="systemRedColor" catalog="System" colorSpace="catalog"/>
|
||||
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
|
||||
</textFieldCell>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="9mz-D9-krh" firstAttribute="leading" secondItem="XAM-Hb-0Hw" secondAttribute="trailing" constant="12" symbolic="YES" id="1li-1u-jpf"/>
|
||||
<constraint firstItem="zwG-Ag-z8o" firstAttribute="leading" secondItem="Uzn-QS-o4p" secondAttribute="leading" id="2gp-cR-WV4"/>
|
||||
<constraint firstItem="Ssh-Dh-xbg" firstAttribute="leading" secondItem="se5-gp-TjO" secondAttribute="leading" constant="20" symbolic="YES" id="3dK-9R-7wX"/>
|
||||
<constraint firstItem="lti-yM-8LV" firstAttribute="leading" secondItem="Ssh-Dh-xbg" secondAttribute="trailing" constant="10" id="8qB-Qh-zBJ"/>
|
||||
<constraint firstAttribute="trailing" secondItem="zwG-Ag-z8o" secondAttribute="trailing" constant="20" symbolic="YES" id="BVi-6b-iOO"/>
|
||||
<constraint firstItem="9mz-D9-krh" firstAttribute="leading" secondItem="XAM-Hb-0Hw" secondAttribute="trailing" constant="12" symbolic="YES" id="CC8-HR-FDy"/>
|
||||
<constraint firstItem="xEl-Ae-5r8" firstAttribute="top" secondItem="lti-yM-8LV" secondAttribute="bottom" constant="5" id="FOT-OS-h0G"/>
|
||||
<constraint firstAttribute="trailing" secondItem="lti-yM-8LV" secondAttribute="trailing" constant="20" symbolic="YES" id="Hxs-l1-XFt"/>
|
||||
<constraint firstItem="Uzn-QS-o4p" firstAttribute="leading" secondItem="Ssh-Dh-xbg" secondAttribute="trailing" constant="10" id="Lm2-GS-vEg"/>
|
||||
<constraint firstItem="XAM-Hb-0Hw" firstAttribute="centerY" secondItem="9mz-D9-krh" secondAttribute="centerY" id="M2M-fb-kfR"/>
|
||||
<constraint firstItem="zwG-Ag-z8o" firstAttribute="top" secondItem="Uzn-QS-o4p" secondAttribute="bottom" constant="-1" id="MII-TX-oBl"/>
|
||||
<constraint firstItem="pPT-Cj-3vI" firstAttribute="leading" secondItem="xEl-Ae-5r8" secondAttribute="trailing" constant="1" id="NXU-SK-5WO"/>
|
||||
<constraint firstAttribute="bottom" secondItem="9mz-D9-krh" secondAttribute="bottom" constant="20" id="PK2-Ye-400"/>
|
||||
<constraint firstItem="pPT-Cj-3vI" firstAttribute="centerY" secondItem="xEl-Ae-5r8" secondAttribute="centerY" id="XKR-hU-WpE"/>
|
||||
<constraint firstItem="Uzn-QS-o4p" firstAttribute="top" secondItem="JSa-LY-zNQ" secondAttribute="bottom" constant="15" id="Z84-bl-0dI"/>
|
||||
<constraint firstItem="XAM-Hb-0Hw" firstAttribute="leading" secondItem="B0W-bh-Evv" secondAttribute="trailing" constant="8" symbolic="YES" id="afl-Cl-zda"/>
|
||||
<constraint firstItem="Ssh-Dh-xbg" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" constant="20" symbolic="YES" id="dDr-Rs-AyZ"/>
|
||||
<constraint firstAttribute="bottom" secondItem="B0W-bh-Evv" secondAttribute="bottom" constant="22" id="dzj-Jm-8mI"/>
|
||||
<constraint firstItem="lti-yM-8LV" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" constant="20" symbolic="YES" id="eIn-Pl-krd"/>
|
||||
<constraint firstAttribute="trailing" secondItem="9mz-D9-krh" secondAttribute="trailing" constant="20" id="fVQ-zN-rKd"/>
|
||||
<constraint firstItem="7Ht-Fn-0Ya" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" constant="20" id="jlY-Jg-KJR"/>
|
||||
<constraint firstItem="zBB-JH-huI" firstAttribute="top" secondItem="xEl-Ae-5r8" secondAttribute="bottom" constant="17" id="rbr-SG-72y"/>
|
||||
<constraint firstItem="7Ht-Fn-0Ya" firstAttribute="centerX" secondItem="se5-gp-TjO" secondAttribute="centerX" id="tAZ-Te-w3H"/>
|
||||
<constraint firstItem="zBB-JH-huI" firstAttribute="leading" secondItem="Ssh-Dh-xbg" secondAttribute="trailing" constant="10" id="wWG-kT-6M7"/>
|
||||
<constraint firstItem="xEl-Ae-5r8" firstAttribute="leading" secondItem="Ssh-Dh-xbg" secondAttribute="trailing" constant="10" id="zAY-I9-eKa"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<connections>
|
||||
<outlet property="delegate" destination="-2" id="0bl-1N-AYu"/>
|
||||
</connections>
|
||||
<point key="canvasLocation" x="116.5" y="136.5"/>
|
||||
</window>
|
||||
</objects>
|
||||
<resources>
|
||||
<image name="accountFeedWrangler" width="261" height="261"/>
|
||||
<namedColor name="AccentColor">
|
||||
<color red="0.030999999493360519" green="0.41600000858306885" blue="0.93300002813339233" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||
</namedColor>
|
||||
</resources>
|
||||
</document>
|
@ -1,134 +0,0 @@
|
||||
//
|
||||
// AccountsFeedWranglerWindowController.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-08-29.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import AppKit
|
||||
import Account
|
||||
import RSWeb
|
||||
import Secrets
|
||||
|
||||
class AccountsFeedWranglerWindowController: NSWindowController {
|
||||
|
||||
@IBOutlet weak var signInTextField: NSTextField!
|
||||
@IBOutlet weak var noAccountTextField: NSTextField!
|
||||
@IBOutlet weak var createNewAccountButton: NSButton!
|
||||
@IBOutlet weak var progressIndicator: NSProgressIndicator!
|
||||
@IBOutlet weak var usernameTextField: NSTextField!
|
||||
@IBOutlet weak var passwordTextField: NSSecureTextField!
|
||||
@IBOutlet weak var errorMessageLabel: NSTextField!
|
||||
@IBOutlet weak var actionButton: NSButton!
|
||||
|
||||
var account: Account?
|
||||
|
||||
private weak var hostWindow: NSWindow?
|
||||
|
||||
convenience init() {
|
||||
self.init(windowNibName: NSNib.Name("AccountsFeedWrangler"))
|
||||
}
|
||||
|
||||
override func windowDidLoad() {
|
||||
if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) {
|
||||
usernameTextField.stringValue = credentials.username
|
||||
actionButton.title = NSLocalizedString("Update", comment: "Update")
|
||||
signInTextField.stringValue = NSLocalizedString("Update your Feed Wrangler account credentials.", comment: "SignIn")
|
||||
noAccountTextField.isHidden = true
|
||||
createNewAccountButton.isHidden = true
|
||||
} else {
|
||||
actionButton.title = NSLocalizedString("Create", comment: "Create")
|
||||
signInTextField.stringValue = NSLocalizedString("Sign in to your Feed Wrangler account.", comment: "SignIn")
|
||||
}
|
||||
enableAutofill()
|
||||
usernameTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
func runSheetOnWindow(_ hostWindow: NSWindow, completion: ((NSApplication.ModalResponse) -> Void)? = nil) {
|
||||
self.hostWindow = hostWindow
|
||||
hostWindow.beginSheet(window!, completionHandler: completion)
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
hostWindow!.endSheet(window!, returnCode: NSApplication.ModalResponse.cancel)
|
||||
}
|
||||
|
||||
@IBAction func action(_ sender: Any) {
|
||||
self.errorMessageLabel.stringValue = ""
|
||||
|
||||
guard !usernameTextField.stringValue.isEmpty && !passwordTextField.stringValue.isEmpty else {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("Username & password required.", comment: "Credentials Error")
|
||||
return
|
||||
}
|
||||
|
||||
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedWrangler, username: usernameTextField.stringValue) else {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("There is already a FeedWrangler account with that username created.", comment: "Duplicate Error")
|
||||
return
|
||||
}
|
||||
|
||||
actionButton.isEnabled = false
|
||||
progressIndicator.isHidden = false
|
||||
progressIndicator.startAnimation(self)
|
||||
|
||||
let credentials = Credentials(type: .feedWranglerBasic, username: usernameTextField.stringValue, secret: passwordTextField.stringValue)
|
||||
Account.validateCredentials(type: .feedWrangler, credentials: credentials) { [weak self] result in
|
||||
|
||||
guard let self = self else { return }
|
||||
|
||||
self.actionButton.isEnabled = true
|
||||
self.progressIndicator.isHidden = true
|
||||
self.progressIndicator.stopAnimation(self)
|
||||
|
||||
switch result {
|
||||
case .success(let validatedCredentials):
|
||||
guard let validatedCredentials = validatedCredentials else {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error")
|
||||
return
|
||||
}
|
||||
if self.account == nil {
|
||||
self.account = AccountManager.shared.createAccount(type: .feedWrangler)
|
||||
}
|
||||
|
||||
do {
|
||||
try self.account?.removeCredentials(type: .feedWranglerBasic)
|
||||
try self.account?.removeCredentials(type: .feedWranglerToken)
|
||||
try self.account?.storeCredentials(credentials)
|
||||
try self.account?.storeCredentials(validatedCredentials)
|
||||
|
||||
self.account?.refreshAll() { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
NSApplication.shared.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
self.hostWindow?.endSheet(self.window!, returnCode: NSApplication.ModalResponse.OK)
|
||||
} catch {
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")
|
||||
}
|
||||
|
||||
case .failure:
|
||||
|
||||
self.errorMessageLabel.stringValue = NSLocalizedString("Network error. Try again later.", comment: "Credentials Error")
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func createAccountWithProvider(_ sender: Any) {
|
||||
NSWorkspace.shared.open(URL(string: "https://feedwrangler.net/users/new")!)
|
||||
}
|
||||
|
||||
// MARK: Autofill
|
||||
func enableAutofill() {
|
||||
usernameTextField.contentType = .username
|
||||
passwordTextField.contentType = .password
|
||||
}
|
||||
}
|
@ -24,6 +24,7 @@ final class AccountsPreferencesViewController: NSViewController {
|
||||
@IBOutlet weak var deleteButton: NSButton!
|
||||
var addAccountDelegate: AccountsPreferencesAddAccountDelegate?
|
||||
var addAccountWindowController: NSWindowController?
|
||||
var addAccountsViewController: NSHostingController<AddAccountsView>?
|
||||
|
||||
private var sortedAccounts = [Account]()
|
||||
|
||||
@ -51,6 +52,7 @@ final class AccountsPreferencesViewController: NSViewController {
|
||||
@IBAction func addAccount(_ sender: Any) {
|
||||
let controller = NSHostingController(rootView: AddAccountsView(delegate: self))
|
||||
controller.rootView.parent = controller
|
||||
addAccountsViewController = controller
|
||||
presentAsSheet(controller)
|
||||
}
|
||||
|
||||
@ -168,10 +170,6 @@ extension AccountsPreferencesViewController: AccountsPreferencesAddAccountDelega
|
||||
let accountsFeedbinWindowController = AccountsFeedbinWindowController()
|
||||
accountsFeedbinWindowController.runSheetOnWindow(self.view.window!)
|
||||
addAccountWindowController = accountsFeedbinWindowController
|
||||
case .feedWrangler:
|
||||
let accountsFeedWranglerWindowController = AccountsFeedWranglerWindowController()
|
||||
accountsFeedWranglerWindowController.runSheetOnWindow(self.view.window!)
|
||||
addAccountWindowController = accountsFeedWranglerWindowController
|
||||
case .freshRSS, .inoreader, .bazQux, .theOldReader:
|
||||
let accountsReaderAPIWindowController = AccountsReaderAPIWindowController()
|
||||
accountsReaderAPIWindowController.accountType = accountType
|
||||
|
@ -1,38 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.737",
|
||||
"green" : "0.569",
|
||||
"red" : "0.118"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0.698",
|
||||
"green" : "0.529",
|
||||
"red" : "0.078"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "feedwranger-any-slice.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "feedwranger-dark-slice.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "feedwranger-any-slice@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "feedwranger-dark-slice@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "feedwranger-any-slice@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "feedwranger-dark-slice@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 153 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 77 KiB |
Before Width: | Height: | Size: 153 KiB |
@ -69,7 +69,6 @@
|
||||
<enumerator name="onmymac" code="Locl" description="An On my Mac (local) account"/>
|
||||
<enumerator name="feedly" code="Fdly" description="A Feedly account"/>
|
||||
<enumerator name="feedbin" code="Fdbn" description="A Feedbin account"/>
|
||||
<enumerator name="feed wrangler" code="FWrg" description="A Feed Wrangler account"/>
|
||||
<enumerator name="newsblur" code="NBlr" description="A Newsblur account"/>
|
||||
<enumerator name="fresh rss" code="Frsh" description="A Fresh RSS account"/>
|
||||
</enumeration>
|
||||
|
@ -164,8 +164,6 @@ class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectConta
|
||||
osType = "Fdly"
|
||||
case .feedbin:
|
||||
osType = "Fdbn"
|
||||
case .feedWrangler:
|
||||
osType = "FWrg"
|
||||
case .newsBlur:
|
||||
osType = "NBlr"
|
||||
case .freshRSS:
|
||||
|
@ -15,8 +15,6 @@ public enum CredentialsError: Error {
|
||||
|
||||
public enum CredentialsType: String {
|
||||
case basic = "password"
|
||||
case feedWranglerBasic = "feedWranglerBasic"
|
||||
case feedWranglerToken = "feedWranglerToken"
|
||||
case newsBlurBasic = "newsBlurBasic"
|
||||
case newsBlurSessionId = "newsBlurSessionId"
|
||||
case readerBasic = "readerBasic"
|
||||
|
@ -8,7 +8,6 @@
|
||||
import Foundation
|
||||
|
||||
public protocol SecretsProvider {
|
||||
var feedWranglerKey: String { get }
|
||||
var mercuryClientId: String { get }
|
||||
var mercuryClientSecret: String { get }
|
||||
var feedlyClientId: String { get }
|
||||
|
@ -38,8 +38,6 @@ extension AccountType {
|
||||
return NSLocalizedString("BazQux", comment: "Account name")
|
||||
case .cloudKit:
|
||||
return NSLocalizedString("iCloud", comment: "Account name")
|
||||
case .feedWrangler:
|
||||
return NSLocalizedString("FeedWrangler", comment: "Account name")
|
||||
case .feedbin:
|
||||
return NSLocalizedString("Feedbin", comment: "Account name")
|
||||
case .feedly:
|
||||
@ -73,8 +71,6 @@ extension AccountType {
|
||||
return Image("accountBazQux")
|
||||
case .cloudKit:
|
||||
return Image("accountCloudKit")
|
||||
case .feedWrangler:
|
||||
return Image("accountFeedWrangler")
|
||||
case .feedbin:
|
||||
return Image("accountFeedbin")
|
||||
case .feedly:
|
||||
|
@ -2,7 +2,7 @@
|
||||
%{
|
||||
import os
|
||||
|
||||
secrets = ['FEED_WRANGLER_KEY', 'MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'INOREADER_APP_ID', 'INOREADER_APP_KEY']
|
||||
secrets = ['MERCURY_CLIENT_ID', 'MERCURY_CLIENT_SECRET', 'FEEDLY_CLIENT_ID', 'FEEDLY_CLIENT_SECRET', 'INOREADER_APP_ID', 'INOREADER_APP_KEY']
|
||||
|
||||
def chunks(seq, size):
|
||||
return (seq[i:(i + size)] for i in range(0, len(seq), size))
|
||||
|
@ -1,9 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21225" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="23504" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||
<dependencies>
|
||||
<deployment identifier="iOS"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21207"/>
|
||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="23506"/>
|
||||
<capability name="Named colors" minToolsVersion="9.0"/>
|
||||
<capability name="System colors in document resources" minToolsVersion="11.0"/>
|
||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||
@ -41,187 +40,6 @@
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3177" y="-528"/>
|
||||
</scene>
|
||||
<!--Feed Wrangler-->
|
||||
<scene sceneID="66V-IF-sys">
|
||||
<objects>
|
||||
<tableViewController id="fPs-Pp-Qk4" customClass="FeedWranglerAccountViewController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<tableView key="view" clipsSubviews="YES" contentMode="scaleToFill" alwaysBounceVertical="YES" dataMode="static" style="insetGrouped" separatorStyle="default" rowHeight="-1" estimatedRowHeight="-1" sectionHeaderHeight="18" sectionFooterHeight="18" id="pvq-Hi-fuC">
|
||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||
<view key="tableFooterView" contentMode="scaleToFill" id="lTu-NN-o2n">
|
||||
<rect key="frame" x="0.0" y="202.5" width="414" height="150"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" textAlignment="center" lineBreakMode="wordWrap" numberOfLines="0" baselineAdjustment="alignBaselines" adjustsFontForContentSizeCategory="YES" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="6Ik-WC-e74">
|
||||
<rect key="frame" x="20" y="8" width="373" height="78.5"/>
|
||||
<string key="text">Sign in to your Feed Wranger account to sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.
|
||||
|
||||
Don’t have a Feed Wrangler account?</string>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<color key="textColor" systemColor="secondaryLabelColor"/>
|
||||
<nil key="highlightedColor"/>
|
||||
</label>
|
||||
<button opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="8ez-iV-55B">
|
||||
<rect key="frame" x="169.5" y="85" width="75" height="27"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleCaption1"/>
|
||||
<state key="normal" title="Sign Up Here"/>
|
||||
<connections>
|
||||
<action selector="signUpWithProvider:" destination="fPs-Pp-Qk4" eventType="touchUpInside" id="Was-na-sg5"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<color key="backgroundColor" systemColor="systemGroupedBackgroundColor"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="trailing" secondItem="6Ik-WC-e74" secondAttribute="trailing" constant="21" id="1CB-EI-IPX"/>
|
||||
<constraint firstItem="6Ik-WC-e74" firstAttribute="top" secondItem="lTu-NN-o2n" secondAttribute="top" constant="8" id="BJc-vU-mFv"/>
|
||||
<constraint firstItem="8ez-iV-55B" firstAttribute="top" secondItem="6Ik-WC-e74" secondAttribute="bottom" constant="-1.5" id="CUU-H2-Exb"/>
|
||||
<constraint firstItem="6Ik-WC-e74" firstAttribute="leading" secondItem="lTu-NN-o2n" secondAttribute="leading" constant="20" symbolic="YES" id="fhG-hY-7xP"/>
|
||||
<constraint firstItem="8ez-iV-55B" firstAttribute="centerX" secondItem="lTu-NN-o2n" secondAttribute="centerX" id="kbC-MK-8IL"/>
|
||||
</constraints>
|
||||
</view>
|
||||
<sections>
|
||||
<tableViewSection id="aBu-yB-8do">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="7kE-yi-fSC">
|
||||
<rect key="frame" x="20" y="18" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="7kE-yi-fSC" id="03i-i5-FcH">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Email" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="o06-fe-i3S">
|
||||
<rect key="frame" x="20" y="11" width="334" height="22"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" spellCheckingType="no" keyboardType="emailAddress" textContentType="username"/>
|
||||
</textField>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="o06-fe-i3S" firstAttribute="centerY" secondItem="03i-i5-FcH" secondAttribute="centerY" id="CZh-l6-1rR"/>
|
||||
<constraint firstItem="o06-fe-i3S" firstAttribute="leading" secondItem="03i-i5-FcH" secondAttribute="leading" constant="20" id="ELy-L4-kqa"/>
|
||||
<constraint firstAttribute="trailing" secondItem="o06-fe-i3S" secondAttribute="trailing" constant="20" id="Zqt-e6-Hyi"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="DZE-9N-drH">
|
||||
<rect key="frame" x="20" y="61.5" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="DZE-9N-drH" id="R0T-Op-v3i">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<textField opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="center" placeholder="Password" textAlignment="natural" adjustsFontForContentSizeCategory="YES" minimumFontSize="17" translatesAutoresizingMaskIntoConstraints="NO" id="Z6i-nX-CwJ">
|
||||
<rect key="frame" x="20" y="11.5" width="284" height="21"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<textInputTraits key="textInputTraits" secureTextEntry="YES" textContentType="password"/>
|
||||
</textField>
|
||||
<button opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="251" contentHorizontalAlignment="center" contentVerticalAlignment="center" buttonType="system" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="lBg-Pn-8ao">
|
||||
<rect key="frame" x="312" y="5.5" width="42" height="33"/>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Show"/>
|
||||
<connections>
|
||||
<action selector="showHidePassword:" destination="fPs-Pp-Qk4" eventType="touchUpInside" id="nEJ-ls-ZGH"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="lBg-Pn-8ao" firstAttribute="leading" secondItem="Z6i-nX-CwJ" secondAttribute="trailing" constant="8" symbolic="YES" id="Ljx-ZV-dqS"/>
|
||||
<constraint firstItem="Z6i-nX-CwJ" firstAttribute="leading" secondItem="R0T-Op-v3i" secondAttribute="leading" constant="20" id="MJf-rd-h8l"/>
|
||||
<constraint firstAttribute="trailing" secondItem="lBg-Pn-8ao" secondAttribute="trailing" constant="20" symbolic="YES" id="QbM-sI-Grd"/>
|
||||
<constraint firstItem="lBg-Pn-8ao" firstAttribute="centerY" secondItem="R0T-Op-v3i" secondAttribute="centerY" id="jZb-uf-0i9"/>
|
||||
<constraint firstItem="Z6i-nX-CwJ" firstAttribute="centerY" secondItem="R0T-Op-v3i" secondAttribute="centerY" id="zbK-Fj-UdZ"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
<tableViewSection id="91x-2Y-RyC">
|
||||
<cells>
|
||||
<tableViewCell clipsSubviews="YES" contentMode="scaleToFill" preservesSuperviewLayoutMargins="YES" selectionStyle="none" indentationWidth="10" id="DdI-cc-rL4">
|
||||
<rect key="frame" x="20" y="141" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<tableViewCellContentView key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" preservesSuperviewLayoutMargins="YES" insetsLayoutMarginsFromSafeArea="NO" tableViewCell="DdI-cc-rL4" id="VUw-ck-1xk">
|
||||
<rect key="frame" x="0.0" y="0.0" width="374" height="43.5"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
<subviews>
|
||||
<button opaque="NO" contentMode="scaleToFill" enabled="NO" contentHorizontalAlignment="center" contentVerticalAlignment="center" lineBreakMode="middleTruncation" translatesAutoresizingMaskIntoConstraints="NO" id="kKc-mk-vsU" customClass="VibrantButton" customModule="NetNewsWire" customModuleProvider="target">
|
||||
<rect key="frame" x="0.0" y="-0.5" width="374" height="44.5"/>
|
||||
<constraints>
|
||||
<constraint firstAttribute="height" constant="44" id="Ucm-6X-Jvf"/>
|
||||
</constraints>
|
||||
<fontDescription key="fontDescription" style="UICTFontTextStyleBody"/>
|
||||
<state key="normal" title="Action">
|
||||
<color key="titleColor" name="secondaryAccentColor"/>
|
||||
</state>
|
||||
<connections>
|
||||
<action selector="action:" destination="fPs-Pp-Qk4" eventType="touchUpInside" id="K0L-le-dSs"/>
|
||||
</connections>
|
||||
</button>
|
||||
</subviews>
|
||||
<constraints>
|
||||
<constraint firstItem="kKc-mk-vsU" firstAttribute="centerY" secondItem="VUw-ck-1xk" secondAttribute="centerY" id="6Kf-JS-VU9"/>
|
||||
<constraint firstAttribute="trailing" secondItem="kKc-mk-vsU" secondAttribute="trailing" id="hLa-MZ-Dc6"/>
|
||||
<constraint firstItem="kKc-mk-vsU" firstAttribute="leading" secondItem="VUw-ck-1xk" secondAttribute="leading" id="iec-dp-dkS"/>
|
||||
</constraints>
|
||||
</tableViewCellContentView>
|
||||
</tableViewCell>
|
||||
</cells>
|
||||
</tableViewSection>
|
||||
</sections>
|
||||
<connections>
|
||||
<outlet property="dataSource" destination="fPs-Pp-Qk4" id="C5c-gU-SJN"/>
|
||||
<outlet property="delegate" destination="fPs-Pp-Qk4" id="gf8-01-E1Y"/>
|
||||
</connections>
|
||||
</tableView>
|
||||
<navigationItem key="navigationItem" title="Feed Wrangler" id="D6L-4j-gVo">
|
||||
<barButtonItem key="leftBarButtonItem" systemItem="cancel" id="zbP-iL-kfC">
|
||||
<connections>
|
||||
<action selector="cancel:" destination="fPs-Pp-Qk4" id="C0j-OR-yQ2"/>
|
||||
</connections>
|
||||
</barButtonItem>
|
||||
<barButtonItem key="rightBarButtonItem" id="ojY-jN-yDr">
|
||||
<view key="customView" contentMode="scaleToFill" id="6k3-VP-uPP">
|
||||
<rect key="frame" x="374" y="12" width="20" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
<subviews>
|
||||
<activityIndicatorView opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" fixedFrame="YES" style="medium" translatesAutoresizingMaskIntoConstraints="NO" id="mVm-hL-hqw">
|
||||
<rect key="frame" x="36" y="6" width="20" height="20"/>
|
||||
<autoresizingMask key="autoresizingMask" flexibleMaxX="YES" flexibleMaxY="YES"/>
|
||||
</activityIndicatorView>
|
||||
</subviews>
|
||||
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
||||
</view>
|
||||
</barButtonItem>
|
||||
</navigationItem>
|
||||
<connections>
|
||||
<outlet property="actionButton" destination="kKc-mk-vsU" id="TXr-cm-Oyp"/>
|
||||
<outlet property="activityIndicator" destination="mVm-hL-hqw" id="8Og-kO-70M"/>
|
||||
<outlet property="cancelBarButtonItem" destination="zbP-iL-kfC" id="TT3-iu-IvG"/>
|
||||
<outlet property="emailTextField" destination="o06-fe-i3S" id="WHW-3E-trH"/>
|
||||
<outlet property="footerLabel" destination="6Ik-WC-e74" id="KwB-tD-kTN"/>
|
||||
<outlet property="passwordTextField" destination="Z6i-nX-CwJ" id="p36-53-RsD"/>
|
||||
<outlet property="showHideButton" destination="lBg-Pn-8ao" id="GgE-Nx-gFL"/>
|
||||
</connections>
|
||||
</tableViewController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="Hsf-Kf-Hxa" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3870" y="145"/>
|
||||
</scene>
|
||||
<!--Modal Navigation Controller-->
|
||||
<scene sceneID="q8q-hQ-uMk">
|
||||
<objects>
|
||||
<navigationController storyboardIdentifier="FeedWranglerAccountNavigationViewController" id="p2l-Ub-oeO" customClass="ModalNavigationController" customModule="NetNewsWire" customModuleProvider="target" sceneMemberID="viewController">
|
||||
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="D1T-5x-xpV">
|
||||
<rect key="frame" x="0.0" y="48" width="414" height="44"/>
|
||||
<autoresizingMask key="autoresizingMask"/>
|
||||
</navigationBar>
|
||||
<connections>
|
||||
<segue destination="fPs-Pp-Qk4" kind="relationship" relationship="rootViewController" id="7Qk-Xc-9XZ"/>
|
||||
</connections>
|
||||
</navigationController>
|
||||
<placeholder placeholderIdentifier="IBFirstResponder" id="V1s-8K-v9X" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
|
||||
</objects>
|
||||
<point key="canvasLocation" x="3870" y="-528"/>
|
||||
</scene>
|
||||
<!--On My Device-->
|
||||
<scene sceneID="J93-FN-Yey">
|
||||
<objects>
|
||||
|
@ -1,188 +0,0 @@
|
||||
//
|
||||
// FeedWranglerAccountViewController.swift
|
||||
// NetNewsWire
|
||||
//
|
||||
// Created by Jonathan Bennett on 2019-11-24.
|
||||
// Copyright © 2019 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Account
|
||||
import RSWeb
|
||||
import Secrets
|
||||
import SafariServices
|
||||
|
||||
class FeedWranglerAccountViewController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var activityIndicator: UIActivityIndicatorView!
|
||||
@IBOutlet weak var cancelBarButtonItem: UIBarButtonItem!
|
||||
@IBOutlet weak var emailTextField: UITextField!
|
||||
@IBOutlet weak var passwordTextField: UITextField!
|
||||
@IBOutlet weak var showHideButton: UIButton!
|
||||
@IBOutlet weak var actionButton: UIButton!
|
||||
@IBOutlet weak var footerLabel: UILabel!
|
||||
|
||||
weak var account: Account?
|
||||
weak var delegate: AddAccountDismissDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupFooter()
|
||||
|
||||
activityIndicator.isHidden = true
|
||||
emailTextField.delegate = self
|
||||
passwordTextField.delegate = self
|
||||
|
||||
if let account = account, let credentials = try? account.retrieveCredentials(type: .feedWranglerBasic) {
|
||||
actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal)
|
||||
emailTextField.text = credentials.username
|
||||
passwordTextField.text = credentials.secret
|
||||
} else {
|
||||
actionButton.setTitle(NSLocalizedString("Add Account", comment: "Update Credentials"), for: .normal)
|
||||
}
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: emailTextField)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField)
|
||||
|
||||
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
}
|
||||
|
||||
private func setupFooter() {
|
||||
footerLabel.text = NSLocalizedString("Sign in to your Feed Wrangler account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDon’t have a Feed Wrangler account?", comment: "Feed Wrangler")
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
if section == 0 {
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
|
||||
headerView.imageView.image = AppAssets.image(for: .feedWrangler)
|
||||
return headerView
|
||||
} else {
|
||||
return super.tableView(tableView, viewForHeaderInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@IBAction func showHidePassword(_ sender: Any) {
|
||||
if passwordTextField.isSecureTextEntry {
|
||||
passwordTextField.isSecureTextEntry = false
|
||||
showHideButton.setTitle(NSLocalizedString("Hide", comment: "Button Label"), for: .normal)
|
||||
} else {
|
||||
passwordTextField.isSecureTextEntry = true
|
||||
showHideButton.setTitle(NSLocalizedString("Show", comment: "Button Label"), for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func action(_ sender: Any) {
|
||||
guard let email = emailTextField.text, let password = passwordTextField.text else {
|
||||
showError(NSLocalizedString("Username & password required.", comment: "Credentials Error"))
|
||||
return
|
||||
}
|
||||
// When you fill in the email address via auto-complete it adds extra whitespace
|
||||
let trimmedEmail = email.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedWrangler, username: trimmedEmail) else {
|
||||
showError(NSLocalizedString("There is already a FeedWrangler account with that username created.", comment: "Duplicate Error"))
|
||||
return
|
||||
}
|
||||
|
||||
resignFirstResponder()
|
||||
toggleActivityIndicatorAnimation(visible: true)
|
||||
setNavigationEnabled(to: false)
|
||||
|
||||
let credentials = Credentials(type: .feedWranglerBasic, username: trimmedEmail, secret: password)
|
||||
Account.validateCredentials(type: .feedWrangler, credentials: credentials) { result in
|
||||
|
||||
self.toggleActivityIndicatorAnimation(visible: false)
|
||||
self.setNavigationEnabled(to: true)
|
||||
|
||||
switch result {
|
||||
case .success(let validatedCredentials):
|
||||
guard let validatedCredentials = validatedCredentials else {
|
||||
self.showError(NSLocalizedString("Invalid email/password combination.", comment: "Credentials Error"))
|
||||
return
|
||||
}
|
||||
|
||||
if self.account == nil {
|
||||
self.account = AccountManager.shared.createAccount(type: .feedWrangler)
|
||||
}
|
||||
|
||||
do {
|
||||
try self.account?.removeCredentials(type: .feedWranglerBasic)
|
||||
try self.account?.removeCredentials(type: .feedWranglerToken)
|
||||
try self.account?.storeCredentials(credentials)
|
||||
try self.account?.storeCredentials(validatedCredentials)
|
||||
|
||||
self.account?.refreshAll { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
case .failure(let error):
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
self.delegate?.dismiss()
|
||||
|
||||
} catch {
|
||||
self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error"))
|
||||
}
|
||||
case .failure:
|
||||
self.showError(NSLocalizedString("Network error. Try again later.", comment: "Credentials Error"))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@IBAction func signUpWithProvider(_ sender: Any) {
|
||||
let url = URL(string: "https://feedwrangler.net/users/new")!
|
||||
let safari = SFSafariViewController(url: url)
|
||||
safari.modalPresentationStyle = .currentContext
|
||||
self.present(safari, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
@objc func textDidChange(_ note: Notification) {
|
||||
actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false)
|
||||
}
|
||||
|
||||
|
||||
private func showError(_ message: String) {
|
||||
presentError(title: NSLocalizedString("Error", comment: "Credentials Error"), message: message)
|
||||
}
|
||||
|
||||
private func setNavigationEnabled(to value:Bool){
|
||||
cancelBarButtonItem.isEnabled = value
|
||||
actionButton.isEnabled = value
|
||||
}
|
||||
|
||||
private func toggleActivityIndicatorAnimation(visible value: Bool){
|
||||
activityIndicator.isHidden = !value
|
||||
if value {
|
||||
activityIndicator.startAnimating()
|
||||
} else {
|
||||
activityIndicator.stopAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
extension FeedWranglerAccountViewController: UITextFieldDelegate {
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
if textField == emailTextField {
|
||||
passwordTextField.becomeFirstResponder()
|
||||
} else {
|
||||
textField.resignFirstResponder()
|
||||
action(self)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
@ -27,10 +27,6 @@ struct AppAssets {
|
||||
return UIImage(named: "accountFeedly")!
|
||||
}()
|
||||
|
||||
static var accountFeedWranglerImage: UIImage = {
|
||||
return UIImage(named: "accountFeedWrangler")!
|
||||
}()
|
||||
|
||||
static var accountFreshRSSImage: UIImage = {
|
||||
return UIImage(named: "accountFreshRSS")!
|
||||
}()
|
||||
@ -268,8 +264,6 @@ struct AppAssets {
|
||||
return AppAssets.accountFeedbinImage
|
||||
case .feedly:
|
||||
return AppAssets.accountFeedlyImage
|
||||
case .feedWrangler:
|
||||
return AppAssets.accountFeedWranglerImage
|
||||
case .freshRSS:
|
||||
return AppAssets.accountFreshRSSImage
|
||||
case .newsBlur:
|
||||
|
@ -69,12 +69,6 @@ class AccountInspectorViewController: UITableViewController {
|
||||
addViewController.account = account
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
present(navController, animated: true)
|
||||
case .feedWrangler:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedWranglerAccountNavigationViewController") as! UINavigationController
|
||||
let addViewController = navController.topViewController as! FeedWranglerAccountViewController
|
||||
addViewController.account = account
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
present(navController, animated: true)
|
||||
case .newsBlur:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "NewsBlurAccountNavigationViewController") as! UINavigationController
|
||||
let addViewController = navController.topViewController as! NewsBlurAccountViewController
|
||||
|
@ -1,38 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "188",
|
||||
"green" : "145",
|
||||
"red" : "30"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "178",
|
||||
"green" : "135",
|
||||
"red" : "20"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
@ -1,59 +0,0 @@
|
||||
{
|
||||
"images" : [
|
||||
{
|
||||
"filename" : "feedwranger-any.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "feedwranger-dark.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "feedwranger-any@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "feedwranger-dark@2x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"filename" : "feedwranger-any@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"filename" : "feedwranger-dark@3x.png",
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "original"
|
||||
}
|
||||
}
|
Before Width: | Height: | Size: 25 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 124 KiB |
Before Width: | Height: | Size: 26 KiB |
Before Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 125 KiB |
@ -56,7 +56,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
|
||||
return [.cloudKit]
|
||||
case .web:
|
||||
#if DEBUG
|
||||
return [.bazQux, .feedbin, .feedly, .feedWrangler, .inoreader, .newsBlur, .theOldReader]
|
||||
return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader]
|
||||
#else
|
||||
return [.bazQux, .feedbin, .feedly, .inoreader, .newsBlur, .theOldReader]
|
||||
#endif
|
||||
@ -142,7 +142,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
|
||||
cell.comboNameLabel?.text = AddAccountSections.web.sectionContent[indexPath.row].localizedAccountName()
|
||||
cell.comboImage?.image = AppAssets.image(for: AddAccountSections.web.sectionContent[indexPath.row])
|
||||
let type = AddAccountSections.web.sectionContent[indexPath.row]
|
||||
if (type == .feedly || type == .feedWrangler || type == .inoreader) && AppDefaults.shared.isDeveloperBuild {
|
||||
if (type == .feedly || type == .inoreader) && AppDefaults.shared.isDeveloperBuild {
|
||||
cell.isUserInteractionEnabled = false
|
||||
cell.comboNameLabel?.isEnabled = false
|
||||
}
|
||||
@ -201,12 +201,6 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
|
||||
addAccount.delegate = self
|
||||
addAccount.presentationAnchor = self.view.window!
|
||||
MainThreadOperationQueue.shared.add(addAccount)
|
||||
case .feedWrangler:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedWranglerAccountNavigationViewController") as! UINavigationController
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
let addViewController = navController.topViewController as! FeedWranglerAccountViewController
|
||||
addViewController.delegate = self
|
||||
present(navController, animated: true)
|
||||
case .newsBlur:
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "NewsBlurAccountNavigationViewController") as! UINavigationController
|
||||
navController.modalPresentationStyle = .currentContext
|
||||
|