From c25e42fc7ec8938ecdaf22f6ba8b2009e84e6910 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Mon, 26 Oct 2020 09:03:32 +0800 Subject: [PATCH] additional iOS account types --- NetNewsWire.xcodeproj/project.pbxproj | 4 + iOS/Account/Account.storyboard | 191 +++++++++++- .../ReaderAPIAccountViewController.swift | 274 ++++++++++++++++++ iOS/Settings/AddAccountViewController.swift | 23 +- iOS/Settings/Settings.storyboard | 30 +- 5 files changed, 493 insertions(+), 29 deletions(-) create mode 100644 iOS/Account/ReaderAPIAccountViewController.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 160d6bf8a..8934a3e7c 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -36,6 +36,7 @@ 1769E33824BD97CB000E1E8E /* AccountUpdateErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1769E33724BD97CB000E1E8E /* AccountUpdateErrors.swift */; }; 1776E88E24AC5F8A00E78166 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1776E88D24AC5F8A00E78166 /* AppDefaults.swift */; }; 1776E88F24AC5F8A00E78166 /* AppDefaults.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1776E88D24AC5F8A00E78166 /* AppDefaults.swift */; }; + 177A0C2D25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 177A0C2C25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift */; }; 17897ACA24C281A40014BA03 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17897AC924C281A40014BA03 /* InspectorView.swift */; }; 17897ACB24C281A40014BA03 /* InspectorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17897AC924C281A40014BA03 /* InspectorView.swift */; }; 17930ED424AF10EE00A9BA52 /* AddWebFeedView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17930ED324AF10EE00A9BA52 /* AddWebFeedView.swift */; }; @@ -1462,6 +1463,7 @@ 1769E33524BD9621000E1E8E /* EditAccountCredentialsModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EditAccountCredentialsModel.swift; sourceTree = ""; }; 1769E33724BD97CB000E1E8E /* AccountUpdateErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountUpdateErrors.swift; sourceTree = ""; }; 1776E88D24AC5F8A00E78166 /* AppDefaults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDefaults.swift; sourceTree = ""; }; + 177A0C2C25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReaderAPIAccountViewController.swift; sourceTree = ""; }; 17897AC924C281A40014BA03 /* InspectorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorView.swift; sourceTree = ""; }; 17930ED324AF10EE00A9BA52 /* AddWebFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedView.swift; sourceTree = ""; }; 1799E6A824C2F93F00511E91 /* InspectorPlatformModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorPlatformModifier.swift; sourceTree = ""; }; @@ -2437,6 +2439,7 @@ 51A16996235E10D700EB091F /* FeedbinAccountViewController.swift */, 3B3A328B238B820900314204 /* FeedWranglerAccountViewController.swift */, 769F2D3643779DB02786278E /* NewsBlurAccountViewController.swift */, + 177A0C2C25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift */, ); path = Account; sourceTree = ""; @@ -5038,6 +5041,7 @@ 517A745B2443665000B553B9 /* UIPageViewController-Extensions.swift in Sources */, 51BB7C272335A8E5008E8144 /* ArticleActivityItemSource.swift in Sources */, 51F85BF52273625800C787DC /* Bundle-Extensions.swift in Sources */, + 177A0C2D25454AAB00D7EAF6 /* ReaderAPIAccountViewController.swift in Sources */, 51C452A622650A3500C03939 /* Node-Extensions.swift in Sources */, 51C45294226509C800C03939 /* SearchFeedDelegate.swift in Sources */, 5F323809231DF9F000706F6B /* VibrantTableViewCell.swift in Sources */, diff --git a/iOS/Account/Account.storyboard b/iOS/Account/Account.storyboard index b4f4a227d..57fa299dd 100644 --- a/iOS/Account/Account.storyboard +++ b/iOS/Account/Account.storyboard @@ -1,8 +1,9 @@ - + - + + @@ -77,12 +78,12 @@ - + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -629,6 +780,22 @@ + + + + + + + + + + + + + + + + diff --git a/iOS/Account/ReaderAPIAccountViewController.swift b/iOS/Account/ReaderAPIAccountViewController.swift new file mode 100644 index 000000000..e755f73cb --- /dev/null +++ b/iOS/Account/ReaderAPIAccountViewController.swift @@ -0,0 +1,274 @@ +// +// ReaderAPIAccountViewController.swift +// NetNewsWire-iOS +// +// Created by Stuart Breckenridge on 25/10/20. +// Copyright © 2020 Ranchero Software. All rights reserved. +// + +import UIKit +import Account +import Secrets +import RSWeb + +class ReaderAPIAccountViewController: UITableViewController { + + @IBOutlet weak var activityIndicator: UIActivityIndicatorView! + @IBOutlet weak var cancelBarButtonItem: UIBarButtonItem! + @IBOutlet weak var usernameTextField: UITextField! + @IBOutlet weak var passwordTextField: UITextField! + @IBOutlet weak var apiURLTextField: UITextField! + @IBOutlet weak var showHideButton: UIButton! + @IBOutlet weak var actionButton: UIButton! + + + weak var account: Account? + var accountType: AccountType? + weak var delegate: AddAccountDismissDelegate? + + override func viewDidLoad() { + super.viewDidLoad() + + activityIndicator.isHidden = true + usernameTextField.delegate = self + passwordTextField.delegate = self + + if let unwrappedAcount = account, + let credentials = try? retrieveCredentialsForAccount(for: unwrappedAcount) { + actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal) + actionButton.isEnabled = true + usernameTextField.text = credentials.username + passwordTextField.text = credentials.secret + } else { + actionButton.setTitle(NSLocalizedString("Add Account", comment: "Add Account"), for: .normal) + } + + if let unwrappedAccountType = accountType { + switch unwrappedAccountType { + case .freshRSS: + title = NSLocalizedString("FreshRSS", comment: "FreshRSS") + case .inoreader: + title = NSLocalizedString("InoReader", comment: "InoReader") + case .bazQux: + title = NSLocalizedString("BazQux", comment: "BazQux") + case .theOldReader: + title = NSLocalizedString("The Old Reader", comment: "The Old Reader") + default: + title = "" + } + } + + NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: usernameTextField) + NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField) + + tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") + + } + + 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 = headerViewImage() + return headerView + } else { + return super.tableView(tableView, viewForHeaderInSection: section) + } + } + + override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { + switch section { + case 0: + switch accountType { + case .freshRSS: + return 3 + default: + return 2 + } + default: + return 1 + } + } + + + @IBAction func cancel(_ sender: Any) { + dismiss(animated: true, completion: nil) + } + + @IBAction func showHidePassword(_ sender: Any) { + if passwordTextField.isSecureTextEntry { + passwordTextField.isSecureTextEntry = false + showHideButton.setTitle("Hide", for: .normal) + } else { + passwordTextField.isSecureTextEntry = true + showHideButton.setTitle("Show", for: .normal) + } + } + + @IBAction func action(_ sender: Any) { + + validateDataEntry() + + let username = usernameTextField.text! + let password = passwordTextField.text! + let url = apiURL()! + + startAnimatingActivityIndicator() + disableNavigation() + + // When you fill in the email address via auto-complete it adds extra whitespace + let trimmedUsername = username.trimmingCharacters(in: .whitespaces) + let credentials = Credentials(type: .readerBasic, username: trimmedUsername, secret: password) + guard let type = accountType else { + return + } + Account.validateCredentials(type: type, credentials: credentials, endpoint: url) { result in + + self.stopAnimatingActivityIndicator() + self.enableNavigation() + + switch result { + case .success(let validatedCredentials): + if let validatedCredentials = validatedCredentials { + var newAccount = false + if self.account == nil { + self.account = AccountManager.shared.createAccount(type: type) + newAccount = true + } + + do { + self.account?.endpointURL = url + + try self.account?.removeCredentials(type: .readerBasic) + try self.account?.removeCredentials(type: .readerAPIKey) + try self.account?.storeCredentials(credentials) + try self.account?.storeCredentials(validatedCredentials) + + self.dismiss(animated: true, completion: nil) + + if newAccount { + self.account?.refreshAll() { result in + switch result { + case .success: + break + case .failure(let error): + self.showError(NSLocalizedString(error.localizedDescription, comment: "Accoount Refresh Error")) + } + } + } + + self.delegate?.dismiss() + } catch { + self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")) + } + } else { + self.showError(NSLocalizedString("Invalid username/password combination.", comment: "Credentials Error")) + } + case .failure(let error): + self.showError(error.localizedDescription) + } + + } + } + + private func retrieveCredentialsForAccount(for account: Account) throws -> Credentials? { + switch accountType { + case .bazQux, .inoreader, .theOldReader, .freshRSS: + return try account.retrieveCredentials(type: .readerBasic) + default: + return nil + } + } + + private func headerViewImage() -> UIImage? { + if let accountType = accountType { + switch accountType { + case .bazQux: + return AppAssets.accountBazQuxImage + case .inoreader: + return AppAssets.accountInoreaderImage + case .theOldReader: + return AppAssets.accountTheOldReaderImage + case .freshRSS: + return AppAssets.accountFreshRSSImage + default: + return nil + } + } + return nil + } + + private func validateDataEntry() { + switch accountType { + case .freshRSS: + if !usernameTextField.hasText || !passwordTextField.hasText || !apiURLTextField.hasText { + showError(NSLocalizedString("Username, password, and API URL are required.", comment: "Credentials Error")) + return + } + guard let _ = URL(string: apiURLTextField.text!) else { + showError(NSLocalizedString("Invalid API URL.", comment: "Invalid API URL")) + return + } + default: + if !usernameTextField.hasText || !passwordTextField.hasText { + showError(NSLocalizedString("Username and password are required.", comment: "Credentials Error")) + return + } + } + } + + private func apiURL() -> URL? { + switch accountType { + case .freshRSS: + return URL(string: apiURLTextField.text!)! + case .inoreader: + return URL(string: ReaderAPIVariant.inoreader.host)! + case .bazQux: + return URL(string: ReaderAPIVariant.bazQux.host)! + case .theOldReader: + return URL(string: ReaderAPIVariant.theOldReader.host)! + default: + return nil + } + } + + @objc func textDidChange(_ note: Notification) { + actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false) + } + + private func showError(_ message: String) { + presentError(title: "Error", message: message) + } + + private func enableNavigation() { + self.cancelBarButtonItem.isEnabled = true + self.actionButton.isEnabled = true + } + + private func disableNavigation() { + cancelBarButtonItem.isEnabled = false + actionButton.isEnabled = false + } + + private func startAnimatingActivityIndicator() { + activityIndicator.isHidden = false + activityIndicator.startAnimating() + } + + private func stopAnimatingActivityIndicator() { + self.activityIndicator.isHidden = true + self.activityIndicator.stopAnimating() + } +} + +extension ReaderAPIAccountViewController: UITextFieldDelegate { + + func textFieldShouldReturn(_ textField: UITextField) -> Bool { + textField.resignFirstResponder() + return true + } + +} diff --git a/iOS/Settings/AddAccountViewController.swift b/iOS/Settings/AddAccountViewController.swift index 5ca7017b7..baf38ab79 100644 --- a/iOS/Settings/AddAccountViewController.swift +++ b/iOS/Settings/AddAccountViewController.swift @@ -17,9 +17,9 @@ protocol AddAccountDismissDelegate: UIViewController { class AddAccountViewController: UITableViewController, AddAccountDismissDelegate { #if DEBUG - private var addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .feedWrangler, .cloudKit, .newsBlur] + private var addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .feedWrangler, .cloudKit, .newsBlur, .bazQux, .theOldReader, .freshRSS, .inoreader] #else - private var addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .cloudKit, .newsBlur] + private var addableAccountTypes: [AccountType] = [.onMyMac, .feedbin, .feedly, .cloudKit, .newsBlur, .bazQux, .theOldReader, .freshRSS, .inoreader] #endif override func viewDidLoad() { @@ -61,6 +61,18 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate case .newsBlur: cell.comboNameLabel?.text = NSLocalizedString("NewsBlur", comment: "NewsBlur") cell.comboImage?.image = AppAssets.accountNewsBlurImage + case .bazQux: + cell.comboNameLabel?.text = NSLocalizedString("BazQux", comment: "BazQux") + cell.comboImage?.image = AppAssets.accountBazQuxImage + case .theOldReader: + cell.comboNameLabel?.text = NSLocalizedString("The Old Reader", comment: "The Old Reader") + cell.comboImage?.image = AppAssets.accountTheOldReaderImage + case .freshRSS: + cell.comboNameLabel?.text = NSLocalizedString("FreshRSS", comment: "FreshRSS") + cell.comboImage?.image = AppAssets.accountFreshRSSImage + case .inoreader: + cell.comboNameLabel?.text = NSLocalizedString("Inoreader", comment: "Inoreader") + cell.comboImage?.image = AppAssets.accountInoreaderImage default: break } @@ -105,6 +117,13 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate let addViewController = navController.topViewController as! NewsBlurAccountViewController addViewController.delegate = self present(navController, animated: true) + case .bazQux, .inoreader, .freshRSS, .theOldReader: + let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "ReaderAPIAccountNavigationViewController") as! UINavigationController + navController.modalPresentationStyle = .currentContext + let addViewController = navController.topViewController as! ReaderAPIAccountViewController + addViewController.accountType = addableAccountTypes[indexPath.row] + addViewController.delegate = self + present(navController, animated: true) default: break } diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard index 6dd97105e..ccebe7898 100644 --- a/iOS/Settings/Settings.storyboard +++ b/iOS/Settings/Settings.storyboard @@ -1,9 +1,9 @@ - + - + @@ -212,7 +212,7 @@