Allow adding Feed Wrangler accounts

This commit is contained in:
Jonathan Bennett 2019-09-28 00:44:58 -04:00
parent 5dfb4f6488
commit 50428f3179
14 changed files with 606 additions and 6 deletions

View File

@ -226,6 +226,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
self.delegate = ReaderAPIAccountDelegate(dataFolder: dataFolder, transport: transport)
case .feedly:
self.delegate = FeedlyAccountDelegate(dataFolder: dataFolder, transport: transport)
case .feedWrangler:
self.delegate = FeedWranglerAccountDelegate(dataFolder: dataFolder, transport: transport)
default:
return nil
}
@ -302,6 +304,8 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
FeedbinAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
case .freshRSS:
ReaderAPIAccountDelegate.validateCredentials(transport: transport, credentials: credentials, endpoint: endpoint, completion: completion)
case .feedWrangler:
FeedWranglerAccountDelegate.validateCredentials(transport: transport, credentials: credentials, completion: completion)
default:
break
}

View File

@ -7,6 +7,9 @@
objects = {
/* Begin PBXBuildFile section */
3B29BC6E233EA83C002A346D /* FeedWranglerAPICaller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B29BC6C233EA83C002A346D /* FeedWranglerAPICaller.swift */; };
3B29BC6F233EA83C002A346D /* FeedWranglerAccountDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B29BC6D233EA83C002A346D /* FeedWranglerAccountDelegate.swift */; };
3B90F51F233F0EF800D481CC /* FeedWranglerConfig.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B90F51E233F0EF800D481CC /* FeedWranglerConfig.swift */; };
5107A099227DE42E00C7C3C5 /* AccountCredentialsTest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */; };
5107A09B227DE49500C7C3C5 /* TestAccountManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */; };
5107A09D227DE77700C7C3C5 /* TestTransport.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5107A09C227DE77700C7C3C5 /* TestTransport.swift */; };
@ -144,6 +147,9 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
3B29BC6C233EA83C002A346D /* FeedWranglerAPICaller.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerAPICaller.swift; sourceTree = "<group>"; };
3B29BC6D233EA83C002A346D /* FeedWranglerAccountDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedWranglerAccountDelegate.swift; sourceTree = "<group>"; };
3B90F51E233F0EF800D481CC /* FeedWranglerConfig.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FeedWranglerConfig.swift; sourceTree = "<group>"; };
5107A098227DE42E00C7C3C5 /* AccountCredentialsTest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountCredentialsTest.swift; sourceTree = "<group>"; };
5107A09A227DE49500C7C3C5 /* TestAccountManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestAccountManager.swift; sourceTree = "<group>"; };
5107A09C227DE77700C7C3C5 /* TestTransport.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TestTransport.swift; sourceTree = "<group>"; };
@ -268,6 +274,16 @@
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
3B29BC6B233EA83C002A346D /* FeedWrangler */ = {
isa = PBXGroup;
children = (
3B29BC6C233EA83C002A346D /* FeedWranglerAPICaller.swift */,
3B29BC6D233EA83C002A346D /* FeedWranglerAccountDelegate.swift */,
3B90F51E233F0EF800D481CC /* FeedWranglerConfig.swift */,
);
path = FeedWrangler;
sourceTree = "<group>";
};
515E4EB12324FF7D0057B0E7 /* Credentials */ = {
isa = PBXGroup;
children = (
@ -405,6 +421,7 @@
84245C7D1FDDD2580074AFBB /* Feedbin */,
552032EA229D5D5A009559E0 /* ReaderAPI */,
9EA31339231E368100268BA0 /* Feedly */,
3B29BC6B233EA83C002A346D /* FeedWrangler */,
848935031F62484F00CEBD24 /* AccountTests */,
848934F71F62484F00CEBD24 /* Products */,
8469F80F1F6DC3C10084783E /* Frameworks */,
@ -665,14 +682,17 @@
84C8B3F41F89DE430053CCA6 /* DataExtensions.swift in Sources */,
552032F9229D5D5A009559E0 /* ReaderAPISubscription.swift in Sources */,
84C3654A1F899F3B001EC85C /* CombinedRefreshProgress.swift in Sources */,
3B29BC6F233EA83C002A346D /* FeedWranglerAccountDelegate.swift in Sources */,
9EC688EE232C58E800A8D0A2 /* OAuthAuthorizationCodeGranting.swift in Sources */,
9EC688EC232C583300A8D0A2 /* FeedlyAccountDelegate+OAuth.swift in Sources */,
8469F81C1F6DD15E0084783E /* Account.swift in Sources */,
9EAEC60E2332FEC20085D7C9 /* FeedlyFeed.swift in Sources */,
5144EA4E227B829A00D19003 /* FeedbinAccountDelegate.swift in Sources */,
9EA3133B231E368100268BA0 /* FeedlyAccountDelegate.swift in Sources */,
3B29BC6E233EA83C002A346D /* FeedWranglerAPICaller.swift in Sources */,
51E5959B228C781500FCC42B /* FeedbinStarredEntry.swift in Sources */,
9E1D155923343B2A00F4944C /* FeedlyGetStreamParsedItemsOperation.swift in Sources */,
3B90F51F233F0EF800D481CC /* FeedWranglerConfig.swift in Sources */,
846E77451F6EF9B900A165E2 /* Container.swift in Sources */,
9E1D15532334304B00F4944C /* FeedlyGetCollectionStreamOperation.swift in Sources */,
9E12B0202334696A00ADE5A0 /* FeedlyCreateFeedsForCollectionFoldersOperation.swift in Sources */,

View File

@ -15,6 +15,8 @@ public enum CredentialsError: Error {
public enum CredentialsType: String {
case basic = "password"
case feedWranglerBasic = "feedWranglerBasic"
case feedWranglerToken = "feedWranglerToken"
case readerBasic = "readerBasic"
case readerAPIKey = "readerAPIKey"
case oauthAccessToken = "oauthAccessToken"

View File

@ -25,6 +25,19 @@ public extension URLRequest {
let base64 = data?.base64EncodedString()
let auth = "Basic \(base64 ?? "")"
setValue(auth, forHTTPHeaderField: HTTPRequestHeader.authorization)
case .feedWranglerBasic:
guard var components = URLComponents(url: url, resolvingAgainstBaseURL: false) else {
return
}
components.queryItems = [
URLQueryItem(name: "email", value: credentials.username),
URLQueryItem(name: "password", value: credentials.secret),
URLQueryItem(name: "client_key", value: FeedWranglerConfig.clientKey)
]
self.url = components.url
case .feedWranglerToken:
fatalError() // TODO: implement
case .readerBasic:
setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
httpMethod = "POST"

View File

@ -0,0 +1,59 @@
//
// FeedWranglerAPICaller.swift
// Account
//
// Created by Jonathan Bennett on 2019-08-29.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
import Foundation
import RSWeb
final class FeedWranglerAPICaller: NSObject {
private var transport: Transport!
var credentials: Credentials?
weak var accountMetadata: AccountMetadata?
init(transport: Transport) {
super.init()
self.transport = transport
}
func validateCredentials(completion: @escaping (Result<Credentials?, Error>) -> Void) {
let callURL = FeedWranglerConfig.clientURL.appendingPathComponent("users/authorize")
let request = URLRequest(url: callURL, credentials: credentials)
let username = self.credentials?.username ?? ""
transport.send(request: request) { result in
switch result {
case .success(let (_, data)):
guard let data = data else {
completion(.success(nil))
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String:Any] {
if let accessToken = json["access_token"] as? String {
let authCredentials = Credentials(type: .feedWranglerToken, username: username, secret: accessToken)
completion(.success(authCredentials))
return
}
}
completion(.success(nil))
} catch let error {
completion(.failure(error))
}
case .failure(let error):
completion(.failure(error))
}
}
}
}

View File

@ -0,0 +1,156 @@
//
// 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
final class FeedWranglerAccountDelegate: AccountDelegate {
var behaviors: AccountBehaviors = []
var isOPMLImportInProgress = false
var server: String? = FeedWranglerConfig.clientPath
var 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("/Sync.sqlite"))
}
func refreshAll(for account: Account, completion: @escaping (Result<Void, Error>) -> Void) {
refreshProgress.addToNumberOfTasksAndRemaining(6)
self.sendArticleStatus(for: account) {
self.refreshArticleStatus(for: account) {
self.refreshArticles(for: account) {
self.refreshMissingArticles(for: account) {
self.refreshProgress.clear()
DispatchQueue.main.async {
completion(.success(()))
}
}
}
}
}
}
func refreshArticles(for account: Account, completion: @escaping (() -> Void)) {
os_log(.debug, log: log, "Refreshing articles...")
}
func refreshMissingArticles(for account: Account, completion: @escaping (() -> Void)) {
os_log(.debug, log: log, "Refreshing missing articles...")
completion()
}
func sendArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
os_log(.debug, log: log, "Sending article status...")
completion()
}
func refreshArticleStatus(for account: Account, completion: @escaping (() -> Void)) {
os_log(.debug, log: log, "Refreshing article status...")
completion()
}
func importOPML(for account: Account, opmlFile: URL, completion: @escaping (Result<Void, Error>) -> Void) {
fatalError()
}
func addFolder(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, completion: @escaping (Result<Feed, Error>) -> Void) {
fatalError()
}
func renameFeed(for account: Account, with feed: Feed, to name: String, completion: @escaping (Result<Void, Error>) -> Void) {
fatalError()
}
func addFeed(for account: Account, with: Feed, to container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
fatalError()
}
func removeFeed(for account: Account, with feed: Feed, from container: Container, completion: @escaping (Result<Void, Error>) -> Void) {
fatalError()
}
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) {
fatalError()
}
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) -> Set<Article>? {
fatalError()
}
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: Private
private extension FeedWranglerAccountDelegate {
}

View File

@ -0,0 +1,17 @@
//
// FeedWranglerConfig.swift
// NetNewsWire
//
// Created by Jonathan Bennett on 9/27/19.
// Copyright © 2019 Ranchero Software, LLC. All rights reserved.
//
import Foundation
enum FeedWranglerConfig {
static let clientKey = "{FEEDWRANGLERKEY}"
static let clientPath = "https://feedwrangler.net/api/v2/"
static let clientURL = {
URL(string: FeedWranglerConfig.clientPath)!
}()
}

View File

@ -38,6 +38,10 @@ struct AppAssets {
return RSImage(named: "accountFeedly")
}()
static var accountFeedWrangler: RSImage! = {
return RSImage(named: "accountFeedWrangler")
}()
static var accountFreshRSS: RSImage! = {
return RSImage(named: "accountFreshRSS")
}()

View File

@ -15,7 +15,7 @@ class AccountsAddViewController: NSViewController {
private var accountsAddWindowController: NSWindowController?
private let addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .freshRSS]
private let addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .feedWrangler, .freshRSS]
init() {
super.init(nibName: "AccountsAdd", bundle: nil)
@ -65,6 +65,9 @@ extension AccountsAddViewController: NSTableViewDelegate {
case .feedbin:
cell.accountNameLabel?.stringValue = NSLocalizedString("Feedbin", comment: "Feedbin")
cell.accountImageView?.image = AppAssets.accountFeedbin
case .feedWrangler:
cell.accountNameLabel?.stringValue = NSLocalizedString("Feed Wrangler", comment: "Feed Wrangler")
cell.accountImageView?.image = AppAssets.accountFeedWrangler
case .freshRSS:
cell.accountNameLabel?.stringValue = NSLocalizedString("FreshRSS", comment: "FreshRSS")
cell.accountImageView?.image = AppAssets.accountFreshRSS
@ -95,6 +98,10 @@ extension AccountsAddViewController: NSTableViewDelegate {
let accountsFeedbinWindowController = AccountsFeedbinWindowController()
accountsFeedbinWindowController.runSheetOnWindow(self.view.window!)
accountsAddWindowController = accountsFeedbinWindowController
case .feedWrangler:
let accountsFeedWranglerWindowController = AccountsFeedWranglerWindowController()
accountsFeedWranglerWindowController.runSheetOnWindow(self.view.window!)
accountsAddWindowController = accountsFeedWranglerWindowController
case .freshRSS:
let accountsReaderAPIWindowController = AccountsReaderAPIWindowController()
accountsReaderAPIWindowController.accountType = .freshRSS

View File

@ -0,0 +1,185 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="14490.70" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES" customObjectInstantitationMethod="direct">
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="14490.70"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="AccountsFeedWranglerWindowController" customModuleProvider="target">
<connections>
<outlet property="actionButton" destination="9mz-D9-krh" id="ozu-6Q-9Lb"/>
<outlet property="errorMessageLabel" destination="byK-Sd-r7F" id="8zt-9d-dWQ"/>
<outlet property="passwordTextField" destination="JSa-LY-zNQ" id="5cF-bM-CJE"/>
<outlet property="progressIndicator" destination="B0W-bh-Evv" id="Tiq-gx-s3F"/>
<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="3840" height="2137"/>
<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="91" y="190" width="251" height="39"/>
<subviews>
<imageView horizontalHuggingPriority="251" verticalHuggingPriority="251" translatesAutoresizingMaskIntoConstraints="NO" id="Ssh-Dh-xbg">
<rect key="frame" x="0.0" y="0.0" width="36" height="36"/>
<constraints>
<constraint firstAttribute="height" constant="36" id="Ern-Kk-8LX"/>
<constraint firstAttribute="width" constant="36" id="PLS-68-NMc"/>
</constraints>
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" image="accountFeedWrangler" id="y38-YL-woC"/>
</imageView>
<textField horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="lti-yM-8LV">
<rect key="frame" x="53" y="0.0" width="200" height="39"/>
<textFieldCell key="cell" lineBreakMode="clipping" title="Feed Wrangler" id="ras-dj-nP8">
<font key="font" metaFont="system" size="32"/>
<color key="textColor" name="labelColor" catalog="System" colorSpace="catalog"/>
<color key="backgroundColor" name="textBackgroundColor" catalog="System" colorSpace="catalog"/>
</textFieldCell>
</textField>
</subviews>
<visibilityPriorities>
<integer value="1000"/>
<integer value="1000"/>
</visibilityPriorities>
<customSpacing>
<real value="3.4028234663852886e+38"/>
<real value="3.4028234663852886e+38"/>
</customSpacing>
</stackView>
<gridView xPlacement="trailing" yPlacement="center" rowAlignment="none" rowSpacing="12" columnSpacing="14" translatesAutoresizingMaskIntoConstraints="NO" id="zBB-JH-huI">
<rect key="frame" x="79" y="61" width="276" height="97"/>
<rows>
<gridRow id="DRl-lC-vUc"/>
<gridRow id="eW8-uH-txq"/>
<gridRow id="DbI-7g-Xme"/>
</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="78" width="41" height="17"/>
<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="76" y="75" width="200" height="22"/>
<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" 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="44" width="66" height="17"/>
<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="76" y="41" width="200" height="22"/>
<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" 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>
<gridCell row="DbI-7g-Xme" column="fCQ-jY-Mts" headOfMergedCell="xX0-vn-AId" xPlacement="leading" id="xX0-vn-AId">
<textField key="contentView" horizontalHuggingPriority="251" verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="byK-Sd-r7F">
<rect key="frame" x="-2" y="6" width="104" height="17"/>
<textFieldCell key="cell" lineBreakMode="clipping" id="0yh-Ab-UTX">
<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="DbI-7g-Xme" column="7CY-bX-6x4" headOfMergedCell="xX0-vn-AId" id="hk5-St-E4y"/>
</gridCells>
</gridView>
<button verticalHuggingPriority="750" translatesAutoresizingMaskIntoConstraints="NO" id="9mz-D9-krh">
<rect key="frame" x="340" y="13" width="79" height="32"/>
<buttonCell key="cell" type="push" title="Action" 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="258" 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>
<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="209" y="166" width="16" height="16"/>
</progressIndicator>
</subviews>
<constraints>
<constraint firstItem="9mz-D9-krh" firstAttribute="leading" secondItem="XAM-Hb-0Hw" secondAttribute="trailing" constant="12" symbolic="YES" id="CC8-HR-FDy"/>
<constraint firstItem="XAM-Hb-0Hw" firstAttribute="centerY" secondItem="9mz-D9-krh" secondAttribute="centerY" id="M2M-fb-kfR"/>
<constraint firstAttribute="bottom" secondItem="9mz-D9-krh" secondAttribute="bottom" constant="20" id="PK2-Ye-400"/>
<constraint firstItem="zBB-JH-huI" firstAttribute="top" secondItem="B0W-bh-Evv" secondAttribute="bottom" constant="8" id="V7z-a7-OOG"/>
<constraint firstItem="9mz-D9-krh" firstAttribute="top" secondItem="zBB-JH-huI" secondAttribute="bottom" constant="20" symbolic="YES" id="Wu3-hp-Vzh"/>
<constraint firstItem="zBB-JH-huI" firstAttribute="centerX" secondItem="se5-gp-TjO" secondAttribute="centerX" id="aFI-4s-mMv"/>
<constraint firstAttribute="trailing" secondItem="9mz-D9-krh" secondAttribute="trailing" constant="20" id="fVQ-zN-rKd"/>
<constraint firstItem="B0W-bh-Evv" firstAttribute="top" secondItem="lti-yM-8LV" secondAttribute="bottom" constant="8" id="gq2-tB-pXH"/>
<constraint firstItem="7Ht-Fn-0Ya" firstAttribute="top" secondItem="se5-gp-TjO" secondAttribute="top" constant="20" id="jlY-Jg-KJR"/>
<constraint firstItem="B0W-bh-Evv" firstAttribute="centerX" secondItem="se5-gp-TjO" secondAttribute="centerX" id="lrN-Gd-iXd"/>
<constraint firstItem="7Ht-Fn-0Ya" firstAttribute="centerX" secondItem="se5-gp-TjO" secondAttribute="centerX" id="tAZ-Te-w3H"/>
</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="144" height="144"/>
</resources>
</document>

View File

@ -0,0 +1,110 @@
//
// AccountsFeedWranglerWindowController.swift
// NetNewsWire
//
// Created by Jonathan Bennett on 2019-08-29.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import AppKit
import Account
import RSWeb
class AccountsFeedWranglerWindowController: NSWindowController {
@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")
} else {
actionButton.title = NSLocalizedString("Create", comment: "Create")
}
}
// MARK: API
func runSheetOnWindow(_ hostWindow: NSWindow, completionHandler handler: ((NSApplication.ModalResponse) -> Void)? = nil) {
self.hostWindow = hostWindow
hostWindow.beginSheet(window!, completionHandler: handler)
}
// 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
}
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
}
var newAccount = false
if self.account == nil {
self.account = AccountManager.shared.createAccount(type: .feedWrangler)
newAccount = true
}
do {
try self.account?.removeCredentials(type: .feedWranglerBasic)
try self.account?.removeCredentials(type: .feedWranglerToken)
try self.account?.storeCredentials(credentials)
try self.account?.storeCredentials(validatedCredentials)
if newAccount {
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")
}
}
}
}

View File

@ -0,0 +1,15 @@
{
"images" : [
{
"idiom" : "universal",
"filename" : "accountFreshRSS.pdf"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
},
"properties" : {
"template-rendering-intent" : "template"
}
}

View File

@ -7,6 +7,8 @@
objects = {
/* Begin PBXBuildFile section */
3B29BC69233EA81F002A346D /* AccountsFeedWranglerWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B29BC5F233EA81F002A346D /* AccountsFeedWranglerWindowController.swift */; };
3B29BC6A233EA81F002A346D /* AccountsFeedWrangler.xib in Resources */ = {isa = PBXBuildFile; fileRef = 3B29BC68233EA81F002A346D /* AccountsFeedWrangler.xib */; };
49F40DF82335B71000552BF4 /* newsfoot.js in Resources */ = {isa = PBXBuildFile; fileRef = 49F40DEF2335B71000552BF4 /* newsfoot.js */; };
49F40DF92335B71000552BF4 /* newsfoot.js in Resources */ = {isa = PBXBuildFile; fileRef = 49F40DEF2335B71000552BF4 /* newsfoot.js */; };
510BD15D232D765D002692E4 /* SettingsReaderAPIAccountView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 557EE1A522B6F4E1004206FA /* SettingsReaderAPIAccountView.swift */; };
@ -777,6 +779,8 @@
/* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */
3B29BC5F233EA81F002A346D /* AccountsFeedWranglerWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsFeedWranglerWindowController.swift; sourceTree = "<group>"; };
3B29BC68233EA81F002A346D /* AccountsFeedWrangler.xib */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = file.xib; path = AccountsFeedWrangler.xib; sourceTree = "<group>"; };
49F40DEF2335B71000552BF4 /* newsfoot.js */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.javascript; path = newsfoot.js; sourceTree = "<group>"; };
510D707322B028E1004E8F65 /* SettingsAddAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsAddAccountView.swift; sourceTree = "<group>"; };
510D707D22B02A4B004E8F65 /* SettingsLocalAccountView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsLocalAccountView.swift; sourceTree = "<group>"; };
@ -1930,6 +1934,8 @@
5144EA4F227B8E4500D19003 /* AccountsFeedbinWindowController.swift */,
9EA33BB82318F8C10097B644 /* AccountsFeedlyWeb.xib */,
9EA33BB72318F8C10097B644 /* AccountsFeedlyWebWindowController.swift */,
3B29BC68233EA81F002A346D /* AccountsFeedWrangler.xib */,
3B29BC5F233EA81F002A346D /* AccountsFeedWranglerWindowController.swift */,
55E15BC1229D65A900D6602A /* AccountsReaderAPI.xib */,
55E15BCA229D65A900D6602A /* AccountsReaderAPIWindowController.swift */,
5144EA352279FC3D00D19003 /* AccountsAddLocal.xib */,
@ -2266,16 +2272,16 @@
TargetAttributes = {
513C5CE5232571C2003D4054 = {
CreatedOnToolsVersion = 11.0;
DevelopmentTeam = SHJK2V3AJG;
DevelopmentTeam = 8EQFQ9RY84;
ProvisioningStyle = Automatic;
};
6581C73220CED60000F4AD34 = {
DevelopmentTeam = SHJK2V3AJG;
DevelopmentTeam = 8EQFQ9RY84;
ProvisioningStyle = Automatic;
};
840D617B2029031C009BC708 = {
CreatedOnToolsVersion = 9.3;
DevelopmentTeam = SHJK2V3AJG;
DevelopmentTeam = 8EQFQ9RY84;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.BackgroundModes = {
@ -2285,7 +2291,7 @@
};
849C645F1ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = SHJK2V3AJG;
DevelopmentTeam = 8EQFQ9RY84;
ProvisioningStyle = Automatic;
SystemCapabilities = {
com.apple.HardenedRuntime = {
@ -2295,7 +2301,7 @@
};
849C64701ED37A5D003D8FC0 = {
CreatedOnToolsVersion = 8.2.1;
DevelopmentTeam = SHJK2V3AJG;
DevelopmentTeam = 8EQFQ9RY84;
ProvisioningStyle = Automatic;
TestTargetID = 849C645F1ED37A5D003D8FC0;
};
@ -2588,6 +2594,7 @@
84C9FC8E22629E8F00D921D6 /* Credits.rtf in Resources */,
84BBB12D20142A4700F054F5 /* Inspector.storyboard in Resources */,
848363022262A3BD00DA1D35 /* AddFeedSheet.xib in Resources */,
3B29BC6A233EA81F002A346D /* AccountsFeedWrangler.xib in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@ -2897,6 +2904,7 @@
847CD6CA232F4CBF00FAC46D /* TimelineAvatarView.swift in Sources */,
51FA73AA2332C2FD0090D516 /* ArticleExtractorConfig.swift in Sources */,
84BBB12E20142A4700F054F5 /* InspectorWindowController.swift in Sources */,
3B29BC69233EA81F002A346D /* AccountsFeedWranglerWindowController.swift in Sources */,
51EF0F7A22771B890050506E /* ColorHash.swift in Sources */,
84E46C7D1F75EF7B005ECFB3 /* AppDefaults.swift in Sources */,
D5907D972004B7EB005947E5 /* Account+Scriptability.swift in Sources */,