Fix lint issues.

This commit is contained in:
Brent Simmons 2025-01-22 22:18:09 -08:00
parent 40ada2ba5a
commit bbef99f2d3
92 changed files with 1651 additions and 1694 deletions

View File

@ -12,7 +12,7 @@ import Account
enum CloudKitAccountViewControllerError: LocalizedError { enum CloudKitAccountViewControllerError: LocalizedError {
case iCloudDriveMissing case iCloudDriveMissing
var errorDescription: String? { var errorDescription: String? {
return NSLocalizedString("Unable to add iCloud Account. Please make sure you have iCloud and iCloud Drive enabled in System Settings.", comment: "Unable to add iCloud Account.") return NSLocalizedString("Unable to add iCloud Account. Please make sure you have iCloud and iCloud Drive enabled in System Settings.", comment: "Unable to add iCloud Account.")
} }
@ -22,14 +22,14 @@ class CloudKitAccountViewController: UITableViewController {
weak var delegate: AddAccountDismissDelegate? weak var delegate: AddAccountDismissDelegate?
@IBOutlet weak var footerLabel: UILabel! @IBOutlet weak var footerLabel: UILabel!
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setupFooter() setupFooter()
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
} }
private func setupFooter() { private func setupFooter() {
footerLabel.text = NSLocalizedString("NetNewsWire will use your iCloud account to sync your subscriptions across your Mac and iOS devices.", comment: "iCloud") footerLabel.text = NSLocalizedString("NetNewsWire will use your iCloud account to sync your subscriptions across your Mac and iOS devices.", comment: "iCloud")
} }
@ -38,22 +38,22 @@ class CloudKitAccountViewController: UITableViewController {
dismiss(animated: true, completion: nil) dismiss(animated: true, completion: nil)
delegate?.dismiss() delegate?.dismiss()
} }
@IBAction func add(_ sender: Any) { @IBAction func add(_ sender: Any) {
guard FileManager.default.ubiquityIdentityToken != nil else { guard FileManager.default.ubiquityIdentityToken != nil else {
presentError(CloudKitAccountViewControllerError.iCloudDriveMissing) presentError(CloudKitAccountViewControllerError.iCloudDriveMissing)
return return
} }
let _ = AccountManager.shared.createAccount(type: .cloudKit) _ = AccountManager.shared.createAccount(type: .cloudKit)
dismiss(animated: true, completion: nil) dismiss(animated: true, completion: nil)
delegate?.dismiss() delegate?.dismiss()
} }
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
} }
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 { if section == 0 {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView

View File

@ -32,7 +32,7 @@ class FeedbinAccountViewController: UITableViewController {
activityIndicator.isHidden = true activityIndicator.isHidden = true
emailTextField.delegate = self emailTextField.delegate = self
passwordTextField.delegate = self passwordTextField.delegate = self
if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) { if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) {
actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal) actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal)
actionButton.isEnabled = true actionButton.isEnabled = true
@ -47,7 +47,7 @@ class FeedbinAccountViewController: UITableViewController {
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
} }
private func setupFooter() { private func setupFooter() {
footerLabel.text = NSLocalizedString("Sign in to your Feedbin account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have a Feedbin account?", comment: "Feedbin") footerLabel.text = NSLocalizedString("Sign in to your Feedbin account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have a Feedbin account?", comment: "Feedbin")
} }
@ -55,7 +55,7 @@ class FeedbinAccountViewController: UITableViewController {
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
} }
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 { if section == 0 {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
@ -69,7 +69,7 @@ class FeedbinAccountViewController: UITableViewController {
@IBAction func cancel(_ sender: Any) { @IBAction func cancel(_ sender: Any) {
dismiss(animated: true, completion: nil) dismiss(animated: true, completion: nil)
} }
@IBAction func showHidePassword(_ sender: Any) { @IBAction func showHidePassword(_ sender: Any) {
if passwordTextField.isSecureTextEntry { if passwordTextField.isSecureTextEntry {
passwordTextField.isSecureTextEntry = false passwordTextField.isSecureTextEntry = false
@ -79,21 +79,21 @@ class FeedbinAccountViewController: UITableViewController {
showHideButton.setTitle("Show", for: .normal) showHideButton.setTitle("Show", for: .normal)
} }
} }
@IBAction func action(_ sender: Any) { @IBAction func action(_ sender: Any) {
guard let email = emailTextField.text, let password = passwordTextField.text else { guard let email = emailTextField.text, let password = passwordTextField.text else {
showError(NSLocalizedString("Username & password required.", comment: "Credentials Error")) showError(NSLocalizedString("Username & password required.", comment: "Credentials Error"))
return return
} }
// When you fill in the email address via auto-complete it adds extra whitespace // When you fill in the email address via auto-complete it adds extra whitespace
let trimmedEmail = email.trimmingCharacters(in: .whitespaces) let trimmedEmail = email.trimmingCharacters(in: .whitespaces)
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedbin, username: trimmedEmail) else { guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedbin, username: trimmedEmail) else {
showError(NSLocalizedString("There is already a Feedbin account with that username created.", comment: "Duplicate Error")) showError(NSLocalizedString("There is already a Feedbin account with that username created.", comment: "Duplicate Error"))
return return
} }
resignFirstResponder() resignFirstResponder()
toggleActivityIndicatorAnimation(visible: true) toggleActivityIndicatorAnimation(visible: true)
setNavigationEnabled(to: false) setNavigationEnabled(to: false)
@ -102,22 +102,22 @@ class FeedbinAccountViewController: UITableViewController {
Account.validateCredentials(type: .feedbin, credentials: credentials) { result in Account.validateCredentials(type: .feedbin, credentials: credentials) { result in
self.toggleActivityIndicatorAnimation(visible: false) self.toggleActivityIndicatorAnimation(visible: false)
self.setNavigationEnabled(to: true) self.setNavigationEnabled(to: true)
switch result { switch result {
case .success(let credentials): case .success(let credentials):
if let credentials = credentials { if let credentials = credentials {
if self.account == nil { if self.account == nil {
self.account = AccountManager.shared.createAccount(type: .feedbin) self.account = AccountManager.shared.createAccount(type: .feedbin)
} }
do { do {
do { do {
try self.account?.removeCredentials(type: .basic) try self.account?.removeCredentials(type: .basic)
} catch {} } catch {}
try self.account?.storeCredentials(credentials) try self.account?.storeCredentials(credentials)
self.account?.refreshAll() { result in self.account?.refreshAll { result in
switch result { switch result {
case .success: case .success:
break break
@ -125,7 +125,7 @@ class FeedbinAccountViewController: UITableViewController {
self.presentError(error) self.presentError(error)
} }
} }
self.dismiss(animated: true, completion: nil) self.dismiss(animated: true, completion: nil)
self.delegate?.dismiss() self.delegate?.dismiss()
} catch { } catch {
@ -137,32 +137,31 @@ class FeedbinAccountViewController: UITableViewController {
case .failure: case .failure:
self.showError(NSLocalizedString("Network error. Try again later.", comment: "Credentials Error")) self.showError(NSLocalizedString("Network error. Try again later.", comment: "Credentials Error"))
} }
} }
} }
@IBAction func signUpWithProvider(_ sender: Any) { @IBAction func signUpWithProvider(_ sender: Any) {
let url = URL(string: "https://feedbin.com/signup")! let url = URL(string: "https://feedbin.com/signup")!
let safari = SFSafariViewController(url: url) let safari = SFSafariViewController(url: url)
safari.modalPresentationStyle = .currentContext safari.modalPresentationStyle = .currentContext
self.present(safari, animated: true, completion: nil) self.present(safari, animated: true, completion: nil)
} }
@objc func textDidChange(_ note: Notification) { @objc func textDidChange(_ note: Notification) {
actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false) actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false)
} }
private func showError(_ message: String) { private func showError(_ message: String) {
presentError(title: NSLocalizedString("Error", comment: "Credentials Error"), message: message) presentError(title: NSLocalizedString("Error", comment: "Credentials Error"), message: message)
} }
private func setNavigationEnabled(to value:Bool){ private func setNavigationEnabled(to value: Bool) {
cancelBarButtonItem.isEnabled = value cancelBarButtonItem.isEnabled = value
actionButton.isEnabled = value actionButton.isEnabled = value
} }
private func toggleActivityIndicatorAnimation(visible value: Bool){ private func toggleActivityIndicatorAnimation(visible value: Bool) {
activityIndicator.isHidden = !value activityIndicator.isHidden = !value
if value { if value {
activityIndicator.startAnimating() activityIndicator.startAnimating()
@ -170,11 +169,11 @@ class FeedbinAccountViewController: UITableViewController {
activityIndicator.stopAnimating() activityIndicator.stopAnimating()
} }
} }
} }
extension FeedbinAccountViewController: UITextFieldDelegate { extension FeedbinAccountViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool { func textFieldShouldReturn(_ textField: UITextField) -> Bool {
if textField == emailTextField { if textField == emailTextField {
passwordTextField.becomeFirstResponder() passwordTextField.becomeFirstResponder()
@ -184,5 +183,5 @@ extension FeedbinAccountViewController: UITextFieldDelegate {
} }
return true return true
} }
} }

View File

@ -13,7 +13,7 @@ class LocalAccountViewController: UITableViewController {
@IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var footerLabel: UILabel! @IBOutlet weak var footerLabel: UILabel!
weak var delegate: AddAccountDismissDelegate? weak var delegate: AddAccountDismissDelegate?
override func viewDidLoad() { override func viewDidLoad() {
@ -23,7 +23,7 @@ class LocalAccountViewController: UITableViewController {
nameTextField.delegate = self nameTextField.delegate = self
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
} }
private func setupFooter() { private func setupFooter() {
footerLabel.text = NSLocalizedString("Local accounts do not sync your feeds across devices.", comment: "Local") footerLabel.text = NSLocalizedString("Local accounts do not sync your feeds across devices.", comment: "Local")
} }
@ -31,18 +31,18 @@ class LocalAccountViewController: UITableViewController {
@IBAction func cancel(_ sender: Any) { @IBAction func cancel(_ sender: Any) {
dismiss(animated: true, completion: nil) dismiss(animated: true, completion: nil)
} }
@IBAction func add(_ sender: Any) { @IBAction func add(_ sender: Any) {
let account = AccountManager.shared.createAccount(type: .onMyMac) let account = AccountManager.shared.createAccount(type: .onMyMac)
account.name = nameTextField.text account.name = nameTextField.text
dismiss(animated: true, completion: nil) dismiss(animated: true, completion: nil)
delegate?.dismiss() delegate?.dismiss()
} }
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
} }
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 { if section == 0 {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
@ -52,14 +52,14 @@ class LocalAccountViewController: UITableViewController {
return super.tableView(tableView, viewForHeaderInSection: section) return super.tableView(tableView, viewForHeaderInSection: section)
} }
} }
} }
extension LocalAccountViewController: UITextFieldDelegate { extension LocalAccountViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool { func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder() textField.resignFirstResponder()
return true return true
} }
} }

View File

@ -46,7 +46,7 @@ class NewsBlurAccountViewController: UITableViewController {
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
} }
private func setupFooter() { private func setupFooter() {
footerLabel.text = NSLocalizedString("Sign in to your NewsBlur account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have a NewsBlur account?", comment: "NewsBlur") footerLabel.text = NSLocalizedString("Sign in to your NewsBlur account and sync your feeds across your devices. Your username and password will be encrypted and stored in Keychain.\n\nDont have a NewsBlur account?", comment: "NewsBlur")
} }
@ -93,7 +93,7 @@ class NewsBlurAccountViewController: UITableViewController {
showError(NSLocalizedString("There is already a NewsBlur account with that username created.", comment: "Duplicate Error")) showError(NSLocalizedString("There is already a NewsBlur account with that username created.", comment: "Duplicate Error"))
return return
} }
let password = passwordTextField.text ?? "" let password = passwordTextField.text ?? ""
startAnimatingActivityIndicator() startAnimatingActivityIndicator()
@ -122,7 +122,7 @@ class NewsBlurAccountViewController: UITableViewController {
try self.account?.storeCredentials(basicCredentials) try self.account?.storeCredentials(basicCredentials)
try self.account?.storeCredentials(sessionCredentials) try self.account?.storeCredentials(sessionCredentials)
self.account?.refreshAll() { result in self.account?.refreshAll { result in
switch result { switch result {
case .success: case .success:
break break
@ -145,7 +145,7 @@ class NewsBlurAccountViewController: UITableViewController {
} }
} }
@IBAction func signUpWithProvider(_ sender: Any) { @IBAction func signUpWithProvider(_ sender: Any) {
let url = URL(string: "https://newsblur.com")! let url = URL(string: "https://newsblur.com")!
let safari = SFSafariViewController(url: url) let safari = SFSafariViewController(url: url)

View File

@ -27,7 +27,7 @@ class ReaderAPIAccountViewController: UITableViewController {
weak var account: Account? weak var account: Account?
var accountType: AccountType? var accountType: AccountType?
weak var delegate: AddAccountDismissDelegate? weak var delegate: AddAccountDismissDelegate?
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
setupFooter() setupFooter()
@ -35,7 +35,7 @@ class ReaderAPIAccountViewController: UITableViewController {
activityIndicator.isHidden = true activityIndicator.isHidden = true
usernameTextField.delegate = self usernameTextField.delegate = self
passwordTextField.delegate = self passwordTextField.delegate = self
if let unwrappedAccount = account, if let unwrappedAccount = account,
let credentials = try? retrieveCredentialsForAccount(for: unwrappedAccount) { let credentials = try? retrieveCredentialsForAccount(for: unwrappedAccount) {
actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal) actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal)
@ -45,7 +45,7 @@ class ReaderAPIAccountViewController: UITableViewController {
} else { } else {
actionButton.setTitle(NSLocalizedString("Add Account", comment: "Add Account"), for: .normal) actionButton.setTitle(NSLocalizedString("Add Account", comment: "Add Account"), for: .normal)
} }
if let unwrappedAccountType = accountType { if let unwrappedAccountType = accountType {
switch unwrappedAccountType { switch unwrappedAccountType {
case .freshRSS: case .freshRSS:
@ -61,14 +61,14 @@ class ReaderAPIAccountViewController: UITableViewController {
title = "" title = ""
} }
} }
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: usernameTextField) NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: usernameTextField)
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField) NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField)
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
} }
private func setupFooter() { private func setupFooter() {
switch accountType { switch accountType {
case .bazQux: case .bazQux:
@ -87,11 +87,11 @@ class ReaderAPIAccountViewController: UITableViewController {
return return
} }
} }
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
} }
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if section == 0 { if section == 0 {
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
@ -101,7 +101,7 @@ class ReaderAPIAccountViewController: UITableViewController {
return super.tableView(tableView, viewForHeaderInSection: section) return super.tableView(tableView, viewForHeaderInSection: section)
} }
} }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section { switch section {
case 0: case 0:
@ -115,8 +115,7 @@ class ReaderAPIAccountViewController: UITableViewController {
return 1 return 1
} }
} }
@IBAction func cancel(_ sender: Any) { @IBAction func cancel(_ sender: Any) {
dismiss(animated: true, completion: nil) dismiss(animated: true, completion: nil)
} }
@ -130,19 +129,19 @@ class ReaderAPIAccountViewController: UITableViewController {
showHideButton.setTitle("Show", for: .normal) showHideButton.setTitle("Show", for: .normal)
} }
} }
@IBAction func action(_ sender: Any) { @IBAction func action(_ sender: Any) {
guard validateDataEntry(), let type = accountType else { guard validateDataEntry(), let type = accountType else {
return return
} }
let username = usernameTextField.text! let username = usernameTextField.text!
let password = passwordTextField.text! let password = passwordTextField.text!
let url = apiURL()! let url = apiURL()!
// When you fill in the email address via auto-complete it adds extra whitespace // When you fill in the email address via auto-complete it adds extra whitespace
let trimmedUsername = username.trimmingCharacters(in: .whitespaces) let trimmedUsername = username.trimmingCharacters(in: .whitespaces)
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: type, username: trimmedUsername) else { guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: type, username: trimmedUsername) else {
showError(NSLocalizedString("There is already an account of that type with that username created.", comment: "Duplicate Error")) showError(NSLocalizedString("There is already an account of that type with that username created.", comment: "Duplicate Error"))
return return
@ -167,15 +166,15 @@ class ReaderAPIAccountViewController: UITableViewController {
do { do {
self.account?.endpointURL = url self.account?.endpointURL = url
try? self.account?.removeCredentials(type: .readerBasic) try? self.account?.removeCredentials(type: .readerBasic)
try? self.account?.removeCredentials(type: .readerAPIKey) try? self.account?.removeCredentials(type: .readerAPIKey)
try self.account?.storeCredentials(credentials) try self.account?.storeCredentials(credentials)
try self.account?.storeCredentials(validatedCredentials) try self.account?.storeCredentials(validatedCredentials)
self.dismiss(animated: true, completion: nil) self.dismiss(animated: true, completion: nil)
self.account?.refreshAll() { result in self.account?.refreshAll { result in
switch result { switch result {
case .success: case .success:
break break
@ -183,7 +182,7 @@ class ReaderAPIAccountViewController: UITableViewController {
self.showError(NSLocalizedString(error.localizedDescription, comment: "Account Refresh Error")) self.showError(NSLocalizedString(error.localizedDescription, comment: "Account Refresh Error"))
} }
} }
self.delegate?.dismiss() self.delegate?.dismiss()
} catch { } catch {
self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error")) self.showError(NSLocalizedString("Keychain error while storing credentials.", comment: "Credentials Error"))
@ -197,7 +196,7 @@ class ReaderAPIAccountViewController: UITableViewController {
} }
} }
private func retrieveCredentialsForAccount(for account: Account) throws -> Credentials? { private func retrieveCredentialsForAccount(for account: Account) throws -> Credentials? {
switch accountType { switch accountType {
case .bazQux, .inoreader, .theOldReader, .freshRSS: case .bazQux, .inoreader, .theOldReader, .freshRSS:
@ -206,7 +205,7 @@ class ReaderAPIAccountViewController: UITableViewController {
return nil return nil
} }
} }
private func headerViewImage() -> UIImage? { private func headerViewImage() -> UIImage? {
if let accountType = accountType { if let accountType = accountType {
switch accountType { switch accountType {
@ -224,7 +223,7 @@ class ReaderAPIAccountViewController: UITableViewController {
} }
return nil return nil
} }
private func validateDataEntry() -> Bool { private func validateDataEntry() -> Bool {
switch accountType { switch accountType {
case .freshRSS: case .freshRSS:
@ -244,7 +243,7 @@ class ReaderAPIAccountViewController: UITableViewController {
} }
return true return true
} }
@IBAction func signUpWithProvider(_ sender: Any) { @IBAction func signUpWithProvider(_ sender: Any) {
var url: URL! var url: URL!
switch accountType { switch accountType {
@ -263,7 +262,7 @@ class ReaderAPIAccountViewController: UITableViewController {
safari.modalPresentationStyle = .currentContext safari.modalPresentationStyle = .currentContext
self.present(safari, animated: true, completion: nil) self.present(safari, animated: true, completion: nil)
} }
private func apiURL() -> URL? { private func apiURL() -> URL? {
switch accountType { switch accountType {
case .freshRSS: case .freshRSS:
@ -278,9 +277,7 @@ class ReaderAPIAccountViewController: UITableViewController {
return nil return nil
} }
} }
@objc func textDidChange(_ note: Notification) { @objc func textDidChange(_ note: Notification) {
actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false) actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false)
} }

View File

@ -12,10 +12,10 @@ class AddComboTableViewCell: VibrantTableViewCell {
@IBOutlet weak var icon: UIImageView! @IBOutlet weak var icon: UIImageView!
@IBOutlet weak var label: UILabel! @IBOutlet weak var label: UILabel!
override func updateVibrancy(animated: Bool) { override func updateVibrancy(animated: Bool) {
super.updateVibrancy(animated: animated) super.updateVibrancy(animated: animated)
let iconTintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : AppAssets.secondaryAccentColor let iconTintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : AppAssets.secondaryAccentColor
if animated { if animated {
UIView.animate(withDuration: Self.duration) { UIView.animate(withDuration: Self.duration) {
@ -26,5 +26,5 @@ class AddComboTableViewCell: VibrantTableViewCell {
} }
updateLabelVibrancy(label, color: labelColor, animated: animated) updateLabelVibrancy(label, color: labelColor, animated: animated)
} }
} }

View File

@ -15,15 +15,15 @@ protocol AddFeedFolderViewControllerDelegate {
} }
class AddFeedFolderViewController: UITableViewController { class AddFeedFolderViewController: UITableViewController {
var delegate: AddFeedFolderViewControllerDelegate? var delegate: AddFeedFolderViewControllerDelegate?
var initialContainer: Container? var initialContainer: Container?
var containers = [Container]() var containers = [Container]()
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
let sortedActiveAccounts = AccountManager.shared.sortedActiveAccounts let sortedActiveAccounts = AccountManager.shared.sortedActiveAccounts
for account in sortedActiveAccounts { for account in sortedActiveAccounts {
@ -53,15 +53,15 @@ class AddFeedFolderViewController: UITableViewController {
return tableView.dequeueReusableCell(withIdentifier: "FolderCell", for: indexPath) as! AddComboTableViewCell return tableView.dequeueReusableCell(withIdentifier: "FolderCell", for: indexPath) as! AddComboTableViewCell
} }
}() }()
if let smallIconProvider = container as? SmallIconProvider { if let smallIconProvider = container as? SmallIconProvider {
cell.icon?.image = smallIconProvider.smallIcon?.image cell.icon?.image = smallIconProvider.smallIcon?.image
} }
if let displayNameProvider = container as? DisplayNameProvider { if let displayNameProvider = container as? DisplayNameProvider {
cell.label?.text = displayNameProvider.nameForDisplay cell.label?.text = displayNameProvider.nameForDisplay
} }
if let compContainer = initialContainer, container === compContainer { if let compContainer = initialContainer, container === compContainer {
cell.accessoryType = .checkmark cell.accessoryType = .checkmark
} else { } else {
@ -73,7 +73,7 @@ class AddFeedFolderViewController: UITableViewController {
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let container = containers[indexPath.row] let container = containers[indexPath.row]
if let account = container as? Account, account.behaviors.contains(.disallowFeedInRootFolder) { if let account = container as? Account, account.behaviors.contains(.disallowFeedInRootFolder) {
tableView.selectRow(at: nil, animated: false, scrollPosition: .none) tableView.selectRow(at: nil, animated: false, scrollPosition: .none)
} else { } else {
@ -83,19 +83,19 @@ class AddFeedFolderViewController: UITableViewController {
dismiss() dismiss()
} }
} }
// MARK: Actions // MARK: Actions
@IBAction func cancel(_ sender: Any) { @IBAction func cancel(_ sender: Any) {
dismiss() dismiss()
} }
} }
private extension AddFeedFolderViewController { private extension AddFeedFolderViewController {
func dismiss() { func dismiss() {
dismiss(animated: true) dismiss(animated: true)
} }
} }

View File

@ -9,14 +9,14 @@
import UIKit import UIKit
class AddFeedSelectFolderTableViewCell: VibrantTableViewCell { class AddFeedSelectFolderTableViewCell: VibrantTableViewCell {
@IBOutlet weak var folderLabel: UILabel! @IBOutlet weak var folderLabel: UILabel!
@IBOutlet weak var detailLabel: UILabel! @IBOutlet weak var detailLabel: UILabel!
override func updateVibrancy(animated: Bool) { override func updateVibrancy(animated: Bool) {
super.updateVibrancy(animated: animated) super.updateVibrancy(animated: animated)
updateLabelVibrancy(folderLabel, color: labelColor, animated: animated) updateLabelVibrancy(folderLabel, color: labelColor, animated: animated)
updateLabelVibrancy(detailLabel, color: labelColor, animated: animated) updateLabelVibrancy(detailLabel, color: labelColor, animated: animated)
} }
} }

View File

@ -19,9 +19,9 @@ class AddFeedViewController: UITableViewController {
@IBOutlet weak var urlTextField: UITextField! @IBOutlet weak var urlTextField: UITextField!
@IBOutlet weak var urlTextFieldToSuperViewConstraint: NSLayoutConstraint! @IBOutlet weak var urlTextFieldToSuperViewConstraint: NSLayoutConstraint!
@IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var nameTextField: UITextField!
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0) static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0)
private var folderLabel = "" private var folderLabel = ""
private var userCancelled = false private var userCancelled = false
@ -29,88 +29,88 @@ class AddFeedViewController: UITableViewController {
var initialFeedName: String? var initialFeedName: String?
var container: Container? var container: Container?
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
activityIndicator.isHidden = true activityIndicator.isHidden = true
activityIndicator.color = .label activityIndicator.color = .label
if initialFeed == nil, let urlString = UIPasteboard.general.string { if initialFeed == nil, let urlString = UIPasteboard.general.string {
if urlString.mayBeURL { if urlString.mayBeURL {
initialFeed = urlString.normalizedURL initialFeed = urlString.normalizedURL
} }
} }
urlTextField.autocorrectionType = .no urlTextField.autocorrectionType = .no
urlTextField.autocapitalizationType = .none urlTextField.autocapitalizationType = .none
urlTextField.text = initialFeed urlTextField.text = initialFeed
urlTextField.delegate = self urlTextField.delegate = self
if initialFeed != nil { if initialFeed != nil {
addButton.isEnabled = true addButton.isEnabled = true
} }
nameTextField.text = initialFeedName nameTextField.text = initialFeedName
nameTextField.delegate = self nameTextField.delegate = self
if let defaultContainer = AddFeedDefaultContainer.defaultContainer { if let defaultContainer = AddFeedDefaultContainer.defaultContainer {
container = defaultContainer container = defaultContainer
} else { } else {
addButton.isEnabled = false addButton.isEnabled = false
} }
updateFolderLabel() updateFolderLabel()
tableView.register(UINib(nibName: "AddFeedSelectFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "AddFeedSelectFolderTableViewCell") tableView.register(UINib(nibName: "AddFeedSelectFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "AddFeedSelectFolderTableViewCell")
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: urlTextField) NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: urlTextField)
if initialFeed == nil { if initialFeed == nil {
urlTextField.becomeFirstResponder() urlTextField.becomeFirstResponder()
} }
} }
@IBAction func cancel(_ sender: Any) { @IBAction func cancel(_ sender: Any) {
userCancelled = true userCancelled = true
dismiss(animated: true) dismiss(animated: true)
} }
@IBAction func add(_ sender: Any) { @IBAction func add(_ sender: Any) {
let urlString = urlTextField.text ?? "" let urlString = urlTextField.text ?? ""
let normalizedURLString = urlString.normalizedURL let normalizedURLString = urlString.normalizedURL
guard !normalizedURLString.isEmpty, let url = URL(string: normalizedURLString) else { guard !normalizedURLString.isEmpty, let url = URL(string: normalizedURLString) else {
return return
} }
guard let container = container else { return } guard let container = container else { return }
var account: Account? var account: Account?
if let containerAccount = container as? Account { if let containerAccount = container as? Account {
account = containerAccount account = containerAccount
} else if let containerFolder = container as? Folder, let containerAccount = containerFolder.account { } else if let containerFolder = container as? Folder, let containerAccount = containerFolder.account {
account = containerAccount account = containerAccount
} }
if account!.hasFeed(withURL: url.absoluteString) { if account!.hasFeed(withURL: url.absoluteString) {
presentError(AccountError.createErrorAlreadySubscribed) presentError(AccountError.createErrorAlreadySubscribed)
return return
} }
addButton.isEnabled = false addButton.isEnabled = false
activityIndicator.isHidden = false activityIndicator.isHidden = false
activityIndicator.startAnimating() activityIndicator.startAnimating()
let feedName = (nameTextField.text?.isEmpty ?? true) ? nil : nameTextField.text let feedName = (nameTextField.text?.isEmpty ?? true) ? nil : nameTextField.text
BatchUpdate.shared.start() BatchUpdate.shared.start()
account!.createFeed(url: url.absoluteString, name: feedName, container: container, validateFeed: true) { result in account!.createFeed(url: url.absoluteString, name: feedName, container: container, validateFeed: true) { result in
BatchUpdate.shared.end() BatchUpdate.shared.end()
switch result { switch result {
case .success(let feed): case .success(let feed):
self.dismiss(animated: true) self.dismiss(animated: true)
@ -125,11 +125,11 @@ class AddFeedViewController: UITableViewController {
} }
} }
@objc func textDidChange(_ note: Notification) { @objc func textDidChange(_ note: Notification) {
updateUI() updateUI()
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.row == 2 { if indexPath.row == 2 {
let cell = tableView.dequeueReusableCell(withIdentifier: "AddFeedSelectFolderTableViewCell", for: indexPath) as? AddFeedSelectFolderTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: "AddFeedSelectFolderTableViewCell", for: indexPath) as? AddFeedSelectFolderTableViewCell
@ -139,7 +139,7 @@ class AddFeedViewController: UITableViewController {
return super.tableView(tableView, cellForRowAt: indexPath) return super.tableView(tableView, cellForRowAt: indexPath)
} }
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if indexPath.row == 2 { if indexPath.row == 2 {
let navController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddFeedFolderNavViewController") as! UINavigationController let navController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddFeedFolderNavViewController") as! UINavigationController
@ -150,7 +150,7 @@ class AddFeedViewController: UITableViewController {
present(navController, animated: true) present(navController, animated: true)
} }
} }
} }
// MARK: AddFeedFolderViewControllerDelegate // MARK: AddFeedFolderViewControllerDelegate
@ -166,22 +166,22 @@ extension AddFeedViewController: AddFeedFolderViewControllerDelegate {
// MARK: UITextFieldDelegate // MARK: UITextFieldDelegate
extension AddFeedViewController: UITextFieldDelegate { extension AddFeedViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool { func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder() textField.resignFirstResponder()
return true return true
} }
} }
// MARK: Private // MARK: Private
private extension AddFeedViewController { private extension AddFeedViewController {
func updateUI() { func updateUI() {
addButton.isEnabled = (urlTextField.text?.mayBeURL ?? false) addButton.isEnabled = (urlTextField.text?.mayBeURL ?? false)
} }
func updateFolderLabel() { func updateFolderLabel() {
if let containerName = (container as? DisplayNameProvider)?.nameForDisplay { if let containerName = (container as? DisplayNameProvider)?.nameForDisplay {
if container is Folder { if container is Folder {

View File

@ -16,13 +16,13 @@ class AddFolderViewController: UITableViewController {
@IBOutlet private weak var nameTextField: UITextField! @IBOutlet private weak var nameTextField: UITextField!
@IBOutlet private weak var accountLabel: UILabel! @IBOutlet private weak var accountLabel: UILabel!
@IBOutlet private weak var accountPickerView: UIPickerView! @IBOutlet private weak var accountPickerView: UIPickerView!
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0) static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0)
private var shouldDisplayPicker: Bool { private var shouldDisplayPicker: Bool {
return accounts.count > 1 return accounts.count > 1
} }
private var accounts: [Account]! { private var accounts: [Account]! {
didSet { didSet {
if let predefinedAccount = accounts.first(where: { $0.accountID == AppDefaults.shared.addFolderAccountID }) { if let predefinedAccount = accounts.first(where: { $0.accountID == AppDefaults.shared.addFolderAccountID }) {
@ -39,33 +39,33 @@ class AddFolderViewController: UITableViewController {
accountLabel.text = selectedAccount.flatMap { ($0 as DisplayNameProvider).nameForDisplay } accountLabel.text = selectedAccount.flatMap { ($0 as DisplayNameProvider).nameForDisplay }
} }
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
accounts = AccountManager.shared accounts = AccountManager.shared
.sortedActiveAccounts .sortedActiveAccounts
.filter { !$0.behaviors.contains(.disallowFolderManagement) } .filter { !$0.behaviors.contains(.disallowFolderManagement) }
nameTextField.delegate = self nameTextField.delegate = self
if shouldDisplayPicker { if shouldDisplayPicker {
accountPickerView.dataSource = self accountPickerView.dataSource = self
accountPickerView.delegate = self accountPickerView.delegate = self
if let index = accounts.firstIndex(of: selectedAccount) { if let index = accounts.firstIndex(of: selectedAccount) {
accountPickerView.selectRow(index, inComponent: 0, animated: false) accountPickerView.selectRow(index, inComponent: 0, animated: false)
} }
} else { } else {
accountPickerView.isHidden = true accountPickerView.isHidden = true
} }
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: nameTextField) NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: nameTextField)
nameTextField.becomeFirstResponder() nameTextField.becomeFirstResponder()
} }
private func didSelect(_ account: Account) { private func didSelect(_ account: Account) {
AppDefaults.shared.addFolderAccountID = account.accountID AppDefaults.shared.addFolderAccountID = account.accountID
selectedAccount = account selectedAccount = account
@ -74,7 +74,7 @@ class AddFolderViewController: UITableViewController {
@IBAction func cancel(_ sender: Any) { @IBAction func cancel(_ sender: Any) {
dismiss(animated: true) dismiss(animated: true)
} }
@IBAction func add(_ sender: Any) { @IBAction func add(_ sender: Any) {
guard let folderName = nameTextField.text else { guard let folderName = nameTextField.text else {
return return
@ -93,42 +93,42 @@ class AddFolderViewController: UITableViewController {
@objc func textDidChange(_ note: Notification) { @objc func textDidChange(_ note: Notification) {
addButton.isEnabled = !(nameTextField.text?.isEmpty ?? false) addButton.isEnabled = !(nameTextField.text?.isEmpty ?? false)
} }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: section) let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: section)
if section == 1 && !shouldDisplayPicker { if section == 1 && !shouldDisplayPicker {
return defaultNumberOfRows - 1 return defaultNumberOfRows - 1
} }
return defaultNumberOfRows return defaultNumberOfRows
} }
} }
extension AddFolderViewController: UIPickerViewDataSource, UIPickerViewDelegate { extension AddFolderViewController: UIPickerViewDataSource, UIPickerViewDelegate {
func numberOfComponents(in pickerView: UIPickerView) ->Int { func numberOfComponents(in pickerView: UIPickerView) -> Int {
return 1 return 1
} }
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int { func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
return accounts.count return accounts.count
} }
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? { func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
return (accounts[row] as DisplayNameProvider).nameForDisplay return (accounts[row] as DisplayNameProvider).nameForDisplay
} }
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) { func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
didSelect(accounts[row]) didSelect(accounts[row])
} }
} }
extension AddFolderViewController: UITextFieldDelegate { extension AddFolderViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool { func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder() textField.resignFirstResponder()
return true return true
} }
} }

View File

@ -12,10 +12,10 @@ class SelectComboTableViewCell: VibrantTableViewCell {
@IBOutlet weak var icon: UIImageView! @IBOutlet weak var icon: UIImageView!
@IBOutlet weak var label: UILabel! @IBOutlet weak var label: UILabel!
override func updateVibrancy(animated: Bool) { override func updateVibrancy(animated: Bool) {
super.updateVibrancy(animated: animated) super.updateVibrancy(animated: animated)
let iconTintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label let iconTintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label
if animated { if animated {
UIView.animate(withDuration: Self.duration) { UIView.animate(withDuration: Self.duration) {
@ -24,8 +24,8 @@ class SelectComboTableViewCell: VibrantTableViewCell {
} else { } else {
self.icon.tintColor = iconTintColor self.icon.tintColor = iconTintColor
} }
updateLabelVibrancy(label, color: labelColor, animated: animated) updateLabelVibrancy(label, color: labelColor, animated: animated)
} }
} }

View File

@ -10,7 +10,7 @@ import RSCore
import Account import Account
struct AppAssets { struct AppAssets {
static var accountBazQuxImage: UIImage = { static var accountBazQuxImage: UIImage = {
return UIImage(named: "accountBazQux")! return UIImage(named: "accountBazQux")!
}() }()
@ -90,43 +90,43 @@ struct AppAssets {
static var circleClosedImage: UIImage = { static var circleClosedImage: UIImage = {
return UIImage(systemName: "largecircle.fill.circle")! return UIImage(systemName: "largecircle.fill.circle")!
}() }()
static var circleOpenImage: UIImage = { static var circleOpenImage: UIImage = {
return UIImage(systemName: "circle")! return UIImage(systemName: "circle")!
}() }()
static var disclosureImage: UIImage = { static var disclosureImage: UIImage = {
return UIImage(named: "disclosure")! return UIImage(named: "disclosure")!
}() }()
static var copyImage: UIImage = { static var copyImage: UIImage = {
return UIImage(systemName: "doc.on.doc")! return UIImage(systemName: "doc.on.doc")!
}() }()
static var deactivateImage: UIImage = { static var deactivateImage: UIImage = {
UIImage(systemName: "minus.circle")! UIImage(systemName: "minus.circle")!
}() }()
static var editImage: UIImage = { static var editImage: UIImage = {
UIImage(systemName: "square.and.pencil")! UIImage(systemName: "square.and.pencil")!
}() }()
static var faviconTemplateImage: RSImage = { static var faviconTemplateImage: RSImage = {
return RSImage(named: "faviconTemplateImage")! return RSImage(named: "faviconTemplateImage")!
}() }()
static var filterInactiveImage: UIImage = { static var filterInactiveImage: UIImage = {
UIImage(systemName: "line.horizontal.3.decrease.circle")! UIImage(systemName: "line.horizontal.3.decrease.circle")!
}() }()
static var filterActiveImage: UIImage = { static var filterActiveImage: UIImage = {
UIImage(systemName: "line.horizontal.3.decrease.circle.fill")! UIImage(systemName: "line.horizontal.3.decrease.circle.fill")!
}() }()
static var folderOutlinePlus: UIImage = { static var folderOutlinePlus: UIImage = {
UIImage(systemName: "folder.badge.plus")! UIImage(systemName: "folder.badge.plus")!
}() }()
static var fullScreenBackgroundColor: UIColor = { static var fullScreenBackgroundColor: UIColor = {
return UIColor(named: "fullScreenBackgroundColor")! return UIColor(named: "fullScreenBackgroundColor")!
}() }()
@ -134,19 +134,19 @@ struct AppAssets {
static var infoImage: UIImage = { static var infoImage: UIImage = {
UIImage(systemName: "info.circle")! UIImage(systemName: "info.circle")!
}() }()
static var markAllAsReadImage: UIImage = { static var markAllAsReadImage: UIImage = {
return UIImage(named: "markAllAsRead")! return UIImage(named: "markAllAsRead")!
}() }()
static var markBelowAsReadImage: UIImage = { static var markBelowAsReadImage: UIImage = {
return UIImage(systemName: "arrowtriangle.down.circle")! return UIImage(systemName: "arrowtriangle.down.circle")!
}() }()
static var markAboveAsReadImage: UIImage = { static var markAboveAsReadImage: UIImage = {
return UIImage(systemName: "arrowtriangle.up.circle")! return UIImage(systemName: "arrowtriangle.up.circle")!
}() }()
static var folderImage: IconImage = { static var folderImage: IconImage = {
return IconImage(UIImage(systemName: "folder.fill")!, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor) return IconImage(UIImage(systemName: "folder.fill")!, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor)
}() }()
@ -154,67 +154,67 @@ struct AppAssets {
static var moreImage: UIImage = { static var moreImage: UIImage = {
return UIImage(systemName: "ellipsis.circle")! return UIImage(systemName: "ellipsis.circle")!
}() }()
static var nextArticleImage: UIImage = { static var nextArticleImage: UIImage = {
return UIImage(systemName: "chevron.down")! return UIImage(systemName: "chevron.down")!
}() }()
static var nextUnreadArticleImage: UIImage = { static var nextUnreadArticleImage: UIImage = {
return UIImage(systemName: "chevron.down.circle")! return UIImage(systemName: "chevron.down.circle")!
}() }()
static var plus: UIImage = { static var plus: UIImage = {
UIImage(systemName: "plus")! UIImage(systemName: "plus")!
}() }()
static var prevArticleImage: UIImage = { static var prevArticleImage: UIImage = {
return UIImage(systemName: "chevron.up")! return UIImage(systemName: "chevron.up")!
}() }()
static var openInSidebarImage: UIImage = { static var openInSidebarImage: UIImage = {
return UIImage(systemName: "arrow.turn.down.left")! return UIImage(systemName: "arrow.turn.down.left")!
}() }()
static var primaryAccentColor: UIColor { static var primaryAccentColor: UIColor {
return UIColor(named: "primaryAccentColor")! return UIColor(named: "primaryAccentColor")!
} }
static var safariImage: UIImage = { static var safariImage: UIImage = {
return UIImage(systemName: "safari")! return UIImage(systemName: "safari")!
}() }()
static var searchFeedImage: IconImage = { static var searchFeedImage: IconImage = {
return IconImage(UIImage(systemName: "magnifyingglass")!, isSymbol: true) return IconImage(UIImage(systemName: "magnifyingglass")!, isSymbol: true)
}() }()
static var secondaryAccentColor: UIColor { static var secondaryAccentColor: UIColor {
return UIColor(named: "secondaryAccentColor")! return UIColor(named: "secondaryAccentColor")!
} }
static var sectionHeaderColor: UIColor = { static var sectionHeaderColor: UIColor = {
return UIColor(named: "sectionHeaderColor")! return UIColor(named: "sectionHeaderColor")!
}() }()
static var shareImage: UIImage = { static var shareImage: UIImage = {
return UIImage(systemName: "square.and.arrow.up")! return UIImage(systemName: "square.and.arrow.up")!
}() }()
static var smartFeedImage: UIImage = { static var smartFeedImage: UIImage = {
return UIImage(systemName: "gear")! return UIImage(systemName: "gear")!
}() }()
static var starColor: UIColor = { static var starColor: UIColor = {
return UIColor(named: "starColor")! return UIColor(named: "starColor")!
}() }()
static var starClosedImage: UIImage = { static var starClosedImage: UIImage = {
return UIImage(systemName: "star.fill")! return UIImage(systemName: "star.fill")!
}() }()
static var starOpenImage: UIImage = { static var starOpenImage: UIImage = {
return UIImage(systemName: "star")! return UIImage(systemName: "star")!
}() }()
static var starredFeedImage: IconImage { static var starredFeedImage: IconImage {
let image = UIImage(systemName: "star.fill")! let image = UIImage(systemName: "star.fill")!
return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppAssets.starColor.cgColor) return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppAssets.starColor.cgColor)
@ -223,12 +223,12 @@ struct AppAssets {
static var tickMarkColor: UIColor = { static var tickMarkColor: UIColor = {
return UIColor(named: "tickMarkColor")! return UIColor(named: "tickMarkColor")!
}() }()
static var timelineStarImage: UIImage = { static var timelineStarImage: UIImage = {
let image = UIImage(systemName: "star.fill")! let image = UIImage(systemName: "star.fill")!
return image.withTintColor(AppAssets.starColor, renderingMode: .alwaysOriginal) return image.withTintColor(AppAssets.starColor, renderingMode: .alwaysOriginal)
}() }()
static var todayFeedImage: IconImage { static var todayFeedImage: IconImage {
let image = UIImage(systemName: "sun.max.fill")! let image = UIImage(systemName: "sun.max.fill")!
return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: UIColor.systemOrange.cgColor) return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: UIColor.systemOrange.cgColor)
@ -237,12 +237,12 @@ struct AppAssets {
static var trashImage: UIImage = { static var trashImage: UIImage = {
return UIImage(systemName: "trash")! return UIImage(systemName: "trash")!
}() }()
static var unreadFeedImage: IconImage { static var unreadFeedImage: IconImage {
let image = UIImage(systemName: "largecircle.fill.circle")! let image = UIImage(systemName: "largecircle.fill.circle")!
return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor) return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor)
} }
static var vibrantTextColor: UIColor = { static var vibrantTextColor: UIColor = {
return UIColor(named: "vibrantTextColor")! return UIColor(named: "vibrantTextColor")!
}() }()
@ -251,7 +251,6 @@ struct AppAssets {
return UIColor(named: "controlBackgroundColor")! return UIColor(named: "controlBackgroundColor")!
}() }()
static func image(for accountType: AccountType) -> UIImage? { static func image(for accountType: AccountType) -> UIImage? {
switch accountType { switch accountType {
case .onMyMac: case .onMyMac:
@ -278,5 +277,5 @@ struct AppAssets {
return AppAssets.accountTheOldReaderImage return AppAssets.accountTheOldReaderImage
} }
} }
} }

View File

@ -23,22 +23,22 @@ enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable {
return NSLocalizedString("Dark", comment: "Dark") return NSLocalizedString("Dark", comment: "Dark")
} }
} }
} }
final class AppDefaults { final class AppDefaults {
static let defaultThemeName = "Default" static let defaultThemeName = "Default"
static let shared = AppDefaults() static let shared = AppDefaults()
private init() {} private init() {}
static var store: UserDefaults = { static var store: UserDefaults = {
let appIdentifierPrefix = Bundle.main.object(forInfoDictionaryKey: "AppIdentifierPrefix") as! String let appIdentifierPrefix = Bundle.main.object(forInfoDictionaryKey: "AppIdentifierPrefix") as! String
let suiteName = "\(appIdentifierPrefix)group.\(Bundle.main.bundleIdentifier!)" let suiteName = "\(appIdentifierPrefix)group.\(Bundle.main.bundleIdentifier!)"
return UserDefaults.init(suiteName: suiteName)! return UserDefaults.init(suiteName: suiteName)!
}() }()
struct Key { struct Key {
static let userInterfaceColorPalette = "userInterfaceColorPalette" static let userInterfaceColorPalette = "userInterfaceColorPalette"
static let lastImageCacheFlushDate = "lastImageCacheFlushDate" static let lastImageCacheFlushDate = "lastImageCacheFlushDate"
@ -74,7 +74,7 @@ final class AppDefaults {
firstRunDate = Date() firstRunDate = Date()
return true return true
}() }()
static var userInterfaceColorPalette: UserInterfaceColorPalette { static var userInterfaceColorPalette: UserInterfaceColorPalette {
get { get {
if let result = UserInterfaceColorPalette(rawValue: int(for: Key.userInterfaceColorPalette)) { if let result = UserInterfaceColorPalette(rawValue: int(for: Key.userInterfaceColorPalette)) {
@ -95,7 +95,7 @@ final class AppDefaults {
AppDefaults.setString(for: Key.addFeedAccountID, newValue) AppDefaults.setString(for: Key.addFeedAccountID, newValue)
} }
} }
var addFeedFolderName: String? { var addFeedFolderName: String? {
get { get {
return AppDefaults.string(for: Key.addFeedFolderName) return AppDefaults.string(for: Key.addFeedFolderName)
@ -104,7 +104,7 @@ final class AppDefaults {
AppDefaults.setString(for: Key.addFeedFolderName, newValue) AppDefaults.setString(for: Key.addFeedFolderName, newValue)
} }
} }
var addFolderAccountID: String? { var addFolderAccountID: String? {
get { get {
return AppDefaults.string(for: Key.addFolderAccountID) return AppDefaults.string(for: Key.addFolderAccountID)
@ -113,7 +113,7 @@ final class AppDefaults {
AppDefaults.setString(for: Key.addFolderAccountID, newValue) AppDefaults.setString(for: Key.addFolderAccountID, newValue)
} }
} }
var useSystemBrowser: Bool { var useSystemBrowser: Bool {
get { get {
return UserDefaults.standard.bool(forKey: Key.useSystemBrowser) return UserDefaults.standard.bool(forKey: Key.useSystemBrowser)
@ -122,7 +122,7 @@ final class AppDefaults {
UserDefaults.standard.setValue(newValue, forKey: Key.useSystemBrowser) UserDefaults.standard.setValue(newValue, forKey: Key.useSystemBrowser)
} }
} }
var lastImageCacheFlushDate: Date? { var lastImageCacheFlushDate: Date? {
get { get {
return AppDefaults.date(for: Key.lastImageCacheFlushDate) return AppDefaults.date(for: Key.lastImageCacheFlushDate)
@ -189,7 +189,7 @@ final class AppDefaults {
AppDefaults.setBool(for: Key.confirmMarkAllAsRead, newValue) AppDefaults.setBool(for: Key.confirmMarkAllAsRead, newValue)
} }
} }
var lastRefresh: Date? { var lastRefresh: Date? {
get { get {
return AppDefaults.date(for: Key.lastRefresh) return AppDefaults.date(for: Key.lastRefresh)
@ -198,7 +198,7 @@ final class AppDefaults {
AppDefaults.setDate(for: Key.lastRefresh, newValue) AppDefaults.setDate(for: Key.lastRefresh, newValue)
} }
} }
var timelineNumberOfLines: Int { var timelineNumberOfLines: Int {
get { get {
return AppDefaults.int(for: Key.timelineNumberOfLines) return AppDefaults.int(for: Key.timelineNumberOfLines)
@ -207,7 +207,7 @@ final class AppDefaults {
AppDefaults.setInt(for: Key.timelineNumberOfLines, newValue) AppDefaults.setInt(for: Key.timelineNumberOfLines, newValue)
} }
} }
var timelineIconSize: IconSize { var timelineIconSize: IconSize {
get { get {
let rawValue = AppDefaults.store.integer(forKey: Key.timelineIconDimension) let rawValue = AppDefaults.store.integer(forKey: Key.timelineIconDimension)
@ -217,7 +217,7 @@ final class AppDefaults {
AppDefaults.store.set(newValue.rawValue, forKey: Key.timelineIconDimension) AppDefaults.store.set(newValue.rawValue, forKey: Key.timelineIconDimension)
} }
} }
var currentThemeName: String? { var currentThemeName: String? {
get { get {
return AppDefaults.string(for: Key.currentThemeName) return AppDefaults.string(for: Key.currentThemeName)
@ -237,7 +237,7 @@ final class AppDefaults {
} }
static func registerDefaults() { static func registerDefaults() {
let defaults: [String : Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue, let defaults: [String: Any] = [Key.userInterfaceColorPalette: UserInterfaceColorPalette.automatic.rawValue,
Key.timelineGroupByFeed: false, Key.timelineGroupByFeed: false,
Key.refreshClearsReadArticles: false, Key.refreshClearsReadArticles: false,
Key.timelineNumberOfLines: 2, Key.timelineNumberOfLines: 2,
@ -266,7 +266,7 @@ private extension AppDefaults {
static func string(for key: String) -> String? { static func string(for key: String) -> String? {
return UserDefaults.standard.string(forKey: key) return UserDefaults.standard.string(forKey: key)
} }
static func setString(for key: String, _ value: String?) { static func setString(for key: String, _ value: String?) {
UserDefaults.standard.set(value, forKey: key) UserDefaults.standard.set(value, forKey: key)
} }
@ -282,11 +282,11 @@ private extension AppDefaults {
static func int(for key: String) -> Int { static func int(for key: String) -> Int {
return AppDefaults.store.integer(forKey: key) return AppDefaults.store.integer(forKey: key)
} }
static func setInt(for key: String, _ x: Int) { static func setInt(for key: String, _ x: Int) {
AppDefaults.store.set(x, forKey: key) AppDefaults.store.set(x, forKey: key)
} }
static func date(for key: String) -> Date? { static func date(for key: String) -> Date? {
return AppDefaults.store.object(forKey: key) as? Date return AppDefaults.store.object(forKey: key) as? Date
} }
@ -295,7 +295,7 @@ private extension AppDefaults {
AppDefaults.store.set(date, forKey: key) AppDefaults.store.set(date, forKey: key)
} }
static func sortDirection(for key:String) -> ComparisonResult { static func sortDirection(for key: String) -> ComparisonResult {
let rawInt = int(for: key) let rawInt = int(for: key)
if rawInt == ComparisonResult.orderedAscending.rawValue { if rawInt == ComparisonResult.orderedAscending.rawValue {
return .orderedAscending return .orderedAscending
@ -306,10 +306,9 @@ private extension AppDefaults {
static func setSortDirection(for key: String, _ value: ComparisonResult) { static func setSortDirection(for key: String, _ value: ComparisonResult) {
if value == .orderedAscending { if value == .orderedAscending {
setInt(for: key, ComparisonResult.orderedAscending.rawValue) setInt(for: key, ComparisonResult.orderedAscending.rawValue)
} } else {
else {
setInt(for: key, ComparisonResult.orderedDescending.rawValue) setInt(for: key, ComparisonResult.orderedDescending.rawValue)
} }
} }
} }

View File

@ -19,14 +19,14 @@ var appDelegate: AppDelegate!
@UIApplicationMain @UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider { class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider {
private var bgTaskDispatchQueue = DispatchQueue.init(label: "BGTaskScheduler") private var bgTaskDispatchQueue = DispatchQueue.init(label: "BGTaskScheduler")
private var waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid private var waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
var syncTimer: ArticleStatusSyncTimer? var syncTimer: ArticleStatusSyncTimer?
var shuttingDown = false { var shuttingDown = false {
didSet { didSet {
if shuttingDown { if shuttingDown {
@ -35,7 +35,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
} }
} }
} }
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Application") private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
var userNotificationManager: UserNotificationManager! var userNotificationManager: UserNotificationManager!
@ -52,10 +52,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
} }
} }
} }
var isSyncArticleStatusRunning = false var isSyncArticleStatusRunning = false
var isWaitingForSyncTasks = false var isWaitingForSyncTasks = false
override init() { override init() {
super.init() super.init()
appDelegate = self appDelegate = self
@ -64,15 +64,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
let documentAccountsFolder = documentFolder.appendingPathComponent("Accounts").absoluteString let documentAccountsFolder = documentFolder.appendingPathComponent("Accounts").absoluteString
let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7))) let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7)))
AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath) AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath)
let documentThemesFolder = documentFolder.appendingPathComponent("Themes").absoluteString let documentThemesFolder = documentFolder.appendingPathComponent("Themes").absoluteString
let documentThemesFolderPath = String(documentThemesFolder.suffix(from: documentAccountsFolder.index(documentThemesFolder.startIndex, offsetBy: 7))) let documentThemesFolderPath = String(documentThemesFolder.suffix(from: documentAccountsFolder.index(documentThemesFolder.startIndex, offsetBy: 7)))
ArticleThemesManager.shared = ArticleThemesManager(folderPath: documentThemesFolderPath) ArticleThemesManager.shared = ArticleThemesManager(folderPath: documentThemesFolderPath)
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
} }
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
AppDefaults.registerDefaults() AppDefaults.registerDefaults()
@ -80,22 +80,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
if isFirstRun { if isFirstRun {
logger.info("Is first run.") logger.info("Is first run.")
} }
if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() { if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() {
let localAccount = AccountManager.shared.defaultAccount let localAccount = AccountManager.shared.defaultAccount
DefaultFeedsImporter.importDefaultFeeds(account: localAccount) DefaultFeedsImporter.importDefaultFeeds(account: localAccount)
} }
registerBackgroundTasks() registerBackgroundTasks()
CacheCleaner.purgeIfNecessary() CacheCleaner.purgeIfNecessary()
initializeDownloaders() initializeDownloaders()
initializeHomeScreenQuickActions() initializeHomeScreenQuickActions()
DispatchQueue.main.async { DispatchQueue.main.async {
self.unreadCount = AccountManager.shared.unreadCount self.unreadCount = AccountManager.shared.unreadCount
} }
UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { (granted, _) in
if granted { if granted {
DispatchQueue.main.async { DispatchQueue.main.async {
UIApplication.shared.registerForRemoteNotifications() UIApplication.shared.registerForRemoteNotifications()
@ -108,7 +108,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
extensionContainersFile = ExtensionContainersFile() extensionContainersFile = ExtensionContainersFile()
extensionFeedAddRequestFile = ExtensionFeedAddRequestFile() extensionFeedAddRequestFile = ExtensionFeedAddRequestFile()
widgetDataEncoder = WidgetDataEncoder() widgetDataEncoder = WidgetDataEncoder()
syncTimer = ArticleStatusSyncTimer() syncTimer = ArticleStatusSyncTimer()
@ -116,12 +116,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
#if DEBUG #if DEBUG
syncTimer!.update() syncTimer!.update()
#endif #endif
return true return true
} }
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) { func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable: Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
DispatchQueue.main.async { DispatchQueue.main.async {
self.resumeDatabaseProcessingIfNecessary() self.resumeDatabaseProcessingIfNecessary()
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) { AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) {
@ -130,7 +130,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
} }
} }
} }
func applicationWillTerminate(_ application: UIApplication) { func applicationWillTerminate(_ application: UIApplication) {
shuttingDown = true shuttingDown = true
} }
@ -138,35 +138,35 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
func applicationDidEnterBackground(_ application: UIApplication) { func applicationDidEnterBackground(_ application: UIApplication) {
IconImageCache.shared.emptyCache() IconImageCache.shared.emptyCache()
} }
// MARK: Notifications // MARK: Notifications
@objc func unreadCountDidChange(_ note: Notification) { @objc func unreadCountDidChange(_ note: Notification) {
if note.object is AccountManager { if note.object is AccountManager {
unreadCount = AccountManager.shared.unreadCount unreadCount = AccountManager.shared.unreadCount
} }
} }
@objc func accountRefreshDidFinish(_ note: Notification) { @objc func accountRefreshDidFinish(_ note: Notification) {
AppDefaults.shared.lastRefresh = Date() AppDefaults.shared.lastRefresh = Date()
} }
// MARK: - API // MARK: - API
func manualRefresh(errorHandler: @escaping (Error) -> ()) { func manualRefresh(errorHandler: @escaping (Error) -> Void) {
UIApplication.shared.connectedScenes.compactMap( { $0.delegate as? SceneDelegate } ).forEach { UIApplication.shared.connectedScenes.compactMap( { $0.delegate as? SceneDelegate }).forEach {
$0.cleanUp(conditional: true) $0.cleanUp(conditional: true)
} }
AccountManager.shared.refreshAll(errorHandler: errorHandler) AccountManager.shared.refreshAll(errorHandler: errorHandler)
} }
func resumeDatabaseProcessingIfNecessary() { func resumeDatabaseProcessingIfNecessary() {
if AccountManager.shared.isSuspended { if AccountManager.shared.isSuspended {
AccountManager.shared.resumeAll() AccountManager.shared.resumeAll()
logger.info("Application processing resumed.") logger.info("Application processing resumed.")
} }
} }
func prepareAccountsForBackground() { func prepareAccountsForBackground() {
extensionFeedAddRequestFile.suspend() extensionFeedAddRequestFile.suspend()
syncTimer?.invalidate() syncTimer?.invalidate()
@ -190,16 +190,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log) AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
} }
} }
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.list, .banner, .badge, .sound]) completionHandler([.list, .banner, .badge, .sound])
} }
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) { func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
defer { completionHandler() } defer { completionHandler() }
let userInfo = response.notification.request.content.userInfo let userInfo = response.notification.request.content.userInfo
switch response.actionIdentifier { switch response.actionIdentifier {
case "MARK_AS_READ": case "MARK_AS_READ":
handleMarkAsRead(userInfo: userInfo) handleMarkAsRead(userInfo: userInfo)
@ -213,15 +213,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
}) })
} }
} }
} }
} }
// MARK: App Initialization // MARK: App Initialization
private extension AppDelegate { private extension AppDelegate {
private func initializeDownloaders() { private func initializeDownloaders() {
let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
let faviconsFolderURL = tempDir.appendingPathComponent("Favicons") let faviconsFolderURL = tempDir.appendingPathComponent("Favicons")
@ -239,7 +239,7 @@ private extension AppDelegate {
let unreadTitle = NSLocalizedString("First Unread", comment: "First Unread") let unreadTitle = NSLocalizedString("First Unread", comment: "First Unread")
let unreadIcon = UIApplicationShortcutIcon(systemImageName: "chevron.down.circle") let unreadIcon = UIApplicationShortcutIcon(systemImageName: "chevron.down.circle")
let unreadItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.FirstUnread", localizedTitle: unreadTitle, localizedSubtitle: nil, icon: unreadIcon, userInfo: nil) let unreadItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.FirstUnread", localizedTitle: unreadTitle, localizedSubtitle: nil, icon: unreadIcon, userInfo: nil)
let searchTitle = NSLocalizedString("Search", comment: "Search") let searchTitle = NSLocalizedString("Search", comment: "Search")
let searchIcon = UIApplicationShortcutIcon(systemImageName: "magnifyingglass") let searchIcon = UIApplicationShortcutIcon(systemImageName: "magnifyingglass")
let searchItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.ShowSearch", localizedTitle: searchTitle, localizedSubtitle: nil, icon: searchIcon, userInfo: nil) let searchItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.ShowSearch", localizedTitle: searchTitle, localizedSubtitle: nil, icon: searchIcon, userInfo: nil)
@ -250,38 +250,38 @@ private extension AppDelegate {
UIApplication.shared.shortcutItems = [addItem, searchItem, unreadItem] UIApplication.shared.shortcutItems = [addItem, searchItem, unreadItem]
} }
} }
// MARK: Go To Background // MARK: Go To Background
private extension AppDelegate { private extension AppDelegate {
func waitForSyncTasksToFinish() { func waitForSyncTasksToFinish() {
guard !isWaitingForSyncTasks && UIApplication.shared.applicationState == .background else { return } guard !isWaitingForSyncTasks && UIApplication.shared.applicationState == .background else { return }
isWaitingForSyncTasks = true isWaitingForSyncTasks = true
self.waitBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { [weak self] in self.waitBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { [weak self] in
guard let self = self else { return } guard let self = self else { return }
self.completeProcessing(true) self.completeProcessing(true)
logger.info("Accounts wait for progress terminated for running too long.") logger.info("Accounts wait for progress terminated for running too long.")
} }
DispatchQueue.main.async { [weak self] in DispatchQueue.main.async { [weak self] in
self?.waitToComplete() { [weak self] suspend in self?.waitToComplete { [weak self] suspend in
self?.completeProcessing(suspend) self?.completeProcessing(suspend)
} }
} }
} }
func waitToComplete(completion: @escaping (Bool) -> Void) { func waitToComplete(completion: @escaping (Bool) -> Void) {
guard UIApplication.shared.applicationState == .background else { guard UIApplication.shared.applicationState == .background else {
logger.info("App came back to foreground, no longer waiting.") logger.info("App came back to foreground, no longer waiting.")
completion(false) completion(false)
return return
} }
if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning || widgetDataEncoder.isRunning { if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning || widgetDataEncoder.isRunning {
logger.info("Waiting for sync to finish…") logger.info("Waiting for sync to finish…")
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
@ -292,7 +292,7 @@ private extension AppDelegate {
completion(true) completion(true)
} }
} }
func completeProcessing(_ suspend: Bool) { func completeProcessing(_ suspend: Bool) {
if suspend { if suspend {
suspendApplication() suspendApplication()
@ -301,33 +301,33 @@ private extension AppDelegate {
self.waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid self.waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
isWaitingForSyncTasks = false isWaitingForSyncTasks = false
} }
func syncArticleStatus() { func syncArticleStatus() {
guard !isSyncArticleStatusRunning else { return } guard !isSyncArticleStatusRunning else { return }
isSyncArticleStatusRunning = true isSyncArticleStatusRunning = true
let completeProcessing = { [unowned self] in let completeProcessing = { [unowned self] in
self.isSyncArticleStatusRunning = false self.isSyncArticleStatusRunning = false
UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask) UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask)
self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
} }
self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask {
completeProcessing() completeProcessing()
self.logger.info("Accounts sync processing terminated for running too long.") self.logger.info("Accounts sync processing terminated for running too long.")
} }
DispatchQueue.main.async { DispatchQueue.main.async {
AccountManager.shared.syncArticleStatusAll() { AccountManager.shared.syncArticleStatusAll {
completeProcessing() completeProcessing()
} }
} }
} }
func suspendApplication() { func suspendApplication() {
guard UIApplication.shared.applicationState == .background else { return } guard UIApplication.shared.applicationState == .background else { return }
AccountManager.shared.suspendNetworkAll() AccountManager.shared.suspendNetworkAll()
AccountManager.shared.suspendDatabaseAll() AccountManager.shared.suspendDatabaseAll()
ArticleThemeDownloader.shared.cleanUp() ArticleThemeDownloader.shared.cleanUp()
@ -338,10 +338,10 @@ private extension AppDelegate {
sceneDelegate.suspend() sceneDelegate.suspend()
} }
} }
logger.info("Application processing suspended.") logger.info("Application processing suspended.")
} }
} }
// MARK: Background Tasks // MARK: Background Tasks
@ -355,7 +355,7 @@ private extension AppDelegate {
self.performBackgroundFeedRefresh(with: task as! BGAppRefreshTask) self.performBackgroundFeedRefresh(with: task as! BGAppRefreshTask)
} }
} }
/// Schedules a background app refresh based on `AppDefaults.refreshInterval`. /// Schedules a background app refresh based on `AppDefaults.refreshInterval`.
func scheduleBackgroundFeedRefresh() { func scheduleBackgroundFeedRefresh() {
let request = BGAppRefreshTaskRequest(identifier: "com.ranchero.NetNewsWire.FeedRefresh") let request = BGAppRefreshTaskRequest(identifier: "com.ranchero.NetNewsWire.FeedRefresh")
@ -371,7 +371,7 @@ private extension AppDelegate {
} }
} }
} }
/// Performs background feed refresh. /// Performs background feed refresh.
/// - Parameter task: `BGAppRefreshTask` /// - Parameter task: `BGAppRefreshTask`
/// - Warning: As of Xcode 11 beta 2, when triggered from the debugger this doesn't work. /// - Warning: As of Xcode 11 beta 2, when triggered from the debugger this doesn't work.
@ -408,9 +408,9 @@ private extension AppDelegate {
// Handle Notification Actions // Handle Notification Actions
private extension AppDelegate { private extension AppDelegate {
func handleMarkAsRead(userInfo: [AnyHashable: Any]) { func handleMarkAsRead(userInfo: [AnyHashable: Any]) {
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any],
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
return return
@ -435,9 +435,9 @@ private extension AppDelegate {
} }
}) })
} }
func handleMarkAsStarred(userInfo: [AnyHashable: Any]) { func handleMarkAsStarred(userInfo: [AnyHashable: Any]) {
guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable : Any], guard let articlePathUserInfo = userInfo[UserInfoKey.articlePath] as? [AnyHashable: Any],
let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String, let accountID = articlePathUserInfo[ArticlePathKey.accountID] as? String,
let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else { let articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
return return

View File

@ -16,9 +16,9 @@ enum ArticleExtractorButtonState {
} }
class ArticleExtractorButton: UIButton { class ArticleExtractorButton: UIButton {
private var animatedLayer: CALayer? private var animatedLayer: CALayer?
var buttonState: ArticleExtractorButtonState = .off { var buttonState: ArticleExtractorButtonState = .off {
didSet { didSet {
if buttonState != oldValue { if buttonState != oldValue {
@ -39,7 +39,7 @@ class ArticleExtractorButton: UIButton {
} }
} }
} }
override var accessibilityLabel: String? { override var accessibilityLabel: String? {
get { get {
switch buttonState { switch buttonState {
@ -57,7 +57,7 @@ class ArticleExtractorButton: UIButton {
super.accessibilityLabel = newValue super.accessibilityLabel = newValue
} }
} }
override func layoutSubviews() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
guard case .animated = buttonState else { guard case .animated = buttonState else {
@ -66,31 +66,31 @@ class ArticleExtractorButton: UIButton {
stripAnimatedSublayer() stripAnimatedSublayer()
addAnimatedSublayer(to: layer) addAnimatedSublayer(to: layer)
} }
private func stripAnimatedSublayer() { private func stripAnimatedSublayer() {
animatedLayer?.removeFromSuperlayer() animatedLayer?.removeFromSuperlayer()
} }
private func addAnimatedSublayer(to hostedLayer: CALayer) { private func addAnimatedSublayer(to hostedLayer: CALayer) {
let image1 = AppAssets.articleExtractorOffTinted.cgImage! let image1 = AppAssets.articleExtractorOffTinted.cgImage!
let image2 = AppAssets.articleExtractorOnTinted.cgImage! let image2 = AppAssets.articleExtractorOnTinted.cgImage!
let images = [image1, image2, image1] let images = [image1, image2, image1]
animatedLayer = CALayer() animatedLayer = CALayer()
let imageSize = AppAssets.articleExtractorOff.size let imageSize = AppAssets.articleExtractorOff.size
animatedLayer!.bounds = CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height) animatedLayer!.bounds = CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height)
animatedLayer!.position = CGPoint(x: bounds.midX, y: bounds.midY) animatedLayer!.position = CGPoint(x: bounds.midX, y: bounds.midY)
hostedLayer.addSublayer(animatedLayer!) hostedLayer.addSublayer(animatedLayer!)
let animation = CAKeyframeAnimation(keyPath: "contents") let animation = CAKeyframeAnimation(keyPath: "contents")
animation.calculationMode = CAAnimationCalculationMode.linear animation.calculationMode = CAAnimationCalculationMode.linear
animation.keyTimes = [0, 0.5, 1] animation.keyTimes = [0, 0.5, 1]
animation.duration = 2 animation.duration = 2
animation.values = images as [Any] animation.values = images as [Any]
animation.repeatCount = HUGE animation.repeatCount = HUGE
animatedLayer!.add(animation, forKey: "contents") animatedLayer!.add(animation, forKey: "contents")
} }
} }

View File

@ -15,15 +15,14 @@ import UIKit
@objc optional func searchBar(_ searchBar: ArticleSearchBar, textDidChange: String) @objc optional func searchBar(_ searchBar: ArticleSearchBar, textDidChange: String)
} }
@IBDesignable final class ArticleSearchBar: UIStackView { @IBDesignable final class ArticleSearchBar: UIStackView {
var searchField: UISearchTextField! var searchField: UISearchTextField!
var nextButton: UIButton! var nextButton: UIButton!
var prevButton: UIButton! var prevButton: UIButton!
var background: UIView! var background: UIView!
weak private var resultsLabel: UILabel! weak private var resultsLabel: UILabel!
var resultsCount: UInt = 0 { var resultsCount: UInt = 0 {
didSet { didSet {
updateUI() updateUI()
@ -34,30 +33,30 @@ import UIKit
updateUI() updateUI()
} }
} }
weak var delegate: SearchBarDelegate? weak var delegate: SearchBarDelegate?
override var keyCommands: [UIKeyCommand]? { override var keyCommands: [UIKeyCommand]? {
return [UIKeyCommand(title: "Exit Find", action: #selector(donePressed(_:)), input: UIKeyCommand.inputEscape)] return [UIKeyCommand(title: "Exit Find", action: #selector(donePressed(_:)), input: UIKeyCommand.inputEscape)]
} }
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
commonInit() commonInit()
} }
required init(coder: NSCoder) { required init(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
override func didMoveToSuperview() { override func didMoveToSuperview() {
super.didMoveToSuperview() super.didMoveToSuperview()
layer.backgroundColor = UIColor(named: "barBackgroundColor")?.cgColor ?? UIColor.white.cgColor layer.backgroundColor = UIColor(named: "barBackgroundColor")?.cgColor ?? UIColor.white.cgColor
isOpaque = true isOpaque = true
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: searchField) NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: searchField)
} }
private func updateUI() { private func updateUI() {
if resultsCount > 0 { if resultsCount > 0 {
let format = NSLocalizedString("%d of %d", comment: "Results selection and count") let format = NSLocalizedString("%d of %d", comment: "Results selection and count")
@ -65,23 +64,23 @@ import UIKit
} else { } else {
resultsLabel.text = NSLocalizedString("No results", comment: "No results") resultsLabel.text = NSLocalizedString("No results", comment: "No results")
} }
nextButton.isEnabled = selectedResult < resultsCount nextButton.isEnabled = selectedResult < resultsCount
prevButton.isEnabled = resultsCount > 0 && selectedResult > 1 prevButton.isEnabled = resultsCount > 0 && selectedResult > 1
} }
@discardableResult override func becomeFirstResponder() -> Bool { @discardableResult override func becomeFirstResponder() -> Bool {
searchField.becomeFirstResponder() searchField.becomeFirstResponder()
} }
@discardableResult override func resignFirstResponder() -> Bool { @discardableResult override func resignFirstResponder() -> Bool {
searchField.resignFirstResponder() searchField.resignFirstResponder()
} }
override var isFirstResponder: Bool { override var isFirstResponder: Bool {
searchField.isFirstResponder searchField.isFirstResponder
} }
deinit { deinit {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
} }
@ -94,12 +93,12 @@ private extension ArticleSearchBar {
spacing = 8 spacing = 8
layoutMargins.left = 8 layoutMargins.left = 8
layoutMargins.right = 8 layoutMargins.right = 8
background = UIView(frame: bounds) background = UIView(frame: bounds)
background.backgroundColor = .systemGray5 background.backgroundColor = .systemGray5
background.autoresizingMask = [.flexibleWidth, .flexibleHeight] background.autoresizingMask = [.flexibleWidth, .flexibleHeight]
addSubview(background) addSubview(background)
let doneButton = UIButton() let doneButton = UIButton()
doneButton.setTitle(NSLocalizedString("Done", comment: "Done"), for: .normal) doneButton.setTitle(NSLocalizedString("Done", comment: "Done"), for: .normal)
doneButton.setTitleColor(UIColor.label, for: .normal) doneButton.setTitleColor(UIColor.label, for: .normal)
@ -108,14 +107,14 @@ private extension ArticleSearchBar {
doneButton.addTarget(self, action: #selector(donePressed), for: .touchUpInside) doneButton.addTarget(self, action: #selector(donePressed), for: .touchUpInside)
doneButton.isEnabled = true doneButton.isEnabled = true
addArrangedSubview(doneButton) addArrangedSubview(doneButton)
let resultsLabel = UILabel() let resultsLabel = UILabel()
searchField = UISearchTextField() searchField = UISearchTextField()
searchField.autocapitalizationType = .none searchField.autocapitalizationType = .none
searchField.autocorrectionType = .no searchField.autocorrectionType = .no
searchField.returnKeyType = .search searchField.returnKeyType = .search
searchField.delegate = self searchField.delegate = self
resultsLabel.font = .systemFont(ofSize: UIFont.smallSystemFontSize) resultsLabel.font = .systemFont(ofSize: UIFont.smallSystemFontSize)
resultsLabel.textColor = .secondaryLabel resultsLabel.textColor = .secondaryLabel
resultsLabel.text = "" resultsLabel.text = ""
@ -123,17 +122,17 @@ private extension ArticleSearchBar {
resultsLabel.adjustsFontSizeToFitWidth = true resultsLabel.adjustsFontSizeToFitWidth = true
searchField.rightView = resultsLabel searchField.rightView = resultsLabel
searchField.rightViewMode = .always searchField.rightViewMode = .always
self.resultsLabel = resultsLabel self.resultsLabel = resultsLabel
addArrangedSubview(searchField) addArrangedSubview(searchField)
prevButton = UIButton(type: .system) prevButton = UIButton(type: .system)
prevButton.setImage(UIImage(systemName: "chevron.up"), for: .normal) prevButton.setImage(UIImage(systemName: "chevron.up"), for: .normal)
prevButton.accessibilityLabel = "Previous Result" prevButton.accessibilityLabel = "Previous Result"
prevButton.isAccessibilityElement = true prevButton.isAccessibilityElement = true
prevButton.addTarget(self, action: #selector(previousPressed), for: .touchUpInside) prevButton.addTarget(self, action: #selector(previousPressed), for: .touchUpInside)
addArrangedSubview(prevButton) addArrangedSubview(prevButton)
nextButton = UIButton(type: .system) nextButton = UIButton(type: .system)
nextButton.setImage(UIImage(systemName: "chevron.down"), for: .normal) nextButton.setImage(UIImage(systemName: "chevron.down"), for: .normal)
nextButton.accessibilityLabel = "Next Result" nextButton.accessibilityLabel = "Next Result"
@ -144,25 +143,25 @@ private extension ArticleSearchBar {
} }
private extension ArticleSearchBar { private extension ArticleSearchBar {
@objc func textDidChange(_ notification: Notification) { @objc func textDidChange(_ notification: Notification) {
delegate?.searchBar?(self, textDidChange: searchField.text ?? "") delegate?.searchBar?(self, textDidChange: searchField.text ?? "")
if searchField.text?.isEmpty ?? true { if searchField.text?.isEmpty ?? true {
searchField.rightViewMode = .never searchField.rightViewMode = .never
} else { } else {
searchField.rightViewMode = .always searchField.rightViewMode = .always
} }
} }
@objc func nextPressed() { @objc func nextPressed() {
delegate?.nextWasPressed?(self) delegate?.nextWasPressed?(self)
} }
@objc func previousPressed() { @objc func previousPressed() {
delegate?.previousWasPressed?(self) delegate?.previousWasPressed?(self)
} }
@objc func donePressed(_ _: Any? = nil) { @objc func donePressed(_ _: Any? = nil) {
delegate?.doneWasPressed?(self) delegate?.doneWasPressed?(self)
} }

View File

@ -16,21 +16,21 @@ final class ContextMenuPreviewViewController: UIViewController {
@IBOutlet weak var blogAuthorLabel: UILabel! @IBOutlet weak var blogAuthorLabel: UILabel!
@IBOutlet weak var articleTitleLabel: UILabel! @IBOutlet weak var articleTitleLabel: UILabel!
@IBOutlet weak var dateTimeLabel: UILabel! @IBOutlet weak var dateTimeLabel: UILabel!
var article: Article? var article: Article?
init(article: Article?) { init(article: Article?) {
self.article = article self.article = article
super.init(nibName: "ContextMenuPreviewViewController", bundle: nil) super.init(nibName: "ContextMenuPreviewViewController", bundle: nil)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
blogNameLabel.text = article?.feed?.nameForDisplay ?? "" blogNameLabel.text = article?.feed?.nameForDisplay ?? ""
blogAuthorLabel.text = article?.byline() blogAuthorLabel.text = article?.byline()
articleTitleLabel.text = article?.title ?? "" articleTitleLabel.text = article?.title ?? ""
@ -39,14 +39,14 @@ final class ContextMenuPreviewViewController: UIViewController {
icon.iconImage = article?.iconImage() icon.iconImage = article?.iconImage()
icon.translatesAutoresizingMaskIntoConstraints = false icon.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(icon) view.addSubview(icon)
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
icon.widthAnchor.constraint(equalToConstant: 48), icon.widthAnchor.constraint(equalToConstant: 48),
icon.heightAnchor.constraint(equalToConstant: 48), icon.heightAnchor.constraint(equalToConstant: 48),
icon.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8), icon.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8),
icon.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20) icon.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20)
]) ])
let dateFormatter = DateFormatter() let dateFormatter = DateFormatter()
dateFormatter.dateStyle = .long dateFormatter.dateStyle = .long
dateFormatter.timeStyle = .medium dateFormatter.timeStyle = .medium
@ -57,7 +57,7 @@ final class ContextMenuPreviewViewController: UIViewController {
// When in landscape the context menu preview will force this controller into a tiny // When in landscape the context menu preview will force this controller into a tiny
// view space. If it is documented anywhere what that is, I haven't found it. This // view space. If it is documented anywhere what that is, I haven't found it. This
// set of magic numbers is what I worked out by testing a variety of phones. // set of magic numbers is what I worked out by testing a variety of phones.
let width: CGFloat let width: CGFloat
let heightPadding: CGFloat let heightPadding: CGFloat
if view.bounds.width > view.bounds.height { if view.bounds.width > view.bounds.height {
@ -68,7 +68,7 @@ final class ContextMenuPreviewViewController: UIViewController {
width = view.bounds.width width = view.bounds.width
heightPadding = 8 heightPadding = 8
} }
view.setNeedsLayout() view.setNeedsLayout()
view.layoutIfNeeded() view.layoutIfNeeded()
preferredContentSize = CGSize(width: width, height: dateTimeLabel.frame.maxY + heightPadding) preferredContentSize = CGSize(width: width, height: dateTimeLabel.frame.maxY + heightPadding)

View File

@ -12,27 +12,27 @@ class FindInArticleActivity: UIActivity {
override var activityTitle: String? { override var activityTitle: String? {
NSLocalizedString("Find in Article", comment: "Find in Article") NSLocalizedString("Find in Article", comment: "Find in Article")
} }
override var activityType: UIActivity.ActivityType? { override var activityType: UIActivity.ActivityType? {
UIActivity.ActivityType(rawValue: "com.ranchero.NetNewsWire.find") UIActivity.ActivityType(rawValue: "com.ranchero.NetNewsWire.find")
} }
override var activityImage: UIImage? { override var activityImage: UIImage? {
UIImage(systemName: "magnifyingglass", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)) UIImage(systemName: "magnifyingglass", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))
} }
override class var activityCategory: UIActivity.Category { override class var activityCategory: UIActivity.Category {
.action .action
} }
override func canPerform(withActivityItems activityItems: [Any]) -> Bool { override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
true true
} }
override func prepare(withActivityItems activityItems: [Any]) { override func prepare(withActivityItems activityItems: [Any]) {
} }
override func perform() { override func perform() {
NotificationCenter.default.post(Notification(name: .FindInArticle)) NotificationCenter.default.post(Notification(name: .FindInArticle))
activityDidFinish(true) activityDidFinish(true)

View File

@ -14,63 +14,63 @@ import UIKit
} }
open class ImageScrollView: UIScrollView { open class ImageScrollView: UIScrollView {
@objc public enum ScaleMode: Int { @objc public enum ScaleMode: Int {
case aspectFill case aspectFill
case aspectFit case aspectFit
case widthFill case widthFill
case heightFill case heightFill
} }
@objc public enum Offset: Int { @objc public enum Offset: Int {
case beginning case beginning
case center case center
} }
static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2 static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2
@objc open var imageContentMode: ScaleMode = .widthFill @objc open var imageContentMode: ScaleMode = .widthFill
@objc open var initialOffset: Offset = .beginning @objc open var initialOffset: Offset = .beginning
@objc public private(set) var zoomView: UIImageView? = nil @objc public private(set) var zoomView: UIImageView?
@objc open weak var imageScrollViewDelegate: ImageScrollViewDelegate? @objc open weak var imageScrollViewDelegate: ImageScrollViewDelegate?
var imageSize: CGSize = CGSize.zero var imageSize: CGSize = CGSize.zero
private var pointToCenterAfterResize: CGPoint = CGPoint.zero private var pointToCenterAfterResize: CGPoint = CGPoint.zero
private var scaleToRestoreAfterResize: CGFloat = 1.0 private var scaleToRestoreAfterResize: CGFloat = 1.0
var maxScaleFromMinScale: CGFloat = 3.0 var maxScaleFromMinScale: CGFloat = 3.0
var zoomedFrame: CGRect { var zoomedFrame: CGRect {
return zoomView?.frame ?? CGRect.zero return zoomView?.frame ?? CGRect.zero
} }
override open var frame: CGRect { override open var frame: CGRect {
willSet { willSet {
if frame.equalTo(newValue) == false && newValue.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false { if frame.equalTo(newValue) == false && newValue.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false {
prepareToResize() prepareToResize()
} }
} }
didSet { didSet {
if frame.equalTo(oldValue) == false && frame.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false { if frame.equalTo(oldValue) == false && frame.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false {
recoverFromResizing() recoverFromResizing()
} }
} }
} }
override public init(frame: CGRect) { override public init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
initialize() initialize()
} }
required public init?(coder aDecoder: NSCoder) { required public init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder) super.init(coder: aDecoder)
initialize() initialize()
} }
private func initialize() { private func initialize() {
showsVerticalScrollIndicator = false showsVerticalScrollIndicator = false
showsHorizontalScrollIndicator = false showsHorizontalScrollIndicator = false
@ -78,135 +78,135 @@ open class ImageScrollView: UIScrollView {
decelerationRate = UIScrollView.DecelerationRate.fast decelerationRate = UIScrollView.DecelerationRate.fast
delegate = self delegate = self
} }
@objc public func adjustFrameToCenter() { @objc public func adjustFrameToCenter() {
guard let unwrappedZoomView = zoomView else { guard let unwrappedZoomView = zoomView else {
return return
} }
var frameToCenter = unwrappedZoomView.frame var frameToCenter = unwrappedZoomView.frame
// center horizontally // center horizontally
if frameToCenter.size.width < bounds.width { if frameToCenter.size.width < bounds.width {
frameToCenter.origin.x = (bounds.width - frameToCenter.size.width) / 2 frameToCenter.origin.x = (bounds.width - frameToCenter.size.width) / 2
} else { } else {
frameToCenter.origin.x = 0 frameToCenter.origin.x = 0
} }
// center vertically // center vertically
if frameToCenter.size.height < bounds.height { if frameToCenter.size.height < bounds.height {
frameToCenter.origin.y = (bounds.height - frameToCenter.size.height) / 2 frameToCenter.origin.y = (bounds.height - frameToCenter.size.height) / 2
} else { } else {
frameToCenter.origin.y = 0 frameToCenter.origin.y = 0
} }
unwrappedZoomView.frame = frameToCenter unwrappedZoomView.frame = frameToCenter
} }
private func prepareToResize() { private func prepareToResize() {
let boundsCenter = CGPoint(x: bounds.midX, y: bounds.midY) let boundsCenter = CGPoint(x: bounds.midX, y: bounds.midY)
pointToCenterAfterResize = convert(boundsCenter, to: zoomView) pointToCenterAfterResize = convert(boundsCenter, to: zoomView)
scaleToRestoreAfterResize = zoomScale scaleToRestoreAfterResize = zoomScale
// If we're at the minimum zoom scale, preserve that by returning 0, which will be converted to the minimum // If we're at the minimum zoom scale, preserve that by returning 0, which will be converted to the minimum
// allowable scale when the scale is restored. // allowable scale when the scale is restored.
if scaleToRestoreAfterResize <= minimumZoomScale + CGFloat(Float.ulpOfOne) { if scaleToRestoreAfterResize <= minimumZoomScale + CGFloat(Float.ulpOfOne) {
scaleToRestoreAfterResize = 0 scaleToRestoreAfterResize = 0
} }
} }
private func recoverFromResizing() { private func recoverFromResizing() {
setMaxMinZoomScalesForCurrentBounds() setMaxMinZoomScalesForCurrentBounds()
// restore zoom scale, first making sure it is within the allowable range. // restore zoom scale, first making sure it is within the allowable range.
let maxZoomScale = max(minimumZoomScale, scaleToRestoreAfterResize) let maxZoomScale = max(minimumZoomScale, scaleToRestoreAfterResize)
zoomScale = min(maximumZoomScale, maxZoomScale) zoomScale = min(maximumZoomScale, maxZoomScale)
// restore center point, first making sure it is within the allowable range. // restore center point, first making sure it is within the allowable range.
// convert our desired center point back to our own coordinate space // convert our desired center point back to our own coordinate space
let boundsCenter = convert(pointToCenterAfterResize, to: zoomView) let boundsCenter = convert(pointToCenterAfterResize, to: zoomView)
// calculate the content offset that would yield that center point // calculate the content offset that would yield that center point
var offset = CGPoint(x: boundsCenter.x - bounds.size.width/2.0, y: boundsCenter.y - bounds.size.height/2.0) var offset = CGPoint(x: boundsCenter.x - bounds.size.width/2.0, y: boundsCenter.y - bounds.size.height/2.0)
// restore offset, adjusted to be within the allowable range // restore offset, adjusted to be within the allowable range
let maxOffset = maximumContentOffset() let maxOffset = maximumContentOffset()
let minOffset = minimumContentOffset() let minOffset = minimumContentOffset()
var realMaxOffset = min(maxOffset.x, offset.x) var realMaxOffset = min(maxOffset.x, offset.x)
offset.x = max(minOffset.x, realMaxOffset) offset.x = max(minOffset.x, realMaxOffset)
realMaxOffset = min(maxOffset.y, offset.y) realMaxOffset = min(maxOffset.y, offset.y)
offset.y = max(minOffset.y, realMaxOffset) offset.y = max(minOffset.y, realMaxOffset)
contentOffset = offset contentOffset = offset
} }
private func maximumContentOffset() -> CGPoint { private func maximumContentOffset() -> CGPoint {
return CGPoint(x: contentSize.width - bounds.width,y:contentSize.height - bounds.height) return CGPoint(x: contentSize.width - bounds.width, y: contentSize.height - bounds.height)
} }
private func minimumContentOffset() -> CGPoint { private func minimumContentOffset() -> CGPoint {
return CGPoint.zero return CGPoint.zero
} }
// MARK: - Set up // MARK: - Set up
open func setup() { open func setup() {
var topSupperView = superview var topSupperView = superview
while topSupperView?.superview != nil { while topSupperView?.superview != nil {
topSupperView = topSupperView?.superview topSupperView = topSupperView?.superview
} }
// Make sure views have already layout with precise frame // Make sure views have already layout with precise frame
topSupperView?.layoutIfNeeded() topSupperView?.layoutIfNeeded()
} }
// MARK: - Display image // MARK: - Display image
@objc open func display(image: UIImage) { @objc open func display(image: UIImage) {
if let zoomView = zoomView { if let zoomView = zoomView {
zoomView.removeFromSuperview() zoomView.removeFromSuperview()
} }
zoomView = UIImageView(image: image) zoomView = UIImageView(image: image)
zoomView!.isUserInteractionEnabled = true zoomView!.isUserInteractionEnabled = true
addSubview(zoomView!) addSubview(zoomView!)
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doubleTapGestureRecognizer(_:))) let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doubleTapGestureRecognizer(_:)))
tapGesture.numberOfTapsRequired = 2 tapGesture.numberOfTapsRequired = 2
zoomView!.addGestureRecognizer(tapGesture) zoomView!.addGestureRecognizer(tapGesture)
let downSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeUpGestureRecognizer(_:))) let downSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeUpGestureRecognizer(_:)))
downSwipeGesture.direction = .down downSwipeGesture.direction = .down
zoomView!.addGestureRecognizer(downSwipeGesture) zoomView!.addGestureRecognizer(downSwipeGesture)
let upSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeDownGestureRecognizer(_:))) let upSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeDownGestureRecognizer(_:)))
upSwipeGesture.direction = .up upSwipeGesture.direction = .up
zoomView!.addGestureRecognizer(upSwipeGesture) zoomView!.addGestureRecognizer(upSwipeGesture)
configureImageForSize(image.size) configureImageForSize(image.size)
adjustFrameToCenter() adjustFrameToCenter()
} }
private func configureImageForSize(_ size: CGSize) { private func configureImageForSize(_ size: CGSize) {
imageSize = size imageSize = size
contentSize = imageSize contentSize = imageSize
setMaxMinZoomScalesForCurrentBounds() setMaxMinZoomScalesForCurrentBounds()
zoomScale = minimumZoomScale zoomScale = minimumZoomScale
switch initialOffset { switch initialOffset {
case .beginning: case .beginning:
contentOffset = CGPoint.zero contentOffset = CGPoint.zero
case .center: case .center:
let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2 let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2
let yOffset = contentSize.height < bounds.height ? 0 : (contentSize.height - bounds.height)/2 let yOffset = contentSize.height < bounds.height ? 0 : (contentSize.height - bounds.height)/2
switch imageContentMode { switch imageContentMode {
case .aspectFit: case .aspectFit:
contentOffset = CGPoint.zero contentOffset = CGPoint.zero
@ -219,14 +219,14 @@ open class ImageScrollView: UIScrollView {
} }
} }
} }
private func setMaxMinZoomScalesForCurrentBounds() { private func setMaxMinZoomScalesForCurrentBounds() {
// calculate min/max zoomscale // calculate min/max zoomscale
let xScale = bounds.width / imageSize.width // the scale needed to perfectly fit the image width-wise let xScale = bounds.width / imageSize.width // the scale needed to perfectly fit the image width-wise
let yScale = bounds.height / imageSize.height // the scale needed to perfectly fit the image height-wise let yScale = bounds.height / imageSize.height // the scale needed to perfectly fit the image height-wise
var minScale: CGFloat = 1 var minScale: CGFloat = 1
switch imageContentMode { switch imageContentMode {
case .aspectFill: case .aspectFill:
minScale = max(xScale, yScale) minScale = max(xScale, yScale)
@ -237,21 +237,20 @@ open class ImageScrollView: UIScrollView {
case .heightFill: case .heightFill:
minScale = yScale minScale = yScale
} }
let maxScale = maxScaleFromMinScale*minScale let maxScale = maxScaleFromMinScale*minScale
// don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.) // don't let minScale exceed maxScale. (If the image is smaller than the screen, we don't want to force it to be zoomed.)
if minScale > maxScale { if minScale > maxScale {
minScale = maxScale minScale = maxScale
} }
maximumZoomScale = maxScale maximumZoomScale = maxScale
minimumZoomScale = minScale // * 0.999 // the multiply factor to prevent user cannot scroll page while they use this control in UIPageViewController minimumZoomScale = minScale // * 0.999 // the multiply factor to prevent user cannot scroll page while they use this control in UIPageViewController
} }
// MARK: - Gesture // MARK: - Gesture
@objc func doubleTapGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { @objc func doubleTapGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
// zoom out if it bigger than middle scale point. Else, zoom in // zoom out if it bigger than middle scale point. Else, zoom in
if zoomScale >= maximumZoomScale / 2.0 { if zoomScale >= maximumZoomScale / 2.0 {
@ -262,96 +261,96 @@ open class ImageScrollView: UIScrollView {
zoom(to: zoomRect, animated: true) zoom(to: zoomRect, animated: true)
} }
} }
@objc func swipeUpGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { @objc func swipeUpGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.state == .ended { if gestureRecognizer.state == .ended {
imageScrollViewDelegate?.imageScrollViewDidGestureSwipeUp(imageScrollView: self) imageScrollViewDelegate?.imageScrollViewDidGestureSwipeUp(imageScrollView: self)
} }
} }
@objc func swipeDownGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) { @objc func swipeDownGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
if gestureRecognizer.state == .ended { if gestureRecognizer.state == .ended {
imageScrollViewDelegate?.imageScrollViewDidGestureSwipeDown(imageScrollView: self) imageScrollViewDelegate?.imageScrollViewDidGestureSwipeDown(imageScrollView: self)
} }
} }
private func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect { private func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect {
var zoomRect = CGRect.zero var zoomRect = CGRect.zero
// the zoom rect is in the content view's coordinates. // the zoom rect is in the content view's coordinates.
// at a zoom scale of 1.0, it would be the size of the imageScrollView's bounds. // at a zoom scale of 1.0, it would be the size of the imageScrollView's bounds.
// as the zoom scale decreases, so more content is visible, the size of the rect grows. // as the zoom scale decreases, so more content is visible, the size of the rect grows.
zoomRect.size.height = frame.size.height / scale zoomRect.size.height = frame.size.height / scale
zoomRect.size.width = frame.size.width / scale zoomRect.size.width = frame.size.width / scale
// choose an origin so as to get the right center. // choose an origin so as to get the right center.
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0) zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0)
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0) zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0)
return zoomRect return zoomRect
} }
open func refresh() { open func refresh() {
if let image = zoomView?.image { if let image = zoomView?.image {
display(image: image) display(image: image)
} }
} }
open func resize() { open func resize() {
self.configureImageForSize(self.imageSize) self.configureImageForSize(self.imageSize)
} }
} }
extension ImageScrollView: UIScrollViewDelegate { extension ImageScrollView: UIScrollViewDelegate {
public func scrollViewDidScroll(_ scrollView: UIScrollView) { public func scrollViewDidScroll(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewDidScroll?(scrollView) imageScrollViewDelegate?.scrollViewDidScroll?(scrollView)
} }
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) { public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewWillBeginDragging?(scrollView) imageScrollViewDelegate?.scrollViewWillBeginDragging?(scrollView)
} }
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) { public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
imageScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset) imageScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
} }
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) { public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
imageScrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate) imageScrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
} }
public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) { public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewWillBeginDecelerating?(scrollView) imageScrollViewDelegate?.scrollViewWillBeginDecelerating?(scrollView)
} }
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) { public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView) imageScrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView)
} }
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView) imageScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
} }
public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) { public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
imageScrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view) imageScrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view)
} }
public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) { public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
imageScrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale) imageScrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale)
} }
public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool { public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
return false return false
} }
public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) { public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
imageScrollViewDelegate?.scrollViewDidChangeAdjustedContentInset?(scrollView) imageScrollViewDelegate?.scrollViewDidChangeAdjustedContentInset?(scrollView)
} }
public func viewForZooming(in scrollView: UIScrollView) -> UIView? { public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
return zoomView return zoomView
} }
public func scrollViewDidZoom(_ scrollView: UIScrollView) { public func scrollViewDidZoom(_ scrollView: UIScrollView) {
adjustFrameToCenter() adjustFrameToCenter()
imageScrollViewDelegate?.scrollViewDidZoom?(scrollView) imageScrollViewDelegate?.scrollViewDidZoom?(scrollView)

View File

@ -16,15 +16,15 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
var originFrame: CGRect! var originFrame: CGRect!
var maskFrame: CGRect! var maskFrame: CGRect!
var originImage: UIImage! var originImage: UIImage!
init(controller: WebViewController) { init(controller: WebViewController) {
self.webViewController = controller self.webViewController = controller
} }
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval { func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
return duration return duration
} }
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) { func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
if presenting { if presenting {
animateTransitionPresenting(using: transitionContext) animateTransitionPresenting(using: transitionContext)
@ -32,23 +32,23 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
animateTransitionReturning(using: transitionContext) animateTransitionReturning(using: transitionContext)
} }
} }
private func animateTransitionPresenting(using transitionContext: UIViewControllerContextTransitioning) { private func animateTransitionPresenting(using transitionContext: UIViewControllerContextTransitioning) {
let imageView = UIImageView(image: originImage) let imageView = UIImageView(image: originImage)
imageView.frame = originFrame imageView.frame = originFrame
let fromView = transitionContext.view(forKey: .from)! let fromView = transitionContext.view(forKey: .from)!
fromView.removeFromSuperview() fromView.removeFromSuperview()
transitionContext.containerView.backgroundColor = AppAssets.fullScreenBackgroundColor transitionContext.containerView.backgroundColor = AppAssets.fullScreenBackgroundColor
transitionContext.containerView.addSubview(imageView) transitionContext.containerView.addSubview(imageView)
webViewController?.hideClickedImage() webViewController?.hideClickedImage()
UIView.animate( UIView.animate(
withDuration: duration, withDuration: duration,
delay:0.0, delay: 0.0,
usingSpringWithDamping: 0.8, usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.2, initialSpringVelocity: 0.2,
animations: { animations: {
@ -61,40 +61,40 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
transitionContext.completeTransition(true) transitionContext.completeTransition(true)
}) })
} }
private func animateTransitionReturning(using transitionContext: UIViewControllerContextTransitioning) { private func animateTransitionReturning(using transitionContext: UIViewControllerContextTransitioning) {
let imageController = transitionContext.viewController(forKey: .from) as! ImageViewController let imageController = transitionContext.viewController(forKey: .from) as! ImageViewController
let imageView = UIImageView(image: originImage) let imageView = UIImageView(image: originImage)
imageView.frame = imageController.zoomedFrame imageView.frame = imageController.zoomedFrame
let fromView = transitionContext.view(forKey: .from)! let fromView = transitionContext.view(forKey: .from)!
let windowFrame = fromView.window!.frame let windowFrame = fromView.window!.frame
fromView.removeFromSuperview() fromView.removeFromSuperview()
let toView = transitionContext.view(forKey: .to)! let toView = transitionContext.view(forKey: .to)!
transitionContext.containerView.addSubview(toView) transitionContext.containerView.addSubview(toView)
let maskingView = UIView() let maskingView = UIView()
let fullMaskFrame = CGRect(x: windowFrame.minX, y: maskFrame.minY, width: windowFrame.width, height: maskFrame.height) let fullMaskFrame = CGRect(x: windowFrame.minX, y: maskFrame.minY, width: windowFrame.width, height: maskFrame.height)
let path = UIBezierPath(rect: fullMaskFrame) let path = UIBezierPath(rect: fullMaskFrame)
let maskLayer = CAShapeLayer() let maskLayer = CAShapeLayer()
maskLayer.path = path.cgPath maskLayer.path = path.cgPath
maskingView.layer.mask = maskLayer maskingView.layer.mask = maskLayer
maskingView.addSubview(imageView) maskingView.addSubview(imageView)
transitionContext.containerView.addSubview(maskingView) transitionContext.containerView.addSubview(maskingView)
UIView.animate( UIView.animate(
withDuration: duration, withDuration: duration,
delay:0.0, delay: 0.0,
usingSpringWithDamping: 0.8, usingSpringWithDamping: 0.8,
initialSpringVelocity: 0.2, initialSpringVelocity: 0.2,
animations: { animations: {
imageView.frame = self.originFrame imageView.frame = self.originFrame
}, completion: { _ in }, completion: { _ in
if let controller = self.webViewController { if let controller = self.webViewController {
controller.showClickedImage() { controller.showClickedImage {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
imageView.removeFromSuperview() imageView.removeFromSuperview()
transitionContext.completeTransition(true) transitionContext.completeTransition(true)
@ -106,5 +106,5 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
} }
}) })
} }
} }

View File

@ -17,7 +17,7 @@ final class ImageViewController: UIViewController {
@IBOutlet weak var titleBackground: UIVisualEffectView! @IBOutlet weak var titleBackground: UIVisualEffectView!
@IBOutlet weak var titleLeading: NSLayoutConstraint! @IBOutlet weak var titleLeading: NSLayoutConstraint!
@IBOutlet weak var titleTrailing: NSLayoutConstraint! @IBOutlet weak var titleTrailing: NSLayoutConstraint!
var image: UIImage! var image: UIImage!
var imageTitle: String? var imageTitle: String?
var zoomedFrame: CGRect { var zoomedFrame: CGRect {
@ -27,27 +27,27 @@ final class ImageViewController: UIViewController {
init() { init() {
super.init(nibName: "ImageViewController", bundle: nil) super.init(nibName: "ImageViewController", bundle: nil)
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
closeButton.imageView?.contentMode = .scaleAspectFit closeButton.imageView?.contentMode = .scaleAspectFit
closeButton.accessibilityLabel = NSLocalizedString("Close", comment: "Close") closeButton.accessibilityLabel = NSLocalizedString("Close", comment: "Close")
shareButton.accessibilityLabel = NSLocalizedString("Share", comment: "Share") shareButton.accessibilityLabel = NSLocalizedString("Share", comment: "Share")
imageScrollView.setup() imageScrollView.setup()
imageScrollView.imageScrollViewDelegate = self imageScrollView.imageScrollViewDelegate = self
imageScrollView.imageContentMode = .aspectFit imageScrollView.imageContentMode = .aspectFit
imageScrollView.initialOffset = .center imageScrollView.initialOffset = .center
imageScrollView.display(image: image) imageScrollView.display(image: image)
titleLabel.text = imageTitle ?? "" titleLabel.text = imageTitle ?? ""
layoutTitleLabel() layoutTitleLabel()
guard imageTitle != "" else { guard imageTitle != "" else {
titleBackground.removeFromSuperview() titleBackground.removeFromSuperview()
return return
@ -57,11 +57,11 @@ final class ImageViewController: UIViewController {
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) { override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
super.viewWillTransition(to: size, with: coordinator) super.viewWillTransition(to: size, with: coordinator)
coordinator.animate(alongsideTransition: { [weak self] context in coordinator.animate(alongsideTransition: { [weak self] _ in
self?.imageScrollView.resize() self?.imageScrollView.resize()
}) })
} }
@IBAction func share(_ sender: Any) { @IBAction func share(_ sender: Any) {
guard let image = image else { return } guard let image = image else { return }
let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: nil) let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: nil)
@ -69,12 +69,12 @@ final class ImageViewController: UIViewController {
activityViewController.popoverPresentationController?.sourceRect = shareButton.bounds activityViewController.popoverPresentationController?.sourceRect = shareButton.bounds
present(activityViewController, animated: true) present(activityViewController, animated: true)
} }
@IBAction func done(_ sender: Any) { @IBAction func done(_ sender: Any) {
dismiss(animated: true) dismiss(animated: true)
} }
private func layoutTitleLabel(){ private func layoutTitleLabel() {
let width = view.frame.width let width = view.frame.width
let multiplier = UIDevice.current.userInterfaceIdiom == .pad ? CGFloat(0.1) : CGFloat(0.04) let multiplier = UIDevice.current.userInterfaceIdiom == .pad ? CGFloat(0.1) : CGFloat(0.04)
titleLeading.constant += width * multiplier titleLeading.constant += width * multiplier
@ -90,10 +90,9 @@ extension ImageViewController: ImageScrollViewDelegate {
func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView) { func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView) {
dismiss(animated: true) dismiss(animated: true)
} }
func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView) { func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView) {
dismiss(animated: true) dismiss(animated: true)
} }
} }

View File

@ -9,17 +9,17 @@
import UIKit import UIKit
class OpenInBrowserActivity: UIActivity { class OpenInBrowserActivity: UIActivity {
private var activityItems: [Any]? private var activityItems: [Any]?
override var activityTitle: String? { override var activityTitle: String? {
return NSLocalizedString("Open in Browser", comment: "Open in Browser") return NSLocalizedString("Open in Browser", comment: "Open in Browser")
} }
override var activityImage: UIImage? { override var activityImage: UIImage? {
return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular)) return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))
} }
override var activityType: UIActivity.ActivityType? { override var activityType: UIActivity.ActivityType? {
return UIActivity.ActivityType(rawValue: "com.rancharo.NetNewsWire-Evergreen.safari") return UIActivity.ActivityType(rawValue: "com.rancharo.NetNewsWire-Evergreen.safari")
} }
@ -27,23 +27,23 @@ class OpenInBrowserActivity: UIActivity {
override class var activityCategory: UIActivity.Category { override class var activityCategory: UIActivity.Category {
return .action return .action
} }
override func canPerform(withActivityItems activityItems: [Any]) -> Bool { override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
return true return true
} }
override func prepare(withActivityItems activityItems: [Any]) { override func prepare(withActivityItems activityItems: [Any]) {
self.activityItems = activityItems self.activityItems = activityItems
} }
override func perform() { override func perform() {
guard let url = activityItems?.first(where: { $0 is URL }) as? URL else { guard let url = activityItems?.first(where: { $0 is URL }) as? URL else {
activityDidFinish(false) activityDidFinish(false)
return return
} }
UIApplication.shared.open(url, options: [:], completionHandler: nil) UIApplication.shared.open(url, options: [:], completionHandler: nil)
activityDidFinish(true) activityDidFinish(true)
} }
} }

View File

@ -30,7 +30,7 @@ final class WebViewController: UIViewController {
private var bottomShowBarsView: UIView! private var bottomShowBarsView: UIView!
private var topShowBarsViewConstraint: NSLayoutConstraint! private var topShowBarsViewConstraint: NSLayoutConstraint!
private var bottomShowBarsViewConstraint: NSLayoutConstraint! private var bottomShowBarsViewConstraint: NSLayoutConstraint!
var webView: WKWebView? { var webView: WKWebView? {
return view.subviews[0] as? WKWebView return view.subviews[0] as? WKWebView
} }
@ -43,7 +43,7 @@ final class WebViewController: UIViewController {
private lazy var transition = ImageTransition(controller: self) private lazy var transition = ImageTransition(controller: self)
private var clickedImageCompletion: (() -> Void)? private var clickedImageCompletion: (() -> Void)?
private var articleExtractor: ArticleExtractor? = nil private var articleExtractor: ArticleExtractor?
var extractedArticle: ExtractedArticle? { var extractedArticle: ExtractedArticle? {
didSet { didSet {
windowScrollY = 0 windowScrollY = 0
@ -56,12 +56,12 @@ final class WebViewController: UIViewController {
delegate?.webViewController(self, articleExtractorButtonStateDidUpdate: articleExtractorButtonState) delegate?.webViewController(self, articleExtractorButtonStateDidUpdate: articleExtractorButtonState)
} }
} }
weak var coordinator: SceneCoordinator! weak var coordinator: SceneCoordinator!
weak var delegate: WebViewControllerDelegate? weak var delegate: WebViewControllerDelegate?
private(set) var article: Article? private(set) var article: Article?
let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 0.3) let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 0.3)
var windowScrollY = 0 var windowScrollY = 0
private var restoreWindowScrollY: Int? private var restoreWindowScrollY: Int?
@ -77,13 +77,13 @@ final class WebViewController: UIViewController {
// Configure the tap zones // Configure the tap zones
configureTopShowBarsView() configureTopShowBarsView()
configureBottomShowBarsView() configureBottomShowBarsView()
loadWebView() loadWebView()
} }
// MARK: Notifications // MARK: Notifications
@objc func feedIconDidBecomeAvailable(_ note: Notification) { @objc func feedIconDidBecomeAvailable(_ note: Notification) {
reloadArticleImage() reloadArticleImage()
} }
@ -101,16 +101,16 @@ final class WebViewController: UIViewController {
} }
// MARK: Actions // MARK: Actions
@objc func showBars(_ sender: Any) { @objc func showBars(_ sender: Any) {
showBars() showBars()
} }
// MARK: API // MARK: API
func setArticle(_ article: Article?, updateView: Bool = true) { func setArticle(_ article: Article?, updateView: Bool = true) {
stopArticleExtractor() stopArticleExtractor()
if article != self.article { if article != self.article {
self.article = article self.article = article
if updateView { if updateView {
@ -121,9 +121,9 @@ final class WebViewController: UIViewController {
loadWebView() loadWebView()
} }
} }
} }
func setScrollPosition(isShowingExtractedArticle: Bool, articleWindowScrollY: Int) { func setScrollPosition(isShowingExtractedArticle: Bool, articleWindowScrollY: Int) {
if isShowingExtractedArticle { if isShowingExtractedArticle {
switch articleExtractor?.state { switch articleExtractor?.state {
@ -144,7 +144,7 @@ final class WebViewController: UIViewController {
loadWebView() loadWebView()
} }
} }
func focus() { func focus() {
webView?.becomeFirstResponder() webView?.becomeFirstResponder()
} }
@ -164,7 +164,7 @@ final class WebViewController: UIViewController {
let overlap = 2 * UIFont.systemFont(ofSize: UIFont.systemFontSize).lineHeight * UIScreen.main.scale let overlap = 2 * UIFont.systemFont(ofSize: UIFont.systemFontSize).lineHeight * UIScreen.main.scale
let scrollToY: CGFloat = { let scrollToY: CGFloat = {
let scrollDistance = webView.scrollView.layoutMarginsGuide.layoutFrame.height - overlap; let scrollDistance = webView.scrollView.layoutMarginsGuide.layoutFrame.height - overlap
let fullScroll = webView.scrollView.contentOffset.y + (scrollingUp ? -scrollDistance : scrollDistance) let fullScroll = webView.scrollView.contentOffset.y + (scrollingUp ? -scrollDistance : scrollDistance)
let final = finalScrollPosition(scrollingUp: scrollingUp) let final = finalScrollPosition(scrollingUp: scrollingUp)
return (scrollingUp ? fullScroll > final : fullScroll < final) ? fullScroll : final return (scrollingUp ? fullScroll > final : fullScroll < final) ? fullScroll : final
@ -186,12 +186,12 @@ final class WebViewController: UIViewController {
func hideClickedImage() { func hideClickedImage() {
webView?.evaluateJavaScript("hideClickedImage();") webView?.evaluateJavaScript("hideClickedImage();")
} }
func showClickedImage(completion: @escaping () -> Void) { func showClickedImage(completion: @escaping () -> Void) {
clickedImageCompletion = completion clickedImageCompletion = completion
webView?.evaluateJavaScript("showClickedImage();") webView?.evaluateJavaScript("showClickedImage();")
} }
func fullReload() { func fullReload() {
loadWebView(replaceExistingWebView: true) loadWebView(replaceExistingWebView: true)
} }
@ -205,7 +205,7 @@ final class WebViewController: UIViewController {
navigationController?.setToolbarHidden(false, animated: true) navigationController?.setToolbarHidden(false, animated: true)
configureContextMenuInteraction() configureContextMenuInteraction()
} }
func hideBars() { func hideBars() {
if isFullScreenAvailable { if isFullScreenAvailable {
AppDefaults.shared.articleFullscreenEnabled = true AppDefaults.shared.articleFullscreenEnabled = true
@ -248,7 +248,7 @@ final class WebViewController: UIViewController {
} }
} }
func stopArticleExtractorIfProcessing() { func stopArticleExtractorIfProcessing() {
if articleExtractor?.state == .processing { if articleExtractor?.state == .processing {
stopArticleExtractor() stopArticleExtractor()
@ -261,7 +261,7 @@ final class WebViewController: UIViewController {
cancelImageLoad(webView) cancelImageLoad(webView)
} }
} }
func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) { func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) {
guard let url = article?.preferredURL else { return } guard let url = article?.preferredURL else { return }
let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()]) let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()])
@ -325,12 +325,12 @@ extension WebViewController: ArticleExtractorDelegate {
extension WebViewController: UIContextMenuInteractionDelegate { extension WebViewController: UIContextMenuInteractionDelegate {
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? {
return UIContextMenuConfiguration(identifier: nil, previewProvider: contextMenuPreviewProvider) { [weak self] suggestedActions in return UIContextMenuConfiguration(identifier: nil, previewProvider: contextMenuPreviewProvider) { [weak self] _ in
guard let self = self else { return nil } guard let self = self else { return nil }
var menus = [UIMenu]() var menus = [UIMenu]()
var navActions = [UIAction]() var navActions = [UIAction]()
if let action = self.prevArticleAction() { if let action = self.prevArticleAction() {
navActions.append(action) navActions.append(action)
@ -341,7 +341,7 @@ extension WebViewController: UIContextMenuInteractionDelegate {
if !navActions.isEmpty { if !navActions.isEmpty {
menus.append(UIMenu(title: "", options: .displayInline, children: navActions)) menus.append(UIMenu(title: "", options: .displayInline, children: navActions))
} }
var toggleActions = [UIAction]() var toggleActions = [UIAction]()
if let action = self.toggleReadAction() { if let action = self.toggleReadAction() {
toggleActions.append(action) toggleActions.append(action)
@ -355,29 +355,29 @@ extension WebViewController: UIContextMenuInteractionDelegate {
menus.append(UIMenu(title: "", options: .displayInline, children: [self.toggleArticleExtractorAction()])) menus.append(UIMenu(title: "", options: .displayInline, children: [self.toggleArticleExtractorAction()]))
menus.append(UIMenu(title: "", options: .displayInline, children: [self.shareAction()])) menus.append(UIMenu(title: "", options: .displayInline, children: [self.shareAction()]))
return UIMenu(title: "", children: menus) return UIMenu(title: "", children: menus)
} }
} }
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
coordinator.showBrowserForCurrentArticle() coordinator.showBrowserForCurrentArticle()
} }
} }
// MARK: WKNavigationDelegate // MARK: WKNavigationDelegate
extension WebViewController: WKNavigationDelegate { extension WebViewController: WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
if navigationAction.navigationType == .linkActivated { if navigationAction.navigationType == .linkActivated {
guard let url = navigationAction.request.url else { guard let url = navigationAction.request.url else {
decisionHandler(.allow) decisionHandler(.allow)
return return
} }
let components = URLComponents(url: url, resolvingAgainstBaseURL: false) let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
if components?.scheme == "http" || components?.scheme == "https" { if components?.scheme == "http" || components?.scheme == "https" {
decisionHandler(.cancel) decisionHandler(.cancel)
@ -391,16 +391,16 @@ extension WebViewController: WKNavigationDelegate {
self.openURLInSafariViewController(url) self.openURLInSafariViewController(url)
} }
} }
} else if components?.scheme == "mailto" { } else if components?.scheme == "mailto" {
decisionHandler(.cancel) decisionHandler(.cancel)
guard let emailAddress = url.percentEncodedEmailAddress else { guard let emailAddress = url.percentEncodedEmailAddress else {
return return
} }
if UIApplication.shared.canOpenURL(emailAddress) { if UIApplication.shared.canOpenURL(emailAddress) {
UIApplication.shared.open(emailAddress, options: [.universalLinksOnly : false], completionHandler: nil) UIApplication.shared.open(emailAddress, options: [.universalLinksOnly: false], completionHandler: nil)
} else { } else {
let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"), message: NSLocalizedString("This device cannot send emails.", comment: "This device cannot send emails."), preferredStyle: .alert) let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"), message: NSLocalizedString("This device cannot send emails.", comment: "This device cannot send emails."), preferredStyle: .alert)
alert.addAction(.init(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil)) alert.addAction(.init(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil))
@ -408,11 +408,11 @@ extension WebViewController: WKNavigationDelegate {
} }
} else if components?.scheme == "tel" { } else if components?.scheme == "tel" {
decisionHandler(.cancel) decisionHandler(.cancel)
if UIApplication.shared.canOpenURL(url) { if UIApplication.shared.canOpenURL(url) {
UIApplication.shared.open(url, options: [.universalLinksOnly : false], completionHandler: nil) UIApplication.shared.open(url, options: [.universalLinksOnly: false], completionHandler: nil)
} }
} else { } else {
decisionHandler(.allow) decisionHandler(.allow)
} }
@ -424,13 +424,13 @@ extension WebViewController: WKNavigationDelegate {
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) { func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
fullReload() fullReload()
} }
} }
// MARK: WKUIDelegate // MARK: WKUIDelegate
extension WebViewController: WKUIDelegate { extension WebViewController: WKUIDelegate {
func webView(_ webView: WKWebView, contextMenuForElement elementInfo: WKContextMenuElementInfo, willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) { func webView(_ webView: WKWebView, contextMenuForElement elementInfo: WKContextMenuElementInfo, willCommitWithAnimator animator: UIContextMenuInteractionCommitAnimating) {
// We need to have at least an unimplemented WKUIDelegate assigned to the WKWebView. This makes the // We need to have at least an unimplemented WKUIDelegate assigned to the WKWebView. This makes the
// link preview launch Safari when the link preview is tapped. In theory, you should be able to get // link preview launch Safari when the link preview is tapped. In theory, you should be able to get
@ -442,11 +442,11 @@ extension WebViewController: WKUIDelegate {
guard let url = navigationAction.request.url else { guard let url = navigationAction.request.url else {
return nil return nil
} }
openURL(url) openURL(url)
return nil return nil
} }
} }
// MARK: WKScriptMessageHandler // MARK: WKScriptMessageHandler
@ -467,7 +467,7 @@ extension WebViewController: WKScriptMessageHandler {
return return
} }
} }
} }
// MARK: UIViewControllerTransitioningDelegate // MARK: UIViewControllerTransitioningDelegate
@ -478,7 +478,7 @@ extension WebViewController: UIViewControllerTransitioningDelegate {
transition.presenting = true transition.presenting = true
return transition return transition
} }
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? { func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
transition.presenting = false transition.presenting = false
return transition return transition
@ -488,11 +488,11 @@ extension WebViewController: UIViewControllerTransitioningDelegate {
// MARK: // MARK:
extension WebViewController: UIScrollViewDelegate { extension WebViewController: UIScrollViewDelegate {
func scrollViewDidScroll(_ scrollView: UIScrollView) { func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
} }
@objc func scrollPositionDidChange() { @objc func scrollPositionDidChange() {
webView?.evaluateJavaScript("window.scrollY") { (scrollY, error) in webView?.evaluateJavaScript("window.scrollY") { (scrollY, error) in
guard error == nil else { return } guard error == nil else { return }
@ -502,11 +502,9 @@ extension WebViewController: UIScrollViewDelegate {
self.windowScrollY = javascriptScrollY self.windowScrollY = javascriptScrollY
} }
} }
} }
// MARK: JSON // MARK: JSON
private struct ImageClickMessage: Codable { private struct ImageClickMessage: Codable {
@ -564,7 +562,7 @@ private extension WebViewController {
func renderPage(_ webView: WKWebView?) { func renderPage(_ webView: WKWebView?) {
guard let webView = webView else { return } guard let webView = webView else { return }
let theme = ArticleThemesManager.shared.currentTheme let theme = ArticleThemesManager.shared.currentTheme
let rendering: ArticleRenderer.Rendering let rendering: ArticleRenderer.Rendering
@ -583,7 +581,7 @@ private extension WebViewController {
} else { } else {
rendering = ArticleRenderer.noSelectionHTML(theme: theme) rendering = ArticleRenderer.noSelectionHTML(theme: theme)
} }
let substitutions = [ let substitutions = [
"title": rendering.title, "title": rendering.title,
"baseURL": rendering.baseURL, "baseURL": rendering.baseURL,
@ -595,7 +593,7 @@ private extension WebViewController {
let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions) let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions)
webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL)) webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL))
} }
func finalScrollPosition(scrollingUp: Bool) -> CGFloat { func finalScrollPosition(scrollingUp: Bool) -> CGFloat {
guard let webView = webView else { return 0 } guard let webView = webView else { return 0 }
@ -605,7 +603,7 @@ private extension WebViewController {
return webView.scrollView.contentSize.height - webView.scrollView.bounds.height + webView.scrollView.safeAreaInsets.bottom return webView.scrollView.contentSize.height - webView.scrollView.bounds.height + webView.scrollView.safeAreaInsets.bottom
} }
} }
func startArticleExtractor() { func startArticleExtractor() {
guard articleExtractor == nil else { return } guard articleExtractor == nil else { return }
if let link = article?.preferredLink, let extractor = ArticleExtractor(link) { if let link = article?.preferredLink, let extractor = ArticleExtractor(link) {
@ -629,12 +627,12 @@ private extension WebViewController {
var components = URLComponents() var components = URLComponents()
components.scheme = ArticleRenderer.imageIconScheme components.scheme = ArticleRenderer.imageIconScheme
components.path = article.articleID components.path = article.articleID
if let imageSrc = components.string { if let imageSrc = components.string {
webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")") webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")")
} }
} }
func imageWasClicked(body: String?) { func imageWasClicked(body: String?) {
guard let webView = webView, guard let webView = webView,
let body = body, let body = body,
@ -642,22 +640,22 @@ private extension WebViewController {
let clickMessage = try? JSONDecoder().decode(ImageClickMessage.self, from: data), let clickMessage = try? JSONDecoder().decode(ImageClickMessage.self, from: data),
let range = clickMessage.imageURL.range(of: ";base64,") let range = clickMessage.imageURL.range(of: ";base64,")
else { return } else { return }
let base64Image = String(clickMessage.imageURL.suffix(from: range.upperBound)) let base64Image = String(clickMessage.imageURL.suffix(from: range.upperBound))
if let imageData = Data(base64Encoded: base64Image), let image = UIImage(data: imageData) { if let imageData = Data(base64Encoded: base64Image), let image = UIImage(data: imageData) {
let y = CGFloat(clickMessage.y) + webView.safeAreaInsets.top let y = CGFloat(clickMessage.y) + webView.safeAreaInsets.top
let rect = CGRect(x: CGFloat(clickMessage.x), y: y, width: CGFloat(clickMessage.width), height: CGFloat(clickMessage.height)) let rect = CGRect(x: CGFloat(clickMessage.x), y: y, width: CGFloat(clickMessage.width), height: CGFloat(clickMessage.height))
transition.originFrame = webView.convert(rect, to: nil) transition.originFrame = webView.convert(rect, to: nil)
if navigationController?.navigationBar.isHidden ?? false { if navigationController?.navigationBar.isHidden ?? false {
transition.maskFrame = webView.convert(webView.frame, to: nil) transition.maskFrame = webView.convert(webView.frame, to: nil)
} else { } else {
transition.maskFrame = webView.convert(webView.safeAreaLayoutGuide.layoutFrame, to: nil) transition.maskFrame = webView.convert(webView.safeAreaLayoutGuide.layoutFrame, to: nil)
} }
transition.originImage = image transition.originImage = image
coordinator.showFullScreenImage(image: image, imageTitle: clickMessage.imageTitle, transitioningDelegate: self) coordinator.showFullScreenImage(image: image, imageTitle: clickMessage.imageTitle, transitioningDelegate: self)
} }
} }
@ -675,13 +673,13 @@ private extension WebViewController {
topShowBarsView.backgroundColor = .clear topShowBarsView.backgroundColor = .clear
topShowBarsView.translatesAutoresizingMaskIntoConstraints = false topShowBarsView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(topShowBarsView) view.addSubview(topShowBarsView)
if AppDefaults.shared.logicalArticleFullscreenEnabled { if AppDefaults.shared.logicalArticleFullscreenEnabled {
topShowBarsViewConstraint = view.topAnchor.constraint(equalTo: topShowBarsView.bottomAnchor, constant: -44.0) topShowBarsViewConstraint = view.topAnchor.constraint(equalTo: topShowBarsView.bottomAnchor, constant: -44.0)
} else { } else {
topShowBarsViewConstraint = view.topAnchor.constraint(equalTo: topShowBarsView.bottomAnchor, constant: 0.0) topShowBarsViewConstraint = view.topAnchor.constraint(equalTo: topShowBarsView.bottomAnchor, constant: 0.0)
} }
NSLayoutConstraint.activate([ NSLayoutConstraint.activate([
topShowBarsViewConstraint, topShowBarsViewConstraint,
view.leadingAnchor.constraint(equalTo: topShowBarsView.leadingAnchor), view.leadingAnchor.constraint(equalTo: topShowBarsView.leadingAnchor),
@ -690,7 +688,7 @@ private extension WebViewController {
]) ])
topShowBarsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBars(_:)))) topShowBarsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBars(_:))))
} }
func configureBottomShowBarsView() { func configureBottomShowBarsView() {
bottomShowBarsView = UIView() bottomShowBarsView = UIView()
topShowBarsView.backgroundColor = .clear topShowBarsView.backgroundColor = .clear
@ -709,7 +707,7 @@ private extension WebViewController {
]) ])
bottomShowBarsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBars(_:)))) bottomShowBarsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBars(_:))))
} }
func configureContextMenuInteraction() { func configureContextMenuInteraction() {
if isFullScreenAvailable { if isFullScreenAvailable {
if navigationController?.isNavigationBarHidden ?? false { if navigationController?.isNavigationBarHidden ?? false {
@ -719,33 +717,33 @@ private extension WebViewController {
} }
} }
} }
func contextMenuPreviewProvider() -> UIViewController { func contextMenuPreviewProvider() -> UIViewController {
ContextMenuPreviewViewController(article: article) ContextMenuPreviewViewController(article: article)
} }
func prevArticleAction() -> UIAction? { func prevArticleAction() -> UIAction? {
guard coordinator.isPrevArticleAvailable else { return nil } guard coordinator.isPrevArticleAvailable else { return nil }
let title = NSLocalizedString("Previous Article", comment: "Previous Article") let title = NSLocalizedString("Previous Article", comment: "Previous Article")
return UIAction(title: title, image: AppAssets.prevArticleImage) { [weak self] action in return UIAction(title: title, image: AppAssets.prevArticleImage) { [weak self] _ in
self?.coordinator.selectPrevArticle() self?.coordinator.selectPrevArticle()
} }
} }
func nextArticleAction() -> UIAction? { func nextArticleAction() -> UIAction? {
guard coordinator.isNextArticleAvailable else { return nil } guard coordinator.isNextArticleAvailable else { return nil }
let title = NSLocalizedString("Next Article", comment: "Next Article") let title = NSLocalizedString("Next Article", comment: "Next Article")
return UIAction(title: title, image: AppAssets.nextArticleImage) { [weak self] action in return UIAction(title: title, image: AppAssets.nextArticleImage) { [weak self] _ in
self?.coordinator.selectNextArticle() self?.coordinator.selectNextArticle()
} }
} }
func toggleReadAction() -> UIAction? { func toggleReadAction() -> UIAction? {
guard let article = article, !article.status.read || article.isAvailableToMarkUnread else { return nil } guard let article = article, !article.status.read || article.isAvailableToMarkUnread else { return nil }
let title = article.status.read ? NSLocalizedString("Mark as Unread", comment: "Mark as Unread") : NSLocalizedString("Mark as Read", comment: "Mark as Read") let title = article.status.read ? NSLocalizedString("Mark as Unread", comment: "Mark as Unread") : NSLocalizedString("Mark as Read", comment: "Mark as Read")
let readImage = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage let readImage = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage
return UIAction(title: title, image: readImage) { [weak self] action in return UIAction(title: title, image: readImage) { [weak self] _ in
self?.coordinator.toggleReadForCurrentArticle() self?.coordinator.toggleReadForCurrentArticle()
} }
} }
@ -754,7 +752,7 @@ private extension WebViewController {
let starred = article?.status.starred ?? false let starred = article?.status.starred ?? false
let title = starred ? NSLocalizedString("Mark as Unstarred", comment: "Mark as Unstarred") : NSLocalizedString("Mark as Starred", comment: "Mark as Starred") let title = starred ? NSLocalizedString("Mark as Unstarred", comment: "Mark as Unstarred") : NSLocalizedString("Mark as Starred", comment: "Mark as Starred")
let starredImage = starred ? AppAssets.starOpenImage : AppAssets.starClosedImage let starredImage = starred ? AppAssets.starOpenImage : AppAssets.starClosedImage
return UIAction(title: title, image: starredImage) { [weak self] action in return UIAction(title: title, image: starredImage) { [weak self] _ in
self?.coordinator.toggleStarredForCurrentArticle() self?.coordinator.toggleStarredForCurrentArticle()
} }
} }
@ -762,23 +760,23 @@ private extension WebViewController {
func nextUnreadArticleAction() -> UIAction? { func nextUnreadArticleAction() -> UIAction? {
guard coordinator.isAnyUnreadAvailable else { return nil } guard coordinator.isAnyUnreadAvailable else { return nil }
let title = NSLocalizedString("Next Unread Article", comment: "Next Unread Article") let title = NSLocalizedString("Next Unread Article", comment: "Next Unread Article")
return UIAction(title: title, image: AppAssets.nextUnreadArticleImage) { [weak self] action in return UIAction(title: title, image: AppAssets.nextUnreadArticleImage) { [weak self] _ in
self?.coordinator.selectNextUnread() self?.coordinator.selectNextUnread()
} }
} }
func toggleArticleExtractorAction() -> UIAction { func toggleArticleExtractorAction() -> UIAction {
let extracted = articleExtractorButtonState == .on let extracted = articleExtractorButtonState == .on
let title = extracted ? NSLocalizedString("Show Feed Article", comment: "Show Feed Article") : NSLocalizedString("Show Reader View", comment: "Show Reader View") let title = extracted ? NSLocalizedString("Show Feed Article", comment: "Show Feed Article") : NSLocalizedString("Show Reader View", comment: "Show Reader View")
let extractorImage = extracted ? AppAssets.articleExtractorOffSF : AppAssets.articleExtractorOnSF let extractorImage = extracted ? AppAssets.articleExtractorOffSF : AppAssets.articleExtractorOnSF
return UIAction(title: title, image: extractorImage) { [weak self] action in return UIAction(title: title, image: extractorImage) { [weak self] _ in
self?.toggleArticleExtractor() self?.toggleArticleExtractor()
} }
} }
func shareAction() -> UIAction { func shareAction() -> UIAction {
let title = NSLocalizedString("Share", comment: "Share") let title = NSLocalizedString("Share", comment: "Share")
return UIAction(title: title, image: AppAssets.shareImage) { [weak self] action in return UIAction(title: title, image: AppAssets.shareImage) { [weak self] _ in
self?.showActivityDialog() self?.showActivityDialog()
} }
} }
@ -816,27 +814,27 @@ internal struct FindInArticleState: Codable {
let width: Double let width: Double
let height: Double let height: Double
} }
struct FindInArticleResult: Codable { struct FindInArticleResult: Codable {
let rects: [WebViewClientRect] let rects: [WebViewClientRect]
let bounds: WebViewClientRect let bounds: WebViewClientRect
let index: UInt let index: UInt
let matchGroups: [String] let matchGroups: [String]
} }
let index: UInt? let index: UInt?
let results: [FindInArticleResult] let results: [FindInArticleResult]
let count: UInt let count: UInt
} }
extension WebViewController { extension WebViewController {
func searchText(_ searchText: String, completionHandler: @escaping (FindInArticleState) -> Void) { func searchText(_ searchText: String, completionHandler: @escaping (FindInArticleState) -> Void) {
guard let json = try? JSONEncoder().encode(FindInArticleOptions(text: searchText)) else { guard let json = try? JSONEncoder().encode(FindInArticleOptions(text: searchText)) else {
return return
} }
let encoded = json.base64EncodedString() let encoded = json.base64EncodedString()
webView?.evaluateJavaScript("updateFind(\"\(encoded)\")") { webView?.evaluateJavaScript("updateFind(\"\(encoded)\")") {
(result, error) in (result, error) in
guard error == nil, guard error == nil,
@ -845,21 +843,21 @@ extension WebViewController {
let findState = try? JSONDecoder().decode(FindInArticleState.self, from: rawData) else { let findState = try? JSONDecoder().decode(FindInArticleState.self, from: rawData) else {
return return
} }
completionHandler(findState) completionHandler(findState)
} }
} }
func endSearch() { func endSearch() {
webView?.evaluateJavaScript("endFind()") webView?.evaluateJavaScript("endFind()")
} }
func selectNextSearchResult() { func selectNextSearchResult() {
webView?.evaluateJavaScript("selectNextResult()") webView?.evaluateJavaScript("selectNextResult()")
} }
func selectPreviousSearchResult() { func selectPreviousSearchResult() {
webView?.evaluateJavaScript("selectPreviousResult()") webView?.evaluateJavaScript("selectPreviousResult()")
} }
} }

View File

@ -10,16 +10,16 @@ import Foundation
import WebKit import WebKit
class WrapperScriptMessageHandler: NSObject, WKScriptMessageHandler { class WrapperScriptMessageHandler: NSObject, WKScriptMessageHandler {
// We need to wrap a message handler to prevent a circlular reference // We need to wrap a message handler to prevent a circlular reference
private weak var handler: WKScriptMessageHandler? private weak var handler: WKScriptMessageHandler?
init(_ handler: WKScriptMessageHandler) { init(_ handler: WKScriptMessageHandler) {
self.handler = handler self.handler = handler
} }
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) { func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
handler?.userContentController(userContentController, didReceive: message) handler?.userContentController(userContentController, didReceive: message)
} }
} }

View File

@ -9,25 +9,25 @@
import UIKit import UIKit
class ArticleActivityItemSource: NSObject, UIActivityItemSource { class ArticleActivityItemSource: NSObject, UIActivityItemSource {
private let url: URL private let url: URL
private let subject: String? private let subject: String?
init(url: URL, subject: String?) { init(url: URL, subject: String?) {
self.url = url self.url = url
self.subject = subject self.subject = subject
} }
func activityViewControllerPlaceholderItem(_ : UIActivityViewController) -> Any { func activityViewControllerPlaceholderItem(_: UIActivityViewController) -> Any {
return url return url
} }
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? { func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
return url return url
} }
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String { func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
return subject ?? "" return subject ?? ""
} }
} }

View File

@ -14,7 +14,7 @@ struct ErrorHandler {
private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application") private static var log = OSLog(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
public static func present(_ viewController: UIViewController) -> (Error) -> () { public static func present(_ viewController: UIViewController) -> (Error) -> Void {
return { [weak viewController] error in return { [weak viewController] error in
if UIApplication.shared.applicationState == .active { if UIApplication.shared.applicationState == .active {
viewController?.presentError(error) viewController?.presentError(error)
@ -23,7 +23,7 @@ struct ErrorHandler {
} }
} }
} }
public static func log(_ error: Error) { public static func log(_ error: Error) {
os_log(.error, log: self.log, "%@", error.localizedDescription) os_log(.error, log: self.log, "%@", error.localizedDescription)
} }

View File

@ -10,7 +10,7 @@ import UIKit
final class IconView: UIView { final class IconView: UIView {
var iconImage: IconImage? = nil { var iconImage: IconImage? {
didSet { didSet {
guard iconImage !== oldValue else { guard iconImage !== oldValue else {
return return
@ -19,8 +19,7 @@ final class IconView: UIView {
if traitCollection.userInterfaceStyle == .dark { if traitCollection.userInterfaceStyle == .dark {
let isDark = iconImage?.isDark ?? false let isDark = iconImage?.isDark ?? false
isDiscernable = !isDark isDiscernable = !isDark
} } else {
else {
let isBright = iconImage?.isBright ?? false let isBright = iconImage?.isBright ?? false
isDiscernable = !isBright isDiscernable = !isBright
} }
@ -45,11 +44,11 @@ final class IconView: UIView {
private var isSymbolImage: Bool { private var isSymbolImage: Bool {
return iconImage?.isSymbol ?? false return iconImage?.isSymbol ?? false
} }
private var isBackgroundSuppressed: Bool { private var isBackgroundSuppressed: Bool {
return iconImage?.isBackgroundSuppressed ?? false return iconImage?.isBackgroundSuppressed ?? false
} }
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
commonInit() commonInit()
@ -59,7 +58,7 @@ final class IconView: UIView {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
convenience init() { convenience init() {
self.init(frame: .zero) self.init(frame: .zero)
} }
@ -96,8 +95,7 @@ private extension IconView {
} }
let offset = floor((viewSize.height - imageSize.height) / 2.0) let offset = floor((viewSize.height - imageSize.height) / 2.0)
return CGRect(x: offset, y: offset, width: imageSize.width, height: imageSize.height) return CGRect(x: offset, y: offset, width: imageSize.width, height: imageSize.height)
} } else if imageSize.height > imageSize.width {
else if imageSize.height > imageSize.width {
let factor = viewSize.height / imageSize.height let factor = viewSize.height / imageSize.height
let width = imageSize.width * factor let width = imageSize.width * factor
let originX = floor((viewSize.width - width) / 2.0) let originX = floor((viewSize.width - width) / 2.0)

View File

@ -21,23 +21,23 @@ class AccountInspectorViewController: UITableViewController {
var isModal = false var isModal = false
weak var account: Account? weak var account: Account?
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
guard let account = account else { return } guard let account = account else { return }
nameTextField.placeholder = account.defaultName nameTextField.placeholder = account.defaultName
nameTextField.text = account.name nameTextField.text = account.name
nameTextField.delegate = self nameTextField.delegate = self
activeSwitch.isOn = account.isActive activeSwitch.isOn = account.isActive
navigationItem.title = account.nameForDisplay navigationItem.title = account.nameForDisplay
if account.type != .onMyMac { if account.type != .onMyMac {
deleteAccountButton.setTitle(NSLocalizedString("Remove Account", comment: "Remove Account"), for: .normal) deleteAccountButton.setTitle(NSLocalizedString("Remove Account", comment: "Remove Account"), for: .normal)
} }
if account.type != .cloudKit { if account.type != .cloudKit {
limitationsAndSolutionsButton.isHidden = true limitationsAndSolutionsButton.isHidden = true
} }
@ -46,11 +46,11 @@ class AccountInspectorViewController: UITableViewController {
let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done)) let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done))
navigationItem.leftBarButtonItem = doneBarButtonItem navigationItem.leftBarButtonItem = doneBarButtonItem
} }
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
} }
override func viewWillDisappear(_ animated: Bool) { override func viewWillDisappear(_ animated: Bool) {
account?.name = nameTextField.text account?.name = nameTextField.text
account?.isActive = activeSwitch.isOn account?.isActive = activeSwitch.isOn
@ -59,7 +59,7 @@ class AccountInspectorViewController: UITableViewController {
@objc func done() { @objc func done() {
dismiss(animated: true) dismiss(animated: true)
} }
@IBAction func credentials(_ sender: Any) { @IBAction func credentials(_ sender: Any) {
guard let account = account else { return } guard let account = account else { return }
switch account.type { switch account.type {
@ -86,12 +86,12 @@ class AccountInspectorViewController: UITableViewController {
break break
} }
} }
@IBAction func deleteAccount(_ sender: Any) { @IBAction func deleteAccount(_ sender: Any) {
guard let account = account else { guard let account = account else {
return return
} }
let title = NSLocalizedString("Remove Account", comment: "Remove Account") let title = NSLocalizedString("Remove Account", comment: "Remove Account")
let message: String = { let message: String = {
switch account.type { switch account.type {
@ -105,9 +105,9 @@ class AccountInspectorViewController: UITableViewController {
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel)
alertController.addAction(cancelAction) alertController.addAction(cancelAction)
let markTitle = NSLocalizedString("Remove", comment: "Remove") let markTitle = NSLocalizedString("Remove", comment: "Remove")
let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (action) in let markAction = UIAlertAction(title: markTitle, style: .default) { [weak self] (_) in
guard let self = self, let account = self.account else { return } guard let self = self, let account = self.account else { return }
AccountManager.shared.deleteAccount(account) AccountManager.shared.deleteAccount(account)
if self.isModal { if self.isModal {
@ -118,7 +118,7 @@ class AccountInspectorViewController: UITableViewController {
} }
alertController.addAction(markAction) alertController.addAction(markAction)
alertController.preferredAction = markAction alertController.preferredAction = markAction
present(alertController, animated: true) present(alertController, animated: true)
} }
@ -132,7 +132,7 @@ class AccountInspectorViewController: UITableViewController {
// MARK: Table View // MARK: Table View
extension AccountInspectorViewController { extension AccountInspectorViewController {
var hidesCredentialsSection: Bool { var hidesCredentialsSection: Bool {
guard let account = account else { guard let account = account else {
return true return true
@ -147,7 +147,7 @@ extension AccountInspectorViewController {
override func numberOfSections(in tableView: UITableView) -> Int { override func numberOfSections(in tableView: UITableView) -> Int {
guard let account = account else { return 0 } guard let account = account else { return 0 }
if account == AccountManager.shared.defaultAccount { if account == AccountManager.shared.defaultAccount {
return 1 return 1
} else if hidesCredentialsSection { } else if hidesCredentialsSection {
@ -156,11 +156,11 @@ extension AccountInspectorViewController {
return super.numberOfSections(in: tableView) return super.numberOfSections(in: tableView)
} }
} }
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section) return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
} }
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
guard let account = account else { return nil } guard let account = account else { return nil }
@ -172,16 +172,16 @@ extension AccountInspectorViewController {
return super.tableView(tableView, viewForHeaderInSection: section) return super.tableView(tableView, viewForHeaderInSection: section)
} }
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell: UITableViewCell let cell: UITableViewCell
if indexPath.section == 1, hidesCredentialsSection { if indexPath.section == 1, hidesCredentialsSection {
cell = super.tableView(tableView, cellForRowAt: IndexPath(row: 0, section: 2)) cell = super.tableView(tableView, cellForRowAt: IndexPath(row: 0, section: 2))
} else { } else {
cell = super.tableView(tableView, cellForRowAt: indexPath) cell = super.tableView(tableView, cellForRowAt: indexPath)
} }
return cell return cell
} }
@ -191,17 +191,17 @@ extension AccountInspectorViewController {
} }
return false return false
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.selectRow(at: nil, animated: true, scrollPosition: .none) tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
} }
} }
// MARK: UITextFieldDelegate // MARK: UITextFieldDelegate
extension AccountInspectorViewController: UITextFieldDelegate { extension AccountInspectorViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool { func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder() textField.resignFirstResponder()
return true return true

View File

@ -12,52 +12,52 @@ import SafariServices
import UserNotifications import UserNotifications
class FeedInspectorViewController: UITableViewController { class FeedInspectorViewController: UITableViewController {
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0) static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0)
var feed: Feed! var feed: Feed!
@IBOutlet weak var nameTextField: UITextField! @IBOutlet weak var nameTextField: UITextField!
@IBOutlet weak var notifyAboutNewArticlesSwitch: UISwitch! @IBOutlet weak var notifyAboutNewArticlesSwitch: UISwitch!
@IBOutlet weak var alwaysShowReaderViewSwitch: UISwitch! @IBOutlet weak var alwaysShowReaderViewSwitch: UISwitch!
@IBOutlet weak var homePageLabel: InteractiveLabel! @IBOutlet weak var homePageLabel: InteractiveLabel!
@IBOutlet weak var feedURLLabel: InteractiveLabel! @IBOutlet weak var feedURLLabel: InteractiveLabel!
private var headerView: InspectorIconHeaderView? private var headerView: InspectorIconHeaderView?
private var iconImage: IconImage? { private var iconImage: IconImage? {
return IconImageCache.shared.imageForFeed(feed) return IconImageCache.shared.imageForFeed(feed)
} }
private let homePageIndexPath = IndexPath(row: 0, section: 1) private let homePageIndexPath = IndexPath(row: 0, section: 1)
private var shouldHideHomePageSection: Bool { private var shouldHideHomePageSection: Bool {
return feed.homePageURL == nil return feed.homePageURL == nil
} }
private var userNotificationSettings: UNNotificationSettings? private var userNotificationSettings: UNNotificationSettings?
override func viewDidLoad() { override func viewDidLoad() {
tableView.register(InspectorIconHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader") tableView.register(InspectorIconHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
navigationItem.title = feed.nameForDisplay navigationItem.title = feed.nameForDisplay
nameTextField.text = feed.nameForDisplay nameTextField.text = feed.nameForDisplay
notifyAboutNewArticlesSwitch.setOn(feed.isNotifyAboutNewArticles ?? false, animated: false) notifyAboutNewArticlesSwitch.setOn(feed.isNotifyAboutNewArticles ?? false, animated: false)
alwaysShowReaderViewSwitch.setOn(feed.isArticleExtractorAlwaysOn ?? false, animated: false) alwaysShowReaderViewSwitch.setOn(feed.isArticleExtractorAlwaysOn ?? false, animated: false)
homePageLabel.text = feed.homePageURL homePageLabel.text = feed.homePageURL
feedURLLabel.text = feed.url feedURLLabel.text = feed.url
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(updateNotificationSettings), name: UIApplication.willEnterForegroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(updateNotificationSettings), name: UIApplication.willEnterForegroundNotification, object: nil)
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
updateNotificationSettings() updateNotificationSettings()
} }
override func viewDidDisappear(_ animated: Bool) { override func viewDidDisappear(_ animated: Bool) {
if nameTextField.text != feed.nameForDisplay { if nameTextField.text != feed.nameForDisplay {
let nameText = nameTextField.text ?? "" let nameText = nameTextField.text ?? ""
@ -65,12 +65,12 @@ class FeedInspectorViewController: UITableViewController {
feed.rename(to: newName) { _ in } feed.rename(to: newName) { _ in }
} }
} }
// MARK: Notifications // MARK: Notifications
@objc func feedIconDidBecomeAvailable(_ notification: Notification) { @objc func feedIconDidBecomeAvailable(_ notification: Notification) {
headerView?.iconView.iconImage = iconImage headerView?.iconView.iconImage = iconImage
} }
@IBAction func notifyAboutNewArticlesChanged(_ sender: Any) { @IBAction func notifyAboutNewArticlesChanged(_ sender: Any) {
guard let settings = userNotificationSettings else { guard let settings = userNotificationSettings else {
notifyAboutNewArticlesSwitch.isOn = !notifyAboutNewArticlesSwitch.isOn notifyAboutNewArticlesSwitch.isOn = !notifyAboutNewArticlesSwitch.isOn
@ -82,7 +82,7 @@ class FeedInspectorViewController: UITableViewController {
} else if settings.authorizationStatus == .authorized { } else if settings.authorizationStatus == .authorized {
feed.isNotifyAboutNewArticles = notifyAboutNewArticlesSwitch.isOn feed.isNotifyAboutNewArticles = notifyAboutNewArticlesSwitch.isOn
} else { } else {
UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { (granted, _) in
self.updateNotificationSettings() self.updateNotificationSettings()
if granted { if granted {
DispatchQueue.main.async { DispatchQueue.main.async {
@ -97,22 +97,22 @@ class FeedInspectorViewController: UITableViewController {
} }
} }
} }
@IBAction func alwaysShowReaderViewChanged(_ sender: Any) { @IBAction func alwaysShowReaderViewChanged(_ sender: Any) {
feed.isArticleExtractorAlwaysOn = alwaysShowReaderViewSwitch.isOn feed.isArticleExtractorAlwaysOn = alwaysShowReaderViewSwitch.isOn
} }
@IBAction func done(_ sender: Any) { @IBAction func done(_ sender: Any) {
dismiss(animated: true) dismiss(animated: true)
} }
/// Returns a new indexPath, taking into consideration any /// Returns a new indexPath, taking into consideration any
/// conditions that may require the tableView to be /// conditions that may require the tableView to be
/// displayed differently than what is setup in the storyboard. /// displayed differently than what is setup in the storyboard.
private func shift(_ indexPath: IndexPath) -> IndexPath { private func shift(_ indexPath: IndexPath) -> IndexPath {
return IndexPath(row: indexPath.row, section: shift(indexPath.section)) return IndexPath(row: indexPath.row, section: shift(indexPath.section))
} }
/// Returns a new section, taking into consideration any /// Returns a new section, taking into consideration any
/// conditions that may require the tableView to be /// conditions that may require the tableView to be
/// displayed differently than what is setup in the storyboard. /// displayed differently than what is setup in the storyboard.
@ -123,7 +123,6 @@ class FeedInspectorViewController: UITableViewController {
return section return section
} }
} }
// MARK: Table View // MARK: Table View
@ -134,15 +133,15 @@ extension FeedInspectorViewController {
let numberOfSections = super.numberOfSections(in: tableView) let numberOfSections = super.numberOfSections(in: tableView)
return shouldHideHomePageSection ? numberOfSections - 1 : numberOfSections return shouldHideHomePageSection ? numberOfSections - 1 : numberOfSections
} }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return super.tableView(tableView, numberOfRowsInSection: shift(section)) return super.tableView(tableView, numberOfRowsInSection: shift(section))
} }
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: shift(section)) return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: shift(section))
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = super.tableView(tableView, cellForRowAt: shift(indexPath)) let cell = super.tableView(tableView, cellForRowAt: shift(indexPath))
if indexPath.section == 0 && indexPath.row == 1 { if indexPath.section == 0 && indexPath.row == 1 {
@ -154,11 +153,11 @@ extension FeedInspectorViewController {
} }
return cell return cell
} }
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
super.tableView(tableView, titleForHeaderInSection: shift(section)) super.tableView(tableView, titleForHeaderInSection: shift(section))
} }
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
if shift(section) == 0 { if shift(section) == 0 {
headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as? InspectorIconHeaderView headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as? InspectorIconHeaderView
@ -168,12 +167,12 @@ extension FeedInspectorViewController {
return super.tableView(tableView, viewForHeaderInSection: shift(section)) return super.tableView(tableView, viewForHeaderInSection: shift(section))
} }
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if shift(indexPath) == homePageIndexPath, if shift(indexPath) == homePageIndexPath,
let homePageUrlString = feed.homePageURL, let homePageUrlString = feed.homePageURL,
let homePageUrl = URL(string: homePageUrlString) { let homePageUrl = URL(string: homePageUrlString) {
let safari = SFSafariViewController(url: homePageUrl) let safari = SFSafariViewController(url: homePageUrl)
safari.modalPresentationStyle = .pageSheet safari.modalPresentationStyle = .pageSheet
present(safari, animated: true) { present(safari, animated: true) {
@ -181,24 +180,24 @@ extension FeedInspectorViewController {
} }
} }
} }
} }
// MARK: UITextFieldDelegate // MARK: UITextFieldDelegate
extension FeedInspectorViewController: UITextFieldDelegate { extension FeedInspectorViewController: UITextFieldDelegate {
func textFieldShouldReturn(_ textField: UITextField) -> Bool { func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder() textField.resignFirstResponder()
return true return true
} }
} }
// MARK: UNUserNotificationCenter // MARK: UNUserNotificationCenter
extension FeedInspectorViewController { extension FeedInspectorViewController {
@objc @objc
func updateNotificationSettings() { func updateNotificationSettings() {
UNUserNotificationCenter.current().getNotificationSettings { (settings) in UNUserNotificationCenter.current().getNotificationSettings { (settings) in
@ -210,12 +209,12 @@ extension FeedInspectorViewController {
} }
} }
} }
func notificationUpdateErrorAlert() -> UIAlertController { func notificationUpdateErrorAlert() -> UIAlertController {
let alert = UIAlertController(title: NSLocalizedString("Enable Notifications", comment: "Notifications"), let alert = UIAlertController(title: NSLocalizedString("Enable Notifications", comment: "Notifications"),
message: NSLocalizedString("Notifications need to be enabled in the Settings app.", comment: "Notifications need to be enabled in the Settings app."), preferredStyle: .alert) message: NSLocalizedString("Notifications need to be enabled in the Settings app.", comment: "Notifications need to be enabled in the Settings app."), preferredStyle: .alert)
let openSettings = UIAlertAction(title: NSLocalizedString("Open Settings", comment: "Open Settings"), style: .default) { (action) in let openSettings = UIAlertAction(title: NSLocalizedString("Open Settings", comment: "Open Settings"), style: .default) { (_) in
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil) UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly: false], completionHandler: nil)
} }
let dismiss = UIAlertAction(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil) let dismiss = UIAlertAction(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil)
alert.addAction(openSettings) alert.addAction(openSettings)
@ -223,5 +222,5 @@ extension FeedInspectorViewController {
alert.preferredAction = openSettings alert.preferredAction = openSettings
return alert return alert
} }
} }

View File

@ -11,17 +11,17 @@ import UIKit
class InspectorIconHeaderView: UITableViewHeaderFooterView { class InspectorIconHeaderView: UITableViewHeaderFooterView {
var iconView = IconView() var iconView = IconView()
override init(reuseIdentifier: String?) { override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier) super.init(reuseIdentifier: reuseIdentifier)
commonInit() commonInit()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
func commonInit() { func commonInit() {
addSubview(iconView) addSubview(iconView)
} }

View File

@ -9,7 +9,7 @@
import Intents import Intents
class IntentHandler: INExtension { class IntentHandler: INExtension {
override func handler(for intent: INIntent) -> Any { override func handler(for intent: INIntent) -> Any {
switch intent { switch intent {
case is AddWebFeedIntent: case is AddWebFeedIntent:
@ -18,5 +18,5 @@ class IntentHandler: INExtension {
fatalError("Unhandled intent type: \(intent)") fatalError("Unhandled intent type: \(intent)")
} }
} }
} }

View File

@ -16,16 +16,16 @@ enum KeyboardType: String {
} }
class KeyboardManager { class KeyboardManager {
private(set) var _keyCommands: [UIKeyCommand] private(set) var _keyCommands: [UIKeyCommand]
var keyCommands: [UIKeyCommand] { var keyCommands: [UIKeyCommand] {
guard !UIResponder.isFirstResponderTextField else { return [UIKeyCommand]() } guard !UIResponder.isFirstResponderTextField else { return [UIKeyCommand]() }
return _keyCommands return _keyCommands
} }
init(type: KeyboardType) { init(type: KeyboardType) {
_keyCommands = KeyboardManager.globalAuxilaryKeyCommands() _keyCommands = KeyboardManager.globalAuxilaryKeyCommands()
switch type { switch type {
case .sidebar: case .sidebar:
_keyCommands.append(contentsOf: KeyboardManager.hardcodeFeedKeyCommands()) _keyCommands.append(contentsOf: KeyboardManager.hardcodeFeedKeyCommands())
@ -34,7 +34,7 @@ class KeyboardManager {
default: default:
break break
} }
let globalFile = Bundle.main.path(forResource: KeyboardType.global.rawValue, ofType: "plist")! let globalFile = Bundle.main.path(forResource: KeyboardType.global.rawValue, ofType: "plist")!
let globalEntries = NSArray(contentsOfFile: globalFile)! as! [[String: Any]] let globalEntries = NSArray(contentsOfFile: globalFile)! as! [[String: Any]]
let globalCommands = globalEntries.compactMap { KeyboardManager.createKeyCommand(keyEntry: $0) } let globalCommands = globalEntries.compactMap { KeyboardManager.createKeyCommand(keyEntry: $0) }
@ -42,9 +42,9 @@ class KeyboardManager {
let specificFile = Bundle.main.path(forResource: type.rawValue, ofType: "plist")! let specificFile = Bundle.main.path(forResource: type.rawValue, ofType: "plist")!
let specificEntries = NSArray(contentsOfFile: specificFile)! as! [[String: Any]] let specificEntries = NSArray(contentsOfFile: specificFile)! as! [[String: Any]]
_keyCommands.append(contentsOf: specificEntries.compactMap { KeyboardManager.createKeyCommand(keyEntry: $0) } ) _keyCommands.append(contentsOf: specificEntries.compactMap { KeyboardManager.createKeyCommand(keyEntry: $0) })
} }
static func createKeyCommand(title: String, action: String, input: String, modifiers: UIKeyModifierFlags) -> UIKeyCommand { static func createKeyCommand(title: String, action: String, input: String, modifiers: UIKeyModifierFlags) -> UIKeyCommand {
let selector = NSSelectorFromString(action) let selector = NSSelectorFromString(action)
let keyCommand = UIKeyCommand(title: title, image: nil, action: selector, input: input, modifierFlags: modifiers, propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .on) let keyCommand = UIKeyCommand(title: title, image: nil, action: selector, input: input, modifierFlags: modifiers, propertyList: nil, alternates: [], discoverabilityTitle: nil, attributes: [], state: .on)
@ -54,7 +54,7 @@ class KeyboardManager {
} }
private extension KeyboardManager { private extension KeyboardManager {
static func createKeyCommand(keyEntry: [String: Any]) -> UIKeyCommand? { static func createKeyCommand(keyEntry: [String: Any]) -> UIKeyCommand? {
guard let input = createKeyCommandInput(keyEntry: keyEntry) else { return nil } guard let input = createKeyCommandInput(keyEntry: keyEntry) else { return nil }
let modifiers = createKeyModifierFlags(keyEntry: keyEntry) let modifiers = createKeyModifierFlags(keyEntry: keyEntry)
@ -71,8 +71,8 @@ private extension KeyboardManager {
static func createKeyCommandInput(keyEntry: [String: Any]) -> String? { static func createKeyCommandInput(keyEntry: [String: Any]) -> String? {
guard let key = keyEntry["key"] as? String else { return nil } guard let key = keyEntry["key"] as? String else { return nil }
switch(key) { switch key {
case "[space]": case "[space]":
return "\u{0020}" return "\u{0020}"
case "[uparrow]": case "[uparrow]":
@ -96,34 +96,34 @@ private extension KeyboardManager {
default: default:
return key return key
} }
} }
static func createKeyModifierFlags(keyEntry: [String: Any]) -> UIKeyModifierFlags { static func createKeyModifierFlags(keyEntry: [String: Any]) -> UIKeyModifierFlags {
var flags = UIKeyModifierFlags() var flags = UIKeyModifierFlags()
if keyEntry["shiftModifier"] as? Bool ?? false { if keyEntry["shiftModifier"] as? Bool ?? false {
flags.insert(.shift) flags.insert(.shift)
} }
if keyEntry["optionModifier"] as? Bool ?? false { if keyEntry["optionModifier"] as? Bool ?? false {
flags.insert(.alternate) flags.insert(.alternate)
} }
if keyEntry["commandModifier"] as? Bool ?? false { if keyEntry["commandModifier"] as? Bool ?? false {
flags.insert(.command) flags.insert(.command)
} }
if keyEntry["controlModifier"] as? Bool ?? false { if keyEntry["controlModifier"] as? Bool ?? false {
flags.insert(.control) flags.insert(.control)
} }
return flags return flags
} }
static func globalAuxilaryKeyCommands() -> [UIKeyCommand] { static func globalAuxilaryKeyCommands() -> [UIKeyCommand] {
var keys = [UIKeyCommand]() var keys = [UIKeyCommand]()
let addNewFeedTitle = NSLocalizedString("New Feed", comment: "New Feed") let addNewFeedTitle = NSLocalizedString("New Feed", comment: "New Feed")
keys.append(KeyboardManager.createKeyCommand(title: addNewFeedTitle, action: "addNewFeed:", input: "n", modifiers: [.command])) keys.append(KeyboardManager.createKeyCommand(title: addNewFeedTitle, action: "addNewFeed:", input: "n", modifiers: [.command]))
@ -147,7 +147,7 @@ private extension KeyboardManager {
let gotoSettings = NSLocalizedString("Go To Settings", comment: "Go To Settings") let gotoSettings = NSLocalizedString("Go To Settings", comment: "Go To Settings")
keys.append(KeyboardManager.createKeyCommand(title: gotoSettings, action: "goToSettings:", input: ",", modifiers: [.command])) keys.append(KeyboardManager.createKeyCommand(title: gotoSettings, action: "goToSettings:", input: ",", modifiers: [.command]))
let articleSearchTitle = NSLocalizedString("Article Search", comment: "Article Search") let articleSearchTitle = NSLocalizedString("Article Search", comment: "Article Search")
keys.append(KeyboardManager.createKeyCommand(title: articleSearchTitle, action: "articleSearch:", input: "f", modifiers: [.command, .alternate])) keys.append(KeyboardManager.createKeyCommand(title: articleSearchTitle, action: "articleSearch:", input: "f", modifiers: [.command, .alternate]))
@ -165,7 +165,7 @@ private extension KeyboardManager {
return keys return keys
} }
static func hardcodeFeedKeyCommands() -> [UIKeyCommand] { static func hardcodeFeedKeyCommands() -> [UIKeyCommand] {
var keys = [UIKeyCommand]() var keys = [UIKeyCommand]()
@ -174,16 +174,16 @@ private extension KeyboardManager {
let nextDownTitle = NSLocalizedString("Select Next Down", comment: "Select Next Down") let nextDownTitle = NSLocalizedString("Select Next Down", comment: "Select Next Down")
keys.append(KeyboardManager.createKeyCommand(title: nextDownTitle, action: "selectNextDown:", input: UIKeyCommand.inputDownArrow, modifiers: [])) keys.append(KeyboardManager.createKeyCommand(title: nextDownTitle, action: "selectNextDown:", input: UIKeyCommand.inputDownArrow, modifiers: []))
let getFeedInfo = NSLocalizedString("Get Feed Info", comment: "Get Feed Info") let getFeedInfo = NSLocalizedString("Get Feed Info", comment: "Get Feed Info")
keys.append(KeyboardManager.createKeyCommand(title: getFeedInfo, action: "showFeedInspector:", input: "i", modifiers: .command)) keys.append(KeyboardManager.createKeyCommand(title: getFeedInfo, action: "showFeedInspector:", input: "i", modifiers: .command))
return keys return keys
} }
static func hardcodeArticleKeyCommands() -> [UIKeyCommand] { static func hardcodeArticleKeyCommands() -> [UIKeyCommand] {
var keys = [UIKeyCommand]() var keys = [UIKeyCommand]()
let openInBrowserTitle = NSLocalizedString("Open In Browser", comment: "Open In Browser") let openInBrowserTitle = NSLocalizedString("Open In Browser", comment: "Open In Browser")
keys.append(KeyboardManager.createKeyCommand(title: openInBrowserTitle, action: "openInBrowser:", input: UIKeyCommand.inputRightArrow, modifiers: [.command])) keys.append(KeyboardManager.createKeyCommand(title: openInBrowserTitle, action: "openInBrowser:", input: UIKeyCommand.inputRightArrow, modifiers: [.command]))
@ -198,7 +198,7 @@ private extension KeyboardManager {
let toggleStarredTitle = NSLocalizedString("Toggle Starred Status", comment: "Toggle Starred Status") let toggleStarredTitle = NSLocalizedString("Toggle Starred Status", comment: "Toggle Starred Status")
keys.append(KeyboardManager.createKeyCommand(title: toggleStarredTitle, action: "toggleStarred:", input: "l", modifiers: [.command, .shift])) keys.append(KeyboardManager.createKeyCommand(title: toggleStarredTitle, action: "toggleStarred:", input: "l", modifiers: [.command, .shift]))
let findInArticleTitle = NSLocalizedString("Find in Article", comment: "Find in Article") let findInArticleTitle = NSLocalizedString("Find in Article", comment: "Find in Article")
keys.append(KeyboardManager.createKeyCommand(title: findInArticleTitle, action: "beginFind:", input: "f", modifiers: [.command])) keys.append(KeyboardManager.createKeyCommand(title: findInArticleTitle, action: "beginFind:", input: "f", modifiers: [.command]))
@ -213,5 +213,5 @@ private extension KeyboardManager {
return keys return keys
} }
} }

View File

@ -11,13 +11,13 @@ import Foundation
class MainFeedRowIdentifier: NSObject, NSCopying { class MainFeedRowIdentifier: NSObject, NSCopying {
var indexPath: IndexPath var indexPath: IndexPath
init(indexPath: IndexPath) { init(indexPath: IndexPath) {
self.indexPath = indexPath self.indexPath = indexPath
} }
func copy(with zone: NSZone? = nil) -> Any { func copy(with zone: NSZone? = nil) -> Any {
return self return self
} }
} }

View File

@ -15,7 +15,7 @@ protocol MainFeedTableViewCellDelegate: AnyObject {
func mainFeedTableViewCellDisclosureDidToggle(_ sender: MainFeedTableViewCell, expanding: Bool) func mainFeedTableViewCellDisclosureDidToggle(_ sender: MainFeedTableViewCell, expanding: Bool)
} }
class MainFeedTableViewCell : VibrantTableViewCell { class MainFeedTableViewCell: VibrantTableViewCell {
weak var delegate: MainFeedTableViewCellDelegate? weak var delegate: MainFeedTableViewCellDelegate?
@ -44,7 +44,7 @@ class MainFeedTableViewCell : VibrantTableViewCell {
} }
} }
} }
var isSeparatorShown = true { var isSeparatorShown = true {
didSet { didSet {
if isSeparatorShown != oldValue { if isSeparatorShown != oldValue {
@ -56,7 +56,7 @@ class MainFeedTableViewCell : VibrantTableViewCell {
} }
} }
} }
var unreadCount: Int { var unreadCount: Int {
get { get {
return unreadCountView.unreadCount return unreadCountView.unreadCount
@ -100,17 +100,17 @@ class MainFeedTableViewCell : VibrantTableViewCell {
view.alpha = 0.5 view.alpha = 0.5
return view return view
}() }()
private var isDisclosureExpanded = false private var isDisclosureExpanded = false
private var disclosureButton: UIButton? private var disclosureButton: UIButton?
private var unreadCountView = MainFeedUnreadCountView(frame: CGRect.zero) private var unreadCountView = MainFeedUnreadCountView(frame: CGRect.zero)
private var isShowingEditControl = false private var isShowingEditControl = false
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
func setDisclosure(isExpanded: Bool, animated: Bool) { func setDisclosure(isExpanded: Bool, animated: Bool) {
isDisclosureExpanded = isExpanded isDisclosureExpanded = isExpanded
let duration = animated ? 0.3 : 0.0 let duration = animated ? 0.3 : 0.0
@ -120,42 +120,38 @@ class MainFeedTableViewCell : VibrantTableViewCell {
self.disclosureButton?.accessibilityLabel = NSLocalizedString("Collapse Folder", comment: "Collapse Folder") self.disclosureButton?.accessibilityLabel = NSLocalizedString("Collapse Folder", comment: "Collapse Folder")
self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 1.570796) self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 1.570796)
} else { } else {
self.disclosureButton?.accessibilityLabel = NSLocalizedString("Expand Folder", comment: "Expand Folder") self.disclosureButton?.accessibilityLabel = NSLocalizedString("Expand Folder", comment: "Expand Folder")
self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 0) self.disclosureButton?.imageView?.transform = CGAffineTransform(rotationAngle: 0)
} }
} }
} }
override func applyThemeProperties() {
super.applyThemeProperties()
}
override func willTransition(to state: UITableViewCell.StateMask) { override func willTransition(to state: UITableViewCell.StateMask) {
super.willTransition(to: state) super.willTransition(to: state)
isShowingEditControl = state.contains(.showingEditControl) isShowingEditControl = state.contains(.showingEditControl)
} }
override func sizeThatFits(_ size: CGSize) -> CGSize { override func sizeThatFits(_ size: CGSize) -> CGSize {
let layout = MainFeedTableViewCellLayout(cellWidth: bounds.size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView, showingEditingControl: isShowingEditControl, indent: indentationLevel == 1, shouldShowDisclosure: isDisclosureAvailable) let layout = MainFeedTableViewCellLayout(cellWidth: bounds.size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView, showingEditingControl: isShowingEditControl, indent: indentationLevel == 1, shouldShowDisclosure: isDisclosureAvailable)
return CGSize(width: bounds.width, height: layout.height) return CGSize(width: bounds.width, height: layout.height)
} }
override func layoutSubviews() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
let layout = MainFeedTableViewCellLayout(cellWidth: bounds.size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView, showingEditingControl: isShowingEditControl, indent: indentationLevel == 1, shouldShowDisclosure: isDisclosureAvailable) let layout = MainFeedTableViewCellLayout(cellWidth: bounds.size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView, showingEditingControl: isShowingEditControl, indent: indentationLevel == 1, shouldShowDisclosure: isDisclosureAvailable)
layoutWith(layout) layoutWith(layout)
} }
@objc func buttonPressed(_ sender: UIButton) { @objc func buttonPressed(_ sender: UIButton) {
if isDisclosureAvailable { if isDisclosureAvailable {
setDisclosure(isExpanded: !isDisclosureExpanded, animated: true) setDisclosure(isExpanded: !isDisclosureExpanded, animated: true)
delegate?.mainFeedTableViewCellDisclosureDidToggle(self, expanding: isDisclosureExpanded) delegate?.mainFeedTableViewCellDisclosureDidToggle(self, expanding: isDisclosureExpanded)
} }
} }
override func updateVibrancy(animated: Bool) { override func updateVibrancy(animated: Bool) {
super.updateVibrancy(animated: animated) super.updateVibrancy(animated: animated)
let iconTintColor: UIColor let iconTintColor: UIColor
if isHighlighted || isSelected { if isHighlighted || isSelected {
iconTintColor = AppAssets.vibrantTextColor iconTintColor = AppAssets.vibrantTextColor
@ -166,7 +162,7 @@ class MainFeedTableViewCell : VibrantTableViewCell {
iconTintColor = AppAssets.secondaryAccentColor iconTintColor = AppAssets.secondaryAccentColor
} }
} }
if animated { if animated {
UIView.animate(withDuration: Self.duration) { UIView.animate(withDuration: Self.duration) {
self.iconView.tintColor = iconTintColor self.iconView.tintColor = iconTintColor
@ -174,10 +170,10 @@ class MainFeedTableViewCell : VibrantTableViewCell {
} else { } else {
self.iconView.tintColor = iconTintColor self.iconView.tintColor = iconTintColor
} }
updateLabelVibrancy(titleView, color: labelColor, animated: animated) updateLabelVibrancy(titleView, color: labelColor, animated: animated)
} }
} }
private extension MainFeedTableViewCell { private extension MainFeedTableViewCell {
@ -200,7 +196,7 @@ private extension MainFeedTableViewCell {
disclosureButton?.addInteraction(UIPointerInteraction()) disclosureButton?.addInteraction(UIPointerInteraction())
addSubviewAtInit(disclosureButton!) addSubviewAtInit(disclosureButton!)
} }
func addSubviewAtInit(_ view: UIView) { func addSubviewAtInit(_ view: UIView) {
addSubview(view) addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
@ -220,11 +216,11 @@ private extension MainFeedTableViewCell {
view.isHidden = true view.isHidden = true
} }
} }
func showView(_ view: UIView) { func showView(_ view: UIView) {
if view.isHidden { if view.isHidden {
view.isHidden = false view.isHidden = false
} }
} }
} }

View File

@ -21,7 +21,7 @@ struct MainFeedTableViewCellLayout {
private static let verticalPadding = CGFloat(integerLiteral: 11) private static let verticalPadding = CGFloat(integerLiteral: 11)
private static let minRowHeight = CGFloat(integerLiteral: 44) private static let minRowHeight = CGFloat(integerLiteral: 44)
static let faviconCornerRadius = CGFloat(integerLiteral: 2) static let faviconCornerRadius = CGFloat(integerLiteral: 2)
let faviconRect: CGRect let faviconRect: CGRect
@ -29,9 +29,9 @@ struct MainFeedTableViewCellLayout {
let unreadCountRect: CGRect let unreadCountRect: CGRect
let disclosureButtonRect: CGRect let disclosureButtonRect: CGRect
let separatorRect: CGRect let separatorRect: CGRect
let height: CGFloat let height: CGFloat
init(cellWidth: CGFloat, insets: UIEdgeInsets, label: UILabel, unreadCountView: MainFeedUnreadCountView, showingEditingControl: Bool, indent: Bool, shouldShowDisclosure: Bool) { init(cellWidth: CGFloat, insets: UIEdgeInsets, label: UILabel, unreadCountView: MainFeedUnreadCountView, showingEditingControl: Bool, indent: Bool, shouldShowDisclosure: Bool) {
var initialIndent = insets.left var initialIndent = insets.left
@ -39,7 +39,7 @@ struct MainFeedTableViewCellLayout {
initialIndent += MainFeedTableViewCellLayout.indentWidth initialIndent += MainFeedTableViewCellLayout.indentWidth
} }
let bounds = CGRect(x: initialIndent, y: 0.0, width: floor(cellWidth - initialIndent - insets.right), height: 0.0) let bounds = CGRect(x: initialIndent, y: 0.0, width: floor(cellWidth - initialIndent - insets.right), height: 0.0)
// Disclosure Button // Disclosure Button
var rDisclosure = CGRect.zero var rDisclosure = CGRect.zero
if shouldShowDisclosure { if shouldShowDisclosure {
@ -66,7 +66,7 @@ struct MainFeedTableViewCellLayout {
rUnread.size = unreadCountSize rUnread.size = unreadCountSize
rUnread.origin.x = bounds.maxX - (MainFeedTableViewCellLayout.unreadCountMarginRight + unreadCountSize.width) rUnread.origin.x = bounds.maxX - (MainFeedTableViewCellLayout.unreadCountMarginRight + unreadCountSize.width)
} }
// Title // Title
var rLabelx = insets.left + MainFeedTableViewCellLayout.disclosureButtonSize.width var rLabelx = insets.left + MainFeedTableViewCellLayout.disclosureButtonSize.width
if !shouldShowDisclosure { if !shouldShowDisclosure {
@ -80,7 +80,7 @@ struct MainFeedTableViewCellLayout {
} else { } else {
labelWidth = cellWidth - (rLabelx + MainFeedTableViewCellLayout.labelMarginRight) labelWidth = cellWidth - (rLabelx + MainFeedTableViewCellLayout.labelMarginRight)
} }
let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth))) let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth)))
// Now that we've got everything (especially the label) computed without the editing controls, update for them. // Now that we've got everything (especially the label) computed without the editing controls, update for them.
@ -99,7 +99,7 @@ struct MainFeedTableViewCellLayout {
} }
var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height) var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
// Determine cell height // Determine cell height
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MainFeedTableViewCellLayout.verticalPadding) let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MainFeedTableViewCellLayout.verticalPadding)
let maxGraphicsHeight = [rFavicon, rUnread, rDisclosure].maxY() let maxGraphicsHeight = [rFavicon, rUnread, rDisclosure].maxY()
@ -107,7 +107,7 @@ struct MainFeedTableViewCellLayout {
if cellHeight < MainFeedTableViewCellLayout.minRowHeight { if cellHeight < MainFeedTableViewCellLayout.minRowHeight {
cellHeight = MainFeedTableViewCellLayout.minRowHeight cellHeight = MainFeedTableViewCellLayout.minRowHeight
} }
// Center in Cell // Center in Cell
let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight) let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight)
if !unreadCountIsHidden { if !unreadCountIsHidden {
@ -126,16 +126,16 @@ struct MainFeedTableViewCellLayout {
// Separator Insets // Separator Insets
let separatorInset = MainFeedTableViewCellLayout.disclosureButtonSize.width let separatorInset = MainFeedTableViewCellLayout.disclosureButtonSize.width
separatorRect = CGRect(x: separatorInset, y: cellHeight - 0.5, width: cellWidth - separatorInset, height: 0.5) separatorRect = CGRect(x: separatorInset, y: cellHeight - 0.5, width: cellWidth - separatorInset, height: 0.5)
// Assign the properties // Assign the properties
self.height = cellHeight self.height = cellHeight
self.faviconRect = rFavicon self.faviconRect = rFavicon
self.unreadCountRect = rUnread self.unreadCountRect = rUnread
self.disclosureButtonRect = rDisclosure self.disclosureButtonRect = rDisclosure
self.titleRect = rLabel self.titleRect = rLabel
} }
// Ideally this will be implemented in RSCore (see RSGeometry) // Ideally this will be implemented in RSCore (see RSGeometry)
static func centerVertically(_ originalRect: CGRect, _ containerRect: CGRect) -> CGRect { static func centerVertically(_ originalRect: CGRect, _ containerRect: CGRect) -> CGRect {
var result = originalRect var result = originalRect
@ -144,5 +144,5 @@ struct MainFeedTableViewCellLayout {
result.size = originalRect.size result.size = originalRect.size
return result return result
} }
} }

View File

@ -15,7 +15,7 @@ protocol MainFeedTableViewSectionHeaderDelegate {
class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView { class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
var delegate: MainFeedTableViewSectionHeaderDelegate? var delegate: MainFeedTableViewSectionHeaderDelegate?
override var accessibilityLabel: String? { override var accessibilityLabel: String? {
set {} set {}
get { get {
@ -37,7 +37,7 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
return NSLocalizedString("Collapsed", comment: "Disclosure button collapsed state for accessibility") return NSLocalizedString("Collapsed", comment: "Disclosure button collapsed state for accessibility")
} }
} }
var unreadCount: Int { var unreadCount: Int {
get { get {
return unreadCountView.unreadCount return unreadCountView.unreadCount
@ -50,7 +50,7 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
} }
} }
} }
var name: String { var name: String {
get { get {
return titleView.text ?? "" return titleView.text ?? ""
@ -62,16 +62,16 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
} }
} }
} }
var disclosureExpanded = false { var disclosureExpanded = false {
didSet { didSet {
updateExpandedState(animate: true) updateExpandedState(animate: true)
updateUnreadCountView() updateUnreadCountView()
} }
} }
var isLastSection = false var isLastSection = false
private let titleView: UILabel = { private let titleView: UILabel = {
let label = NonIntrinsicLabel() let label = NonIntrinsicLabel()
label.numberOfLines = 0 label.numberOfLines = 0
@ -80,7 +80,7 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
label.font = .preferredFont(forTextStyle: .body) label.font = .preferredFont(forTextStyle: .body)
return label return label
}() }()
private let unreadCountView = MainFeedUnreadCountView(frame: CGRect.zero) private let unreadCountView = MainFeedUnreadCountView(frame: CGRect.zero)
private lazy var disclosureButton: UIButton = { private lazy var disclosureButton: UIButton = {
@ -98,27 +98,27 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
view.backgroundColor = UIColor.separator view.backgroundColor = UIColor.separator
return view return view
}() }()
private let bottomSeparatorView: UIView = { private let bottomSeparatorView: UIView = {
let view = UIView() let view = UIView()
view.backgroundColor = UIColor.separator view.backgroundColor = UIColor.separator
return view return view
}() }()
override init(reuseIdentifier: String?) { override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier) super.init(reuseIdentifier: reuseIdentifier)
commonInit() commonInit()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
override func sizeThatFits(_ size: CGSize) -> CGSize { override func sizeThatFits(_ size: CGSize) -> CGSize {
let layout = MainFeedTableViewSectionHeaderLayout(cellWidth: size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView) let layout = MainFeedTableViewSectionHeaderLayout(cellWidth: size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView)
return CGSize(width: bounds.width, height: layout.height) return CGSize(width: bounds.width, height: layout.height)
} }
override func layoutSubviews() { override func layoutSubviews() {
@ -137,7 +137,7 @@ private extension MainFeedTableViewSectionHeader {
@objc func toggleDisclosure() { @objc func toggleDisclosure() {
delegate?.mainFeedTableViewSectionHeaderDisclosureDidToggle(self) delegate?.mainFeedTableViewSectionHeaderDisclosureDidToggle(self)
} }
func commonInit() { func commonInit() {
addSubviewAtInit(unreadCountView) addSubviewAtInit(unreadCountView)
addSubviewAtInit(titleView) addSubviewAtInit(titleView)
@ -147,14 +147,14 @@ private extension MainFeedTableViewSectionHeader {
addSubviewAtInit(topSeparatorView) addSubviewAtInit(topSeparatorView)
addSubviewAtInit(bottomSeparatorView) addSubviewAtInit(bottomSeparatorView)
} }
func updateExpandedState(animate: Bool) { func updateExpandedState(animate: Bool) {
if !isLastSection && self.disclosureExpanded { if !isLastSection && self.disclosureExpanded {
self.bottomSeparatorView.isHidden = false self.bottomSeparatorView.isHidden = false
} }
let duration = animate ? 0.3 : 0.0 let duration = animate ? 0.3 : 0.0
UIView.animate( UIView.animate(
withDuration: duration, withDuration: duration,
animations: { animations: {
@ -169,7 +169,7 @@ private extension MainFeedTableViewSectionHeader {
} }
}) })
} }
func updateUnreadCountView() { func updateUnreadCountView() {
if !disclosureExpanded && unreadCount > 0 { if !disclosureExpanded && unreadCount > 0 {
UIView.animate(withDuration: 0.3) { UIView.animate(withDuration: 0.3) {
@ -186,7 +186,7 @@ private extension MainFeedTableViewSectionHeader {
contentView.addSubview(view) contentView.addSubview(view)
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
} }
func layoutWith(_ layout: MainFeedTableViewSectionHeaderLayout) { func layoutWith(_ layout: MainFeedTableViewSectionHeaderLayout) {
titleView.setFrameIfNotEqual(layout.titleRect) titleView.setFrameIfNotEqual(layout.titleRect)
unreadCountView.setFrameIfNotEqual(layout.unreadCountRect) unreadCountView.setFrameIfNotEqual(layout.unreadCountRect)
@ -198,14 +198,14 @@ private extension MainFeedTableViewSectionHeader {
let top = CGRect(x: x, y: 0, width: width, height: height) let top = CGRect(x: x, y: 0, width: width, height: height)
topSeparatorView.setFrameIfNotEqual(top) topSeparatorView.setFrameIfNotEqual(top)
let bottom = CGRect(x: x, y: frame.height - height, width: width, height: height) let bottom = CGRect(x: x, y: frame.height - height, width: width, height: height)
bottomSeparatorView.setFrameIfNotEqual(bottom) bottomSeparatorView.setFrameIfNotEqual(bottom)
} }
func addBackgroundView() { func addBackgroundView() {
self.backgroundView = UIView(frame: self.bounds) self.backgroundView = UIView(frame: self.bounds)
self.backgroundView?.backgroundColor = AppAssets.sectionHeaderColor self.backgroundView?.backgroundColor = AppAssets.sectionHeaderColor
} }
} }

View File

@ -17,17 +17,17 @@ struct MainFeedTableViewSectionHeaderLayout {
private static let verticalPadding = CGFloat(integerLiteral: 11) private static let verticalPadding = CGFloat(integerLiteral: 11)
private static let minRowHeight = CGFloat(integerLiteral: 44) private static let minRowHeight = CGFloat(integerLiteral: 44)
let titleRect: CGRect let titleRect: CGRect
let unreadCountRect: CGRect let unreadCountRect: CGRect
let disclosureButtonRect: CGRect let disclosureButtonRect: CGRect
let height: CGFloat let height: CGFloat
init(cellWidth: CGFloat, insets: UIEdgeInsets, label: UILabel, unreadCountView: MainFeedUnreadCountView) { init(cellWidth: CGFloat, insets: UIEdgeInsets, label: UILabel, unreadCountView: MainFeedUnreadCountView) {
let bounds = CGRect(x: insets.left, y: 0.0, width: floor(cellWidth - insets.right), height: 0.0) let bounds = CGRect(x: insets.left, y: 0.0, width: floor(cellWidth - insets.right), height: 0.0)
// Disclosure Button // Disclosure Button
var rDisclosure = CGRect.zero var rDisclosure = CGRect.zero
rDisclosure.size = MainFeedTableViewSectionHeaderLayout.disclosureButtonSize rDisclosure.size = MainFeedTableViewSectionHeaderLayout.disclosureButtonSize
@ -42,7 +42,7 @@ struct MainFeedTableViewSectionHeaderLayout {
rUnread.size = unreadCountSize rUnread.size = unreadCountSize
rUnread.origin.x = bounds.maxX - (MainFeedTableViewSectionHeaderLayout.unreadCountMarginRight + unreadCountSize.width) rUnread.origin.x = bounds.maxX - (MainFeedTableViewSectionHeaderLayout.unreadCountMarginRight + unreadCountSize.width)
} }
// Max Unread Count // Max Unread Count
// We can't reload Section Headers so we don't let the title extend into the (probably) worse case Unread Count area. // We can't reload Section Headers so we don't let the title extend into the (probably) worse case Unread Count area.
let maxUnreadCountView = MainFeedUnreadCountView(frame: CGRect.zero) let maxUnreadCountView = MainFeedUnreadCountView(frame: CGRect.zero)
@ -58,7 +58,7 @@ struct MainFeedTableViewSectionHeaderLayout {
let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth))) let labelSizeInfo = MultilineUILabelSizer.size(for: label.text ?? "", font: label.font, numberOfLines: 0, width: Int(floor(labelWidth)))
var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height) var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
// Determine cell height // Determine cell height
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MainFeedTableViewSectionHeaderLayout.verticalPadding) let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MainFeedTableViewSectionHeaderLayout.verticalPadding)
let maxGraphicsHeight = [rUnread, rDisclosure].maxY() let maxGraphicsHeight = [rUnread, rDisclosure].maxY()
@ -66,7 +66,7 @@ struct MainFeedTableViewSectionHeaderLayout {
if cellHeight < MainFeedTableViewSectionHeaderLayout.minRowHeight { if cellHeight < MainFeedTableViewSectionHeaderLayout.minRowHeight {
cellHeight = MainFeedTableViewSectionHeaderLayout.minRowHeight cellHeight = MainFeedTableViewSectionHeaderLayout.minRowHeight
} }
// Center in Cell // Center in Cell
let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight) let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight)
if !unreadCountIsHidden { if !unreadCountIsHidden {
@ -78,13 +78,13 @@ struct MainFeedTableViewSectionHeaderLayout {
if cellHeight == MainFeedTableViewSectionHeaderLayout.minRowHeight { if cellHeight == MainFeedTableViewSectionHeaderLayout.minRowHeight {
rLabel = MainFeedTableViewCellLayout.centerVertically(rLabel, newBounds) rLabel = MainFeedTableViewCellLayout.centerVertically(rLabel, newBounds)
} }
// Assign the properties // Assign the properties
self.height = cellHeight self.height = cellHeight
self.unreadCountRect = rUnread self.unreadCountRect = rUnread
self.disclosureButtonRect = rDisclosure self.disclosureButtonRect = rDisclosure
self.titleRect = rLabel self.titleRect = rLabel
} }
} }

View File

@ -8,18 +8,18 @@
import UIKit import UIKit
class MainFeedUnreadCountView : UIView { class MainFeedUnreadCountView: UIView {
var padding: UIEdgeInsets { var padding: UIEdgeInsets {
return UIEdgeInsets(top: 1.0, left: 9.0, bottom: 1.0, right: 9.0) return UIEdgeInsets(top: 1.0, left: 9.0, bottom: 1.0, right: 9.0)
} }
let cornerRadius = 8.0 let cornerRadius = 8.0
let bgColor = AppAssets.controlBackgroundColor let bgColor = AppAssets.controlBackgroundColor
var textColor: UIColor { var textColor: UIColor {
return UIColor.white return UIColor.white
} }
var textAttributes: [NSAttributedString.Key: AnyObject] { var textAttributes: [NSAttributedString.Key: AnyObject] {
let textFont = UIFont.preferredFont(forTextStyle: .caption1).bold() let textFont = UIFont.preferredFont(forTextStyle: .caption1).bold()
return [NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.font: textFont, NSAttributedString.Key.kern: NSNull()] return [NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.font: textFont, NSAttributedString.Key.kern: NSNull()]
@ -33,7 +33,7 @@ class MainFeedUnreadCountView : UIView {
setNeedsDisplay() setNeedsDisplay()
} }
} }
var unreadCountString: String { var unreadCountString: String {
return unreadCount < 1 ? "" : "\(unreadCount)" return unreadCount < 1 ? "" : "\(unreadCount)"
} }
@ -45,18 +45,18 @@ class MainFeedUnreadCountView : UIView {
super.init(frame: frame) super.init(frame: frame)
self.isOpaque = false self.isOpaque = false
} }
required init?(coder aDecoder: NSCoder) { required init?(coder aDecoder: NSCoder) {
super.init(coder: aDecoder) super.init(coder: aDecoder)
self.isOpaque = false self.isOpaque = false
} }
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
textSizeCache = [Int: CGSize]() textSizeCache = [Int: CGSize]()
contentSizeIsValid = false contentSizeIsValid = false
setNeedsDisplay() setNeedsDisplay()
} }
var contentSize: CGSize { var contentSize: CGSize {
if !contentSizeIsValid { if !contentSizeIsValid {
var size = CGSize.zero var size = CGSize.zero
@ -70,7 +70,7 @@ class MainFeedUnreadCountView : UIView {
} }
return _contentSize return _contentSize
} }
// Prevent autolayout from messing around with our frame settings // Prevent autolayout from messing around with our frame settings
override var intrinsicContentSize: CGSize { override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
@ -92,7 +92,7 @@ class MainFeedUnreadCountView : UIView {
textSizeCache[unreadCount] = size textSizeCache[unreadCount] = size
return size return size
} }
func textRect() -> CGRect { func textRect() -> CGRect {
@ -103,7 +103,7 @@ class MainFeedUnreadCountView : UIView {
r.origin.x = (bounds.maxX - padding.right) - r.size.width r.origin.x = (bounds.maxX - padding.right) - r.size.width
r.origin.y = padding.top r.origin.y = padding.top
return r return r
} }
override func draw(_ dirtyRect: CGRect) { override func draw(_ dirtyRect: CGRect) {
@ -116,8 +116,7 @@ class MainFeedUnreadCountView : UIView {
if unreadCount > 0 { if unreadCount > 0 {
unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes) unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes)
} }
}
}
}
}

View File

@ -12,25 +12,25 @@ import Account
import UniformTypeIdentifiers import UniformTypeIdentifiers
extension MainFeedViewController: UITableViewDragDelegate { extension MainFeedViewController: UITableViewDragDelegate {
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] { func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? Feed else { guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? Feed else {
return [UIDragItem]() return [UIDragItem]()
} }
let data = feed.url.data(using: .utf8) let data = feed.url.data(using: .utf8)
let itemProvider = NSItemProvider() let itemProvider = NSItemProvider()
itemProvider.registerDataRepresentation(forTypeIdentifier: UTType.url.identifier, visibility: .ownProcess) { completion in itemProvider.registerDataRepresentation(forTypeIdentifier: UTType.url.identifier, visibility: .ownProcess) { completion in
Task { @MainActor in Task { @MainActor in
completion(data, nil) completion(data, nil)
} }
return nil return nil
} }
let dragItem = UIDragItem(itemProvider: itemProvider) let dragItem = UIDragItem(itemProvider: itemProvider)
dragItem.localObject = node dragItem.localObject = node
return [dragItem] return [dragItem]
} }
} }

View File

@ -12,16 +12,16 @@ import Account
import RSTree import RSTree
extension MainFeedViewController: UITableViewDropDelegate { extension MainFeedViewController: UITableViewDropDelegate {
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool { func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
return session.localDragSession != nil return session.localDragSession != nil
} }
func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal { func tableView(_ tableView: UITableView, dropSessionDidUpdate session: UIDropSession, withDestinationIndexPath destinationIndexPath: IndexPath?) -> UITableViewDropProposal {
guard let destIndexPath = destinationIndexPath, destIndexPath.section > 0, tableView.hasActiveDrag else { guard let destIndexPath = destinationIndexPath, destIndexPath.section > 0, tableView.hasActiveDrag else {
return UITableViewDropProposal(operation: .forbidden) return UITableViewDropProposal(operation: .forbidden)
} }
guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? SidebarItem, guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? SidebarItem,
let destAccount = destFeed.account, let destAccount = destFeed.account,
let destCell = tableView.cellForRow(at: destIndexPath) else { let destCell = tableView.cellForRow(at: destIndexPath) else {
@ -48,7 +48,7 @@ extension MainFeedViewController: UITableViewDropDelegate {
} }
} }
func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) { func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) {
guard let dragItem = dropCoordinator.items.first?.dragItem, guard let dragItem = dropCoordinator.items.first?.dragItem,
let dragNode = dragItem.localObject as? Node, let dragNode = dragItem.localObject as? Node,
@ -56,17 +56,17 @@ extension MainFeedViewController: UITableViewDropDelegate {
let destIndexPath = dropCoordinator.destinationIndexPath else { let destIndexPath = dropCoordinator.destinationIndexPath else {
return return
} }
let isFolderDrop: Bool = { let isFolderDrop: Bool = {
if coordinator.nodeFor(destIndexPath)?.representedObject is Folder, let propCell = tableView.cellForRow(at: destIndexPath) { if coordinator.nodeFor(destIndexPath)?.representedObject is Folder, let propCell = tableView.cellForRow(at: destIndexPath) {
return dropCoordinator.session.location(in: propCell).y >= 0 return dropCoordinator.session.location(in: propCell).y >= 0
} }
return false return false
}() }()
// Based on the drop we have to determine a node to start looking for a parent container. // Based on the drop we have to determine a node to start looking for a parent container.
let destNode: Node? = { let destNode: Node? = {
if isFolderDrop { if isFolderDrop {
return coordinator.nodeFor(destIndexPath) return coordinator.nodeFor(destIndexPath)
} else { } else {
@ -78,7 +78,7 @@ extension MainFeedViewController: UITableViewDropDelegate {
return nil return nil
} }
} }
}() }()
// Now we start looking for the parent container // Now we start looking for the parent container
@ -90,9 +90,9 @@ extension MainFeedViewController: UITableViewDropDelegate {
return coordinator.rootNode.childAtIndex(destIndexPath.section)?.representedObject as? Account return coordinator.rootNode.childAtIndex(destIndexPath.section)?.representedObject as? Account
} }
}() }()
guard let destination = destinationContainer, let feed = dragNode.representedObject as? Feed else { return } guard let destination = destinationContainer, let feed = dragNode.representedObject as? Feed else { return }
if source.account == destination.account { if source.account == destination.account {
moveFeedInAccount(feed: feed, sourceContainer: source, destinationContainer: destination) moveFeedInAccount(feed: feed, sourceContainer: source, destinationContainer: destination)
} else { } else {
@ -102,7 +102,7 @@ extension MainFeedViewController: UITableViewDropDelegate {
func moveFeedInAccount(feed: Feed, sourceContainer: Container, destinationContainer: Container) { func moveFeedInAccount(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
guard sourceContainer !== destinationContainer else { return } guard sourceContainer !== destinationContainer else { return }
BatchUpdate.shared.start() BatchUpdate.shared.start()
sourceContainer.account?.moveFeed(feed, from: sourceContainer, to: destinationContainer) { result in sourceContainer.account?.moveFeed(feed, from: sourceContainer, to: destinationContainer) { result in
BatchUpdate.shared.end() BatchUpdate.shared.end()
@ -114,11 +114,11 @@ extension MainFeedViewController: UITableViewDropDelegate {
} }
} }
} }
func moveFeedBetweenAccounts(feed: Feed, sourceContainer: Container, destinationContainer: Container) { func moveFeedBetweenAccounts(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
if let existingFeed = destinationContainer.account?.existingFeed(withURL: feed.url) { if let existingFeed = destinationContainer.account?.existingFeed(withURL: feed.url) {
BatchUpdate.shared.start() BatchUpdate.shared.start()
destinationContainer.account?.addFeed(existingFeed, to: destinationContainer) { result in destinationContainer.account?.addFeed(existingFeed, to: destinationContainer) { result in
switch result { switch result {
@ -137,9 +137,9 @@ extension MainFeedViewController: UITableViewDropDelegate {
self.presentError(error) self.presentError(error)
} }
} }
} else { } else {
BatchUpdate.shared.start() BatchUpdate.shared.start()
destinationContainer.account?.createFeed(url: feed.url, name: feed.editedName, container: destinationContainer, validateFeed: false) { result in destinationContainer.account?.createFeed(url: feed.url, name: feed.editedName, container: destinationContainer, validateFeed: false) { result in
switch result { switch result {
@ -158,9 +158,8 @@ extension MainFeedViewController: UITableViewDropDelegate {
self.presentError(error) self.presentError(error)
} }
} }
} }
} }
} }

View File

@ -125,7 +125,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
return return
} }
var node: Node? = nil var node: Node?
if let coordinator = unreadCountProvider as? SceneCoordinator, let feed = coordinator.timelineFeed { if let coordinator = unreadCountProvider as? SceneCoordinator, let feed = coordinator.timelineFeed {
node = coordinator.rootNode.descendantNodeRepresentingObject(feed as AnyObject) node = coordinator.rootNode.descendantNodeRepresentingObject(feed as AnyObject)
} else { } else {
@ -249,7 +249,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
} }
headerView.gestureRecognizers?.removeAll() headerView.gestureRecognizers?.removeAll()
let tap = UITapGestureRecognizer(target: self, action:#selector(self.toggleSectionHeader(_:))) let tap = UITapGestureRecognizer(target: self, action: #selector(self.toggleSectionHeader(_:)))
headerView.addGestureRecognizer(tap) headerView.addGestureRecognizer(tap)
// Without this the swipe gesture registers on the cell below // Without this the swipe gesture registers on the cell below
@ -279,7 +279,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
// Set up the delete action // Set up the delete action
let deleteTitle = NSLocalizedString("Delete", comment: "Delete") let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, completion) in let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (_, _, completion) in
self?.delete(indexPath: indexPath) self?.delete(indexPath: indexPath)
completion(true) completion(true)
} }
@ -288,7 +288,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
// Set up the rename action // Set up the rename action
let renameTitle = NSLocalizedString("Rename", comment: "Rename") let renameTitle = NSLocalizedString("Rename", comment: "Rename")
let renameAction = UIContextualAction(style: .normal, title: renameTitle) { [weak self] (action, view, completion) in let renameAction = UIContextualAction(style: .normal, title: renameTitle) { [weak self] (_, _, completion) in
self?.rename(indexPath: indexPath) self?.rename(indexPath: indexPath)
completion(true) completion(true)
} }
@ -354,7 +354,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
return makeFeedContextMenu(indexPath: indexPath, includeDeleteRename: true) return makeFeedContextMenu(indexPath: indexPath, includeDeleteRename: true)
} else if feed is Folder { } else if feed is Folder {
return makeFolderContextMenu(indexPath: indexPath) return makeFolderContextMenu(indexPath: indexPath)
} else if feed is PseudoFeed { } else if feed is PseudoFeed {
return makePseudoFeedContextMenu(indexPath: indexPath) return makePseudoFeedContextMenu(indexPath: indexPath)
} else { } else {
return nil return nil
@ -686,7 +686,7 @@ extension MainFeedViewController: UIContextMenuInteractionDelegate {
return nil return nil
} }
return UIContextMenuConfiguration(identifier: sectionIndex as NSCopying, previewProvider: nil) { suggestedActions in return UIContextMenuConfiguration(identifier: sectionIndex as NSCopying, previewProvider: nil) { _ in
var menuElements = [UIMenuElement]() var menuElements = [UIMenuElement]()
menuElements.append(UIMenu(title: "", options: .displayInline, children: [self.getAccountInfoAction(account: account)])) menuElements.append(UIMenu(title: "", options: .displayInline, children: [self.getAccountInfoAction(account: account)]))
@ -890,7 +890,7 @@ private extension MainFeedViewController {
} }
func makeFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration { func makeFeedContextMenu(indexPath: IndexPath, includeDeleteRename: Bool) -> UIContextMenuConfiguration {
return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [ weak self] suggestedActions in return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [ weak self] _ in
guard let self = self else { return nil } guard let self = self else { return nil }
@ -935,7 +935,7 @@ private extension MainFeedViewController {
} }
func makeFolderContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration { func makeFolderContextMenu(indexPath: IndexPath) -> UIContextMenuConfiguration {
return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [weak self] suggestedActions in return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { [weak self] _ in
guard let self = self else { return nil } guard let self = self else { return nil }
@ -962,7 +962,7 @@ private extension MainFeedViewController {
return nil return nil
} }
return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { suggestedActions in return UIContextMenuConfiguration(identifier: MainFeedRowIdentifier(indexPath: indexPath), previewProvider: nil, actionProvider: { _ in
return UIMenu(title: "", children: [markAllAction]) return UIMenu(title: "", children: [markAllAction])
}) })
} }
@ -973,7 +973,7 @@ private extension MainFeedViewController {
} }
let title = NSLocalizedString("Open Home Page", comment: "Open Home Page") let title = NSLocalizedString("Open Home Page", comment: "Open Home Page")
let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] _ in
self?.coordinator.showBrowserForFeed(indexPath) self?.coordinator.showBrowserForFeed(indexPath)
} }
return action return action
@ -985,7 +985,7 @@ private extension MainFeedViewController {
} }
let title = NSLocalizedString("Open Home Page", comment: "Open Home Page") let title = NSLocalizedString("Open Home Page", comment: "Open Home Page")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
self?.coordinator.showBrowserForFeed(indexPath) self?.coordinator.showBrowserForFeed(indexPath)
completion(true) completion(true)
} }
@ -999,7 +999,7 @@ private extension MainFeedViewController {
} }
let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL") let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL")
let action = UIAction(title: title, image: AppAssets.copyImage) { action in let action = UIAction(title: title, image: AppAssets.copyImage) { _ in
UIPasteboard.general.url = url UIPasteboard.general.url = url
} }
return action return action
@ -1012,7 +1012,7 @@ private extension MainFeedViewController {
} }
let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL") let title = NSLocalizedString("Copy Feed URL", comment: "Copy Feed URL")
let action = UIAlertAction(title: title, style: .default) { action in let action = UIAlertAction(title: title, style: .default) { _ in
UIPasteboard.general.url = url UIPasteboard.general.url = url
completion(true) completion(true)
} }
@ -1027,7 +1027,7 @@ private extension MainFeedViewController {
} }
let title = NSLocalizedString("Copy Home Page URL", comment: "Copy Home Page URL") let title = NSLocalizedString("Copy Home Page URL", comment: "Copy Home Page URL")
let action = UIAction(title: title, image: AppAssets.copyImage) { action in let action = UIAction(title: title, image: AppAssets.copyImage) { _ in
UIPasteboard.general.url = url UIPasteboard.general.url = url
} }
return action return action
@ -1041,7 +1041,7 @@ private extension MainFeedViewController {
} }
let title = NSLocalizedString("Copy Home Page URL", comment: "Copy Home Page URL") let title = NSLocalizedString("Copy Home Page URL", comment: "Copy Home Page URL")
let action = UIAlertAction(title: title, style: .default) { action in let action = UIAlertAction(title: title, style: .default) { _ in
UIPasteboard.general.url = url UIPasteboard.general.url = url
completion(true) completion(true)
} }
@ -1061,8 +1061,7 @@ private extension MainFeedViewController {
completion(true) completion(true)
} }
let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
let action = UIAlertAction(title: title, style: .default) { [weak self] action in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
self?.coordinator.markAllAsRead(Array(articles)) self?.coordinator.markAllAsRead(Array(articles))
completion(true) completion(true)
@ -1074,7 +1073,7 @@ private extension MainFeedViewController {
func deleteAction(indexPath: IndexPath) -> UIAction { func deleteAction(indexPath: IndexPath) -> UIAction {
let title = NSLocalizedString("Delete", comment: "Delete") let title = NSLocalizedString("Delete", comment: "Delete")
let action = UIAction(title: title, image: AppAssets.trashImage, attributes: .destructive) { [weak self] action in let action = UIAction(title: title, image: AppAssets.trashImage, attributes: .destructive) { [weak self] _ in
self?.delete(indexPath: indexPath) self?.delete(indexPath: indexPath)
} }
return action return action
@ -1082,7 +1081,7 @@ private extension MainFeedViewController {
func renameAction(indexPath: IndexPath) -> UIAction { func renameAction(indexPath: IndexPath) -> UIAction {
let title = NSLocalizedString("Rename", comment: "Rename") let title = NSLocalizedString("Rename", comment: "Rename")
let action = UIAction(title: title, image: AppAssets.editImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.editImage) { [weak self] _ in
self?.rename(indexPath: indexPath) self?.rename(indexPath: indexPath)
} }
return action return action
@ -1094,7 +1093,7 @@ private extension MainFeedViewController {
} }
let title = NSLocalizedString("Get Info", comment: "Get Info") let title = NSLocalizedString("Get Info", comment: "Get Info")
let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] _ in
self?.coordinator.showFeedInspector(for: feed) self?.coordinator.showFeedInspector(for: feed)
} }
return action return action
@ -1102,7 +1101,7 @@ private extension MainFeedViewController {
func getAccountInfoAction(account: Account) -> UIAction { func getAccountInfoAction(account: Account) -> UIAction {
let title = NSLocalizedString("Get Info", comment: "Get Info") let title = NSLocalizedString("Get Info", comment: "Get Info")
let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.infoImage) { [weak self] _ in
self?.coordinator.showAccountInspector(for: account) self?.coordinator.showAccountInspector(for: account)
} }
return action return action
@ -1110,7 +1109,7 @@ private extension MainFeedViewController {
func deactivateAccountAction(account: Account) -> UIAction { func deactivateAccountAction(account: Account) -> UIAction {
let title = NSLocalizedString("Deactivate", comment: "Deactivate") let title = NSLocalizedString("Deactivate", comment: "Deactivate")
let action = UIAction(title: title, image: AppAssets.deactivateImage) { action in let action = UIAction(title: title, image: AppAssets.deactivateImage) { _ in
account.isActive = false account.isActive = false
} }
return action return action
@ -1122,7 +1121,7 @@ private extension MainFeedViewController {
} }
let title = NSLocalizedString("Get Info", comment: "Get Info") let title = NSLocalizedString("Get Info", comment: "Get Info")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
self?.coordinator.showFeedInspector(for: feed) self?.coordinator.showFeedInspector(for: feed)
completion(true) completion(true)
} }
@ -1138,7 +1137,7 @@ private extension MainFeedViewController {
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
if let articles = try? feed.fetchUnreadArticles() { if let articles = try? feed.fetchUnreadArticles() {
self?.coordinator.markAllAsRead(Array(articles)) self?.coordinator.markAllAsRead(Array(articles))
@ -1156,7 +1155,7 @@ private extension MainFeedViewController {
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, account.nameForDisplay) as String let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, account.nameForDisplay) as String
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
// If you don't have this delay the screen flashes when it executes this code // If you don't have this delay the screen flashes when it executes this code
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
@ -1170,7 +1169,6 @@ private extension MainFeedViewController {
return action return action
} }
func rename(indexPath: IndexPath) { func rename(indexPath: IndexPath) {
guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem else { return } guard let feed = coordinator.nodeFor(indexPath)?.representedObject as? SidebarItem else { return }
@ -1183,7 +1181,7 @@ private extension MainFeedViewController {
alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
let renameTitle = NSLocalizedString("Rename", comment: "Rename") let renameTitle = NSLocalizedString("Rename", comment: "Rename")
let renameAction = UIAlertAction(title: renameTitle, style: .default) { [weak self] action in let renameAction = UIAlertAction(title: renameTitle, style: .default) { [weak self] _ in
guard let name = alertController.textFields?[0].text, !name.isEmpty else { guard let name = alertController.textFields?[0].text, !name.isEmpty else {
return return
@ -1214,7 +1212,7 @@ private extension MainFeedViewController {
alertController.addAction(renameAction) alertController.addAction(renameAction)
alertController.preferredAction = renameAction alertController.preferredAction = renameAction
alertController.addTextField() { textField in alertController.addTextField { textField in
textField.text = feed.nameForDisplay textField.text = feed.nameForDisplay
textField.placeholder = NSLocalizedString("Name", comment: "Name") textField.placeholder = NSLocalizedString("Name", comment: "Name")
} }
@ -1234,7 +1232,7 @@ private extension MainFeedViewController {
title = NSLocalizedString("Delete Folder", comment: "Delete folder") title = NSLocalizedString("Delete Folder", comment: "Delete folder")
let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” folder?", comment: "Folder delete text") let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” folder?", comment: "Folder delete text")
message = NSString.localizedStringWithFormat(localizedInformativeText as NSString, feed.nameForDisplay) as String message = NSString.localizedStringWithFormat(localizedInformativeText as NSString, feed.nameForDisplay) as String
} else { } else {
title = NSLocalizedString("Delete Feed", comment: "Delete feed") title = NSLocalizedString("Delete Feed", comment: "Delete feed")
let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” feed?", comment: "Feed delete text") let localizedInformativeText = NSLocalizedString("Are you sure you want to delete the “%@” feed?", comment: "Feed delete text")
message = NSString.localizedStringWithFormat(localizedInformativeText as NSString, feed.nameForDisplay) as String message = NSString.localizedStringWithFormat(localizedInformativeText as NSString, feed.nameForDisplay) as String
@ -1246,7 +1244,7 @@ private extension MainFeedViewController {
alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
let deleteTitle = NSLocalizedString("Delete", comment: "Delete") let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { [weak self] action in let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { [weak self] _ in
self?.performDelete(indexPath: indexPath) self?.performDelete(indexPath: indexPath)
} }
alertController.addAction(deleteAction) alertController.addAction(deleteAction)
@ -1284,6 +1282,6 @@ extension MainFeedViewController: UIGestureRecognizerDelegate {
return false return false
} }
let velocity = gestureRecognizer.velocity(in: self.view) let velocity = gestureRecognizer.velocity(in: self.view)
return abs(velocity.x) > abs(velocity.y); return abs(velocity.x) > abs(velocity.y)
} }
} }

View File

@ -10,10 +10,10 @@ import UIKit
import Account import Account
class RefreshProgressView: UIView { class RefreshProgressView: UIView {
@IBOutlet weak var progressView: UIProgressView! @IBOutlet weak var progressView: UIProgressView!
@IBOutlet weak var label: UILabel! @IBOutlet weak var label: UILabel!
override func awakeFromNib() { override func awakeFromNib() {
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .combinedRefreshProgressDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .combinedRefreshProgressDidChange, object: nil)
@ -24,7 +24,7 @@ class RefreshProgressView: UIView {
isAccessibilityElement = true isAccessibilityElement = true
accessibilityTraits = [.updatesFrequently, .notEnabled] accessibilityTraits = [.updatesFrequently, .notEnabled]
} }
func update() { func update() {
if !AccountManager.shared.combinedRefreshProgress.isComplete { if !AccountManager.shared.combinedRefreshProgress.isComplete {
progressChanged(animated: false) progressChanged(animated: false)
@ -50,7 +50,7 @@ class RefreshProgressView: UIView {
deinit { deinit {
NotificationCenter.default.removeObserver(self) NotificationCenter.default.removeObserver(self)
} }
} }
// MARK: Private // MARK: Private
@ -68,7 +68,7 @@ private extension RefreshProgressView {
if isInViewHierarchy { if isInViewHierarchy {
progressView.setProgress(1, animated: animated) progressView.setProgress(1, animated: animated)
} }
func completeLabel() { func completeLabel() {
// Check that there are no pending downloads. // Check that there are no pending downloads.
if AccountManager.shared.combinedRefreshProgress.isComplete { if AccountManager.shared.combinedRefreshProgress.isComplete {
@ -101,7 +101,7 @@ private extension RefreshProgressView {
} }
} }
} }
func updateRefreshLabel() { func updateRefreshLabel() {
if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime { if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime {
@ -131,5 +131,5 @@ private extension RefreshProgressView {
self?.scheduleUpdateRefreshLabel() self?.scheduleUpdateRefreshLabel()
} }
} }
} }

View File

@ -19,7 +19,7 @@ struct MainTimelineAccessibilityCellLayout: MainTimelineCellLayout {
let summaryRect: CGRect let summaryRect: CGRect
let feedNameRect: CGRect let feedNameRect: CGRect
let dateRect: CGRect let dateRect: CGRect
init(width: CGFloat, insets: UIEdgeInsets, cellData: MainTimelineCellData) { init(width: CGFloat, insets: UIEdgeInsets, cellData: MainTimelineCellData) {
var currentPoint = CGPoint.zero var currentPoint = CGPoint.zero
@ -40,13 +40,13 @@ struct MainTimelineAccessibilityCellLayout: MainTimelineCellLayout {
} else { } else {
self.iconImageRect = CGRect.zero self.iconImageRect = CGRect.zero
} }
let textAreaWidth = width - (currentPoint.x + MainTimelineDefaultCellLayout.cellPadding.right + insets.right) let textAreaWidth = width - (currentPoint.x + MainTimelineDefaultCellLayout.cellPadding.right + insets.right)
// Title Text Block // Title Text Block
let (titleRect, numberOfLinesForTitle) = MainTimelineAccessibilityCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth) let (titleRect, numberOfLinesForTitle) = MainTimelineAccessibilityCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth)
self.titleRect = titleRect self.titleRect = titleRect
// Summary Text Block // Summary Text Block
if self.titleRect != CGRect.zero { if self.titleRect != CGRect.zero {
currentPoint.y = self.titleRect.maxY + MainTimelineDefaultCellLayout.titleBottomMargin currentPoint.y = self.titleRect.maxY + MainTimelineDefaultCellLayout.titleBottomMargin
@ -54,21 +54,21 @@ struct MainTimelineAccessibilityCellLayout: MainTimelineCellLayout {
self.summaryRect = MainTimelineAccessibilityCellLayout.rectForSummary(cellData, currentPoint, textAreaWidth, numberOfLinesForTitle) self.summaryRect = MainTimelineAccessibilityCellLayout.rectForSummary(cellData, currentPoint, textAreaWidth, numberOfLinesForTitle)
currentPoint.y = [self.titleRect, self.summaryRect].maxY() currentPoint.y = [self.titleRect, self.summaryRect].maxY()
if cellData.showFeedName != .none { if cellData.showFeedName != .none {
self.feedNameRect = MainTimelineAccessibilityCellLayout.rectForFeedName(cellData, currentPoint, textAreaWidth) self.feedNameRect = MainTimelineAccessibilityCellLayout.rectForFeedName(cellData, currentPoint, textAreaWidth)
currentPoint.y = self.feedNameRect.maxY currentPoint.y = self.feedNameRect.maxY
} else { } else {
self.feedNameRect = CGRect.zero self.feedNameRect = CGRect.zero
} }
// Feed Name and Pub Date // Feed Name and Pub Date
self.dateRect = MainTimelineAccessibilityCellLayout.rectForDate(cellData, currentPoint, textAreaWidth) self.dateRect = MainTimelineAccessibilityCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)
self.height = self.dateRect.maxY + MainTimelineDefaultCellLayout.cellPadding.bottom self.height = self.dateRect.maxY + MainTimelineDefaultCellLayout.cellPadding.bottom
} }
} }
// MARK: - Calculate Rects // MARK: - Calculate Rects
@ -78,13 +78,13 @@ private extension MainTimelineAccessibilityCellLayout {
static func rectForDate(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect { static func rectForDate(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
var r = CGRect.zero var r = CGRect.zero
let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MainTimelineDefaultCellLayout.dateFont) let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MainTimelineDefaultCellLayout.dateFont)
r.size = size r.size = size
r.origin = point r.origin = point
return r return r
} }
} }

View File

@ -12,7 +12,7 @@ import Articles
struct MainTimelineCellData { struct MainTimelineCellData {
private static let noText = NSLocalizedString("(No Text)", comment: "No Text") private static let noText = NSLocalizedString("(No Text)", comment: "No Text")
let title: String let title: String
let attributedTitle: NSAttributedString let attributedTitle: NSAttributedString
let summary: String let summary: String
@ -38,16 +38,15 @@ struct MainTimelineCellData {
} else { } else {
self.summary = truncatedSummary self.summary = truncatedSummary
} }
self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished) self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished)
if let feedName = feedName { if let feedName = feedName {
self.feedName = ArticleStringFormatter.truncatedFeedName(feedName) self.feedName = ArticleStringFormatter.truncatedFeedName(feedName)
} } else {
else {
self.feedName = "" self.feedName = ""
} }
if let byline = byline { if let byline = byline {
self.byline = byline self.byline = byline
} else { } else {
@ -63,10 +62,10 @@ struct MainTimelineCellData {
self.starred = article.status.starred self.starred = article.status.starred
self.numberOfLines = numberOfLines self.numberOfLines = numberOfLines
self.iconSize = iconSize self.iconSize = iconSize
} }
init() { //Empty init() { // Empty
self.title = "" self.title = ""
self.attributedTitle = NSAttributedString() self.attributedTitle = NSAttributedString()
self.summary = "" self.summary = ""
@ -81,5 +80,5 @@ struct MainTimelineCellData {
self.numberOfLines = 0 self.numberOfLines = 0
self.iconSize = .medium self.iconSize = .medium
} }
} }

View File

@ -18,7 +18,7 @@ protocol MainTimelineCellLayout {
var summaryRect: CGRect {get} var summaryRect: CGRect {get}
var feedNameRect: CGRect {get} var feedNameRect: CGRect {get}
var dateRect: CGRect {get} var dateRect: CGRect {get}
} }
extension MainTimelineCellLayout { extension MainTimelineCellLayout {
@ -30,8 +30,7 @@ extension MainTimelineCellLayout {
r.origin.y = point.y + 5 r.origin.y = point.y + 5
return r return r
} }
static func rectForStar(_ point: CGPoint) -> CGRect { static func rectForStar(_ point: CGPoint) -> CGRect {
var r = CGRect.zero var r = CGRect.zero
r.size.width = MainTimelineDefaultCellLayout.starDimension r.size.width = MainTimelineDefaultCellLayout.starDimension
@ -40,7 +39,7 @@ extension MainTimelineCellLayout {
r.origin.y = point.y + 3 r.origin.y = point.y + 3
return r return r
} }
static func rectForIconView(_ point: CGPoint, iconSize: IconSize) -> CGRect { static func rectForIconView(_ point: CGPoint, iconSize: IconSize) -> CGRect {
var r = CGRect.zero var r = CGRect.zero
r.size = iconSize.size r.size = iconSize.size
@ -48,16 +47,16 @@ extension MainTimelineCellLayout {
r.origin.y = point.y + 4 r.origin.y = point.y + 4
return r return r
} }
static func rectForTitle(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> (CGRect, Int) { static func rectForTitle(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> (CGRect, Int) {
var r = CGRect.zero var r = CGRect.zero
if cellData.title.isEmpty { if cellData.title.isEmpty {
return (r, 0) return (r, 0)
} }
r.origin = point r.origin = point
let sizeInfo = MultilineUILabelSizer.size(for: cellData.title, font: MainTimelineDefaultCellLayout.titleFont, numberOfLines: cellData.numberOfLines, width: Int(textAreaWidth)) let sizeInfo = MultilineUILabelSizer.size(for: cellData.title, font: MainTimelineDefaultCellLayout.titleFont, numberOfLines: cellData.numberOfLines, width: Int(textAreaWidth))
r.size.width = textAreaWidth r.size.width = textAreaWidth
@ -65,22 +64,22 @@ extension MainTimelineCellLayout {
if sizeInfo.numberOfLinesUsed < 1 { if sizeInfo.numberOfLinesUsed < 1 {
r.size.height = 0 r.size.height = 0
} }
return (r, sizeInfo.numberOfLinesUsed) return (r, sizeInfo.numberOfLinesUsed)
} }
static func rectForSummary(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat, _ linesUsed: Int) -> CGRect { static func rectForSummary(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat, _ linesUsed: Int) -> CGRect {
let linesLeft = cellData.numberOfLines - linesUsed let linesLeft = cellData.numberOfLines - linesUsed
var r = CGRect.zero var r = CGRect.zero
if cellData.summary.isEmpty || linesLeft < 1 { if cellData.summary.isEmpty || linesLeft < 1 {
return r return r
} }
r.origin = point r.origin = point
let sizeInfo = MultilineUILabelSizer.size(for: cellData.summary, font: MainTimelineDefaultCellLayout.summaryFont, numberOfLines: linesLeft, width: Int(textAreaWidth)) let sizeInfo = MultilineUILabelSizer.size(for: cellData.summary, font: MainTimelineDefaultCellLayout.summaryFont, numberOfLines: linesLeft, width: Int(textAreaWidth))
r.size.width = textAreaWidth r.size.width = textAreaWidth
@ -88,26 +87,26 @@ extension MainTimelineCellLayout {
if sizeInfo.numberOfLinesUsed < 1 { if sizeInfo.numberOfLinesUsed < 1 {
r.size.height = 0 r.size.height = 0
} }
return r return r
} }
static func rectForFeedName(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect { static func rectForFeedName(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
var r = CGRect.zero var r = CGRect.zero
r.origin = point r.origin = point
let feedName = cellData.showFeedName == .feed ? cellData.feedName : cellData.byline let feedName = cellData.showFeedName == .feed ? cellData.feedName : cellData.byline
let size = SingleLineUILabelSizer.size(for: feedName, font: MainTimelineDefaultCellLayout.feedNameFont) let size = SingleLineUILabelSizer.size(for: feedName, font: MainTimelineDefaultCellLayout.feedNameFont)
r.size = size r.size = size
if r.size.width > textAreaWidth { if r.size.width > textAreaWidth {
r.size.width = textAreaWidth r.size.width = textAreaWidth
} }
return r return r
} }
} }

View File

@ -12,7 +12,7 @@ import RSCore
struct MainTimelineDefaultCellLayout: MainTimelineCellLayout { struct MainTimelineDefaultCellLayout: MainTimelineCellLayout {
static let cellPadding = UIEdgeInsets(top: 12, left: 8, bottom: 12, right: 20) static let cellPadding = UIEdgeInsets(top: 12, left: 8, bottom: 12, right: 20)
static let unreadCircleMarginLeft = CGFloat(integerLiteral: 0) static let unreadCircleMarginLeft = CGFloat(integerLiteral: 0)
static let unreadCircleDimension = CGFloat(integerLiteral: 12) static let unreadCircleDimension = CGFloat(integerLiteral: 12)
static let unreadCircleSize = CGSize(width: MainTimelineDefaultCellLayout.unreadCircleDimension, height: MainTimelineDefaultCellLayout.unreadCircleDimension) static let unreadCircleSize = CGSize(width: MainTimelineDefaultCellLayout.unreadCircleDimension, height: MainTimelineDefaultCellLayout.unreadCircleDimension)
@ -33,7 +33,7 @@ struct MainTimelineDefaultCellLayout: MainTimelineCellLayout {
return UIFont.preferredFont(forTextStyle: .footnote) return UIFont.preferredFont(forTextStyle: .footnote)
} }
static let feedRightMargin = CGFloat(integerLiteral: 8) static let feedRightMargin = CGFloat(integerLiteral: 8)
static var dateFont: UIFont { static var dateFont: UIFont {
return UIFont.preferredFont(forTextStyle: .footnote) return UIFont.preferredFont(forTextStyle: .footnote)
} }
@ -72,13 +72,13 @@ struct MainTimelineDefaultCellLayout: MainTimelineCellLayout {
} else { } else {
self.iconImageRect = CGRect.zero self.iconImageRect = CGRect.zero
} }
let textAreaWidth = width - (currentPoint.x + MainTimelineDefaultCellLayout.cellPadding.right + insets.right) let textAreaWidth = width - (currentPoint.x + MainTimelineDefaultCellLayout.cellPadding.right + insets.right)
// Title Text Block // Title Text Block
let (titleRect, numberOfLinesForTitle) = MainTimelineDefaultCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth) let (titleRect, numberOfLinesForTitle) = MainTimelineDefaultCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth)
self.titleRect = titleRect self.titleRect = titleRect
// Summary Text Block // Summary Text Block
if self.titleRect != CGRect.zero { if self.titleRect != CGRect.zero {
currentPoint.y = self.titleRect.maxY + MainTimelineDefaultCellLayout.titleBottomMargin currentPoint.y = self.titleRect.maxY + MainTimelineDefaultCellLayout.titleBottomMargin
@ -93,7 +93,7 @@ struct MainTimelineDefaultCellLayout: MainTimelineCellLayout {
y -= tmp.height y -= tmp.height
} }
currentPoint.y = y currentPoint.y = y
// Feed Name and Pub Date // Feed Name and Pub Date
self.dateRect = MainTimelineDefaultCellLayout.rectForDate(cellData, currentPoint, textAreaWidth) self.dateRect = MainTimelineDefaultCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)
@ -103,7 +103,7 @@ struct MainTimelineDefaultCellLayout: MainTimelineCellLayout {
self.height = [self.iconImageRect, self.feedNameRect].maxY() + MainTimelineDefaultCellLayout.cellPadding.bottom self.height = [self.iconImageRect, self.feedNameRect].maxY() + MainTimelineDefaultCellLayout.cellPadding.bottom
} }
} }
// MARK: - Calculate Rects // MARK: - Calculate Rects
@ -113,14 +113,14 @@ extension MainTimelineDefaultCellLayout {
static func rectForDate(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect { static func rectForDate(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
var r = CGRect.zero var r = CGRect.zero
let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MainTimelineDefaultCellLayout.dateFont) let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MainTimelineDefaultCellLayout.dateFont)
r.size = size r.size = size
r.origin.x = (point.x + textAreaWidth) - size.width r.origin.x = (point.x + textAreaWidth) - size.width
r.origin.y = point.y r.origin.y = point.y
return r return r
} }
} }

View File

@ -18,11 +18,11 @@ class MainTimelineTableViewCell: VibrantTableViewCell {
private let feedNameView = MainTimelineTableViewCell.singleLineUILabel() private let feedNameView = MainTimelineTableViewCell.singleLineUILabel()
private lazy var iconView = IconView() private lazy var iconView = IconView()
private lazy var starView = { private lazy var starView = {
return NonIntrinsicImageView(image: AppAssets.timelineStarImage) return NonIntrinsicImageView(image: AppAssets.timelineStarImage)
}() }()
private var unreadIndicatorPropertyAnimator: UIViewPropertyAnimator? private var unreadIndicatorPropertyAnimator: UIViewPropertyAnimator?
private var starViewPropertyAnimator: UIViewPropertyAnimator? private var starViewPropertyAnimator: UIViewPropertyAnimator?
@ -31,12 +31,12 @@ class MainTimelineTableViewCell: VibrantTableViewCell {
updateSubviews() updateSubviews()
} }
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
override func prepareForReuse() { override func prepareForReuse() {
unreadIndicatorPropertyAnimator?.stopAnimation(true) unreadIndicatorPropertyAnimator?.stopAnimation(true)
unreadIndicatorPropertyAnimator = nil unreadIndicatorPropertyAnimator = nil
@ -46,19 +46,19 @@ class MainTimelineTableViewCell: VibrantTableViewCell {
starViewPropertyAnimator = nil starViewPropertyAnimator = nil
starView.isHidden = true starView.isHidden = true
} }
override var frame: CGRect { override var frame: CGRect {
didSet { didSet {
setNeedsLayout() setNeedsLayout()
} }
} }
override func updateVibrancy(animated: Bool) { override func updateVibrancy(animated: Bool) {
updateLabelVibrancy(titleView, color: labelColor, animated: animated) updateLabelVibrancy(titleView, color: labelColor, animated: animated)
updateLabelVibrancy(summaryView, color: labelColor, animated: animated) updateLabelVibrancy(summaryView, color: labelColor, animated: animated)
updateLabelVibrancy(dateView, color: secondaryLabelColor, animated: animated) updateLabelVibrancy(dateView, color: secondaryLabelColor, animated: animated)
updateLabelVibrancy(feedNameView, color: secondaryLabelColor, animated: animated) updateLabelVibrancy(feedNameView, color: secondaryLabelColor, animated: animated)
if animated { if animated {
UIView.animate(withDuration: Self.duration) { UIView.animate(withDuration: Self.duration) {
if self.isHighlighted || self.isSelected { if self.isHighlighted || self.isSelected {
@ -75,16 +75,16 @@ class MainTimelineTableViewCell: VibrantTableViewCell {
} }
} }
} }
override func sizeThatFits(_ size: CGSize) -> CGSize { override func sizeThatFits(_ size: CGSize) -> CGSize {
let layout = updatedLayout(width: size.width) let layout = updatedLayout(width: size.width)
return CGSize(width: size.width, height: layout.height) return CGSize(width: size.width, height: layout.height)
} }
override func layoutSubviews() { override func layoutSubviews() {
super.layoutSubviews() super.layoutSubviews()
let layout = updatedLayout(width: bounds.width) let layout = updatedLayout(width: bounds.width)
unreadIndicatorView.setFrameIfNotEqual(layout.unreadIndicatorRect) unreadIndicatorView.setFrameIfNotEqual(layout.unreadIndicatorRect)
@ -97,11 +97,11 @@ class MainTimelineTableViewCell: VibrantTableViewCell {
separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0) separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
} }
func setIconImage(_ image: IconImage) { func setIconImage(_ image: IconImage) {
iconView.iconImage = image iconView.iconImage = image
} }
} }
// MARK: - Private // MARK: - Private
@ -115,7 +115,7 @@ private extension MainTimelineTableViewCell {
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
return label return label
} }
static func multiLineUILabel() -> UILabel { static func multiLineUILabel() -> UILabel {
let label = NonIntrinsicLabel() let label = NonIntrinsicLabel()
label.numberOfLines = 0 label.numberOfLines = 0
@ -124,16 +124,16 @@ private extension MainTimelineTableViewCell {
label.adjustsFontForContentSizeCategory = true label.adjustsFontForContentSizeCategory = true
return label return label
} }
func setFrame(for label: UILabel, rect: CGRect) { func setFrame(for label: UILabel, rect: CGRect) {
if Int(floor(rect.height)) == 0 || Int(floor(rect.width)) == 0 { if Int(floor(rect.height)) == 0 || Int(floor(rect.width)) == 0 {
hideView(label) hideView(label)
} else { } else {
showView(label) showView(label)
label.setFrameIfNotEqual(rect) label.setFrameIfNotEqual(rect)
} }
} }
func addSubviewAtInit(_ view: UIView, hidden: Bool) { func addSubviewAtInit(_ view: UIView, hidden: Bool) {
@ -141,9 +141,9 @@ private extension MainTimelineTableViewCell {
view.translatesAutoresizingMaskIntoConstraints = false view.translatesAutoresizingMaskIntoConstraints = false
view.isHidden = hidden view.isHidden = hidden
} }
func commonInit() { func commonInit() {
addSubviewAtInit(titleView, hidden: false) addSubviewAtInit(titleView, hidden: false)
addSubviewAtInit(summaryView, hidden: true) addSubviewAtInit(summaryView, hidden: true)
addSubviewAtInit(unreadIndicatorView, hidden: true) addSubviewAtInit(unreadIndicatorView, hidden: true)
@ -152,7 +152,7 @@ private extension MainTimelineTableViewCell {
addSubviewAtInit(iconView, hidden: true) addSubviewAtInit(iconView, hidden: true)
addSubviewAtInit(starView, hidden: true) addSubviewAtInit(starView, hidden: true)
} }
func updatedLayout(width: CGFloat) -> MainTimelineCellLayout { func updatedLayout(width: CGFloat) -> MainTimelineCellLayout {
if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory { if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory {
return MainTimelineAccessibilityCellLayout(width: width, insets: safeAreaInsets, cellData: cellData) return MainTimelineAccessibilityCellLayout(width: width, insets: safeAreaInsets, cellData: cellData)
@ -160,25 +160,25 @@ private extension MainTimelineTableViewCell {
return MainTimelineDefaultCellLayout(width: width, insets: safeAreaInsets, cellData: cellData) return MainTimelineDefaultCellLayout(width: width, insets: safeAreaInsets, cellData: cellData)
} }
} }
func updateTitleView() { func updateTitleView() {
titleView.font = MainTimelineDefaultCellLayout.titleFont titleView.font = MainTimelineDefaultCellLayout.titleFont
titleView.textColor = labelColor titleView.textColor = labelColor
updateTextFieldAttributedText(titleView, cellData?.attributedTitle) updateTextFieldAttributedText(titleView, cellData?.attributedTitle)
} }
func updateSummaryView() { func updateSummaryView() {
summaryView.font = MainTimelineDefaultCellLayout.summaryFont summaryView.font = MainTimelineDefaultCellLayout.summaryFont
summaryView.textColor = labelColor summaryView.textColor = labelColor
updateTextFieldText(summaryView, cellData?.summary) updateTextFieldText(summaryView, cellData?.summary)
} }
func updateDateView() { func updateDateView() {
dateView.font = MainTimelineDefaultCellLayout.dateFont dateView.font = MainTimelineDefaultCellLayout.dateFont
dateView.textColor = secondaryLabelColor dateView.textColor = secondaryLabelColor
updateTextFieldText(dateView, cellData.dateString) updateTextFieldText(dateView, cellData.dateString)
} }
func updateTextFieldText(_ label: UILabel, _ text: String?) { func updateTextFieldText(_ label: UILabel, _ text: String?) {
let s = text ?? "" let s = text ?? ""
if label.text != s { if label.text != s {
@ -199,7 +199,7 @@ private extension MainTimelineTableViewCell {
setNeedsLayout() setNeedsLayout()
} }
} }
func updateFeedNameView() { func updateFeedNameView() {
switch cellData.showFeedName { switch cellData.showFeedName {
case .feed: case .feed:
@ -216,7 +216,7 @@ private extension MainTimelineTableViewCell {
hideView(feedNameView) hideView(feedNameView)
} }
} }
func updateUnreadIndicator() { func updateUnreadIndicator() {
if !unreadIndicatorView.isHidden && cellData.read && !cellData.starred { if !unreadIndicatorView.isHidden && cellData.read && !cellData.starred {
unreadIndicatorPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in unreadIndicatorPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in
@ -233,7 +233,7 @@ private extension MainTimelineTableViewCell {
showOrHideView(unreadIndicatorView, cellData.read || cellData.starred) showOrHideView(unreadIndicatorView, cellData.read || cellData.starred)
} }
} }
func updateStarView() { func updateStarView() {
if !starView.isHidden && cellData.read && !cellData.starred { if !starView.isHidden && cellData.read && !cellData.starred {
starViewPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in starViewPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in
@ -250,7 +250,7 @@ private extension MainTimelineTableViewCell {
showOrHideView(starView, !cellData.starred) showOrHideView(starView, !cellData.starred)
} }
} }
func updateIconImage() { func updateIconImage() {
guard let image = cellData.iconImage, cellData.showIcon else { guard let image = cellData.iconImage, cellData.showIcon else {
makeIconEmpty() makeIconEmpty()
@ -258,20 +258,20 @@ private extension MainTimelineTableViewCell {
} }
showView(iconView) showView(iconView)
if iconView.iconImage !== cellData.iconImage { if iconView.iconImage !== cellData.iconImage {
iconView.iconImage = image iconView.iconImage = image
setNeedsLayout() setNeedsLayout()
} }
} }
func updateAccessiblityLabel() { func updateAccessiblityLabel() {
let starredStatus = cellData.starred ? "\(NSLocalizedString("Starred", comment: "Starred article for accessibility")), " : "" let starredStatus = cellData.starred ? "\(NSLocalizedString("Starred", comment: "Starred article for accessibility")), " : ""
let unreadStatus = cellData.read ? "" : "\(NSLocalizedString("Unread", comment: "Unread")), " let unreadStatus = cellData.read ? "" : "\(NSLocalizedString("Unread", comment: "Unread")), "
let label = starredStatus + unreadStatus + "\(cellData.feedName), \(cellData.title), \(cellData.summary), \(cellData.dateString)" let label = starredStatus + unreadStatus + "\(cellData.feedName), \(cellData.title), \(cellData.summary), \(cellData.dateString)"
accessibilityLabel = label accessibilityLabel = label
} }
func makeIconEmpty() { func makeIconEmpty() {
if iconView.iconImage != nil { if iconView.iconImage != nil {
iconView.iconImage = nil iconView.iconImage = nil
@ -279,23 +279,23 @@ private extension MainTimelineTableViewCell {
} }
hideView(iconView) hideView(iconView)
} }
func hideView(_ view: UIView) { func hideView(_ view: UIView) {
if !view.isHidden { if !view.isHidden {
view.isHidden = true view.isHidden = true
} }
} }
func showView(_ view: UIView) { func showView(_ view: UIView) {
if view.isHidden { if view.isHidden {
view.isHidden = false view.isHidden = false
} }
} }
func showOrHideView(_ view: UIView, _ shouldHide: Bool) { func showOrHideView(_ view: UIView, _ shouldHide: Bool) {
shouldHide ? hideView(view) : showView(view) shouldHide ? hideView(view) : showView(view)
} }
func updateSubviews() { func updateSubviews() {
updateTitleView() updateTitleView()
updateSummaryView() updateSummaryView()
@ -306,5 +306,5 @@ private extension MainTimelineTableViewCell {
updateIconImage() updateIconImage()
updateAccessiblityLabel() updateAccessiblityLabel()
} }
} }

View File

@ -15,5 +15,5 @@ class MainUnreadIndicatorView: UIView {
layer.cornerRadius = frame.size.width / 2.0 layer.cornerRadius = frame.size.width / 2.0
clipsToBounds = true clipsToBounds = true
} }
} }

View File

@ -42,7 +42,7 @@ final class MultilineUILabelSizer {
self.singleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/y", 200, font) self.singleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/y", 200, font)
self.doubleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/y\nAqLjJ0/y", 200, font) self.doubleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/y\nAqLjJ0/y", 200, font)
} }
static func size(for string: String, font: UIFont, numberOfLines: Int, width: Int) -> TextFieldSizeInfo { static func size(for string: String, font: UIFont, numberOfLines: Int, width: Int) -> TextFieldSizeInfo {
@ -52,7 +52,7 @@ final class MultilineUILabelSizer {
static func emptyCache() { static func emptyCache() {
sizers = [UILabelSizerSpecifier: MultilineUILabelSizer]() sizers = [UILabelSizerSpecifier: MultilineUILabelSizer]()
} }
} }
// MARK: - Private // MARK: - Private
@ -69,7 +69,7 @@ private extension MultilineUILabelSizer {
let newSizer = MultilineUILabelSizer(numberOfLines: numberOfLines, font: font) let newSizer = MultilineUILabelSizer(numberOfLines: numberOfLines, font: font)
sizers[specifier] = newSizer sizers[specifier] = newSizer
return newSizer return newSizer
} }
func sizeInfo(for string: String, width: Int) -> TextFieldSizeInfo { func sizeInfo(for string: String, width: Int) -> TextFieldSizeInfo {
@ -80,7 +80,7 @@ private extension MultilineUILabelSizer {
let size = CGSize(width: width, height: textFieldHeight) let size = CGSize(width: width, height: textFieldHeight)
let sizeInfo = TextFieldSizeInfo(size: size, numberOfLinesUsed: numberOfLinesUsed) let sizeInfo = TextFieldSizeInfo(size: size, numberOfLinesUsed: numberOfLinesUsed)
return sizeInfo return sizeInfo
} }
func height(for string: String, width: Int) -> Int { func height(for string: String, width: Int) -> Int {
@ -98,14 +98,14 @@ private extension MultilineUILabelSizer {
} }
var height = MultilineUILabelSizer.calculateHeight(string, width, font) var height = MultilineUILabelSizer.calculateHeight(string, width, font)
if numberOfLines != 0 { if numberOfLines != 0 {
let maxHeight = singleLineHeightEstimate * numberOfLines let maxHeight = singleLineHeightEstimate * numberOfLines
if height > maxHeight { if height > maxHeight {
height = maxHeight height = maxHeight
} }
} }
cache[string]![width] = height cache[string]![width] = height
return height return height
@ -123,7 +123,7 @@ private extension MultilineUILabelSizer {
let averageHeight = CGFloat(doubleLineHeightEstimate) / 2.0 let averageHeight = CGFloat(doubleLineHeightEstimate) / 2.0
let lines = Int(round(CGFloat(height) / averageHeight)) let lines = Int(round(CGFloat(height) / averageHeight))
return lines return lines
} }
func heightIsProbablySingleLineHeight(_ height: Int) -> Bool { func heightIsProbablySingleLineHeight(_ height: Int) -> Bool {
@ -140,7 +140,7 @@ private extension MultilineUILabelSizer {
let minimum = estimate - slop let minimum = estimate - slop
let maximum = estimate + slop let maximum = estimate + slop
return height >= minimum && height <= maximum return height >= minimum && height <= maximum
} }
func heightConsideringNeighbors(_ heightCache: WidthHeightCache, _ width: Int) -> Int? { func heightConsideringNeighbors(_ heightCache: WidthHeightCache, _ width: Int) -> Int? {
@ -165,8 +165,7 @@ private extension MultilineUILabelSizer {
if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) { if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) {
smallNeighbor = (oneWidth, oneHeight) smallNeighbor = (oneWidth, oneHeight)
} } else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) {
else if oneWidth > width && (oneWidth < largeNeighbor.width || largeNeighbor.width == 0) {
largeNeighbor = (oneWidth, oneHeight) largeNeighbor = (oneWidth, oneHeight)
} }
@ -176,7 +175,7 @@ private extension MultilineUILabelSizer {
} }
return nil return nil
} }
} }

View File

@ -30,10 +30,10 @@ final class SingleLineUILabelSizer {
let height = text.height(withConstrainedWidth: .greatestFiniteMagnitude, font: font) let height = text.height(withConstrainedWidth: .greatestFiniteMagnitude, font: font)
let width = text.width(withConstrainedHeight: .greatestFiniteMagnitude, font: font) let width = text.width(withConstrainedHeight: .greatestFiniteMagnitude, font: font)
let calculatedSize = CGSize(width: ceil(width), height: ceil(height)) let calculatedSize = CGSize(width: ceil(width), height: ceil(height))
cache[text] = calculatedSize cache[text] = calculatedSize
return calculatedSize return calculatedSize
} }
static private var sizers = [UIFont: SingleLineUILabelSizer]() static private var sizers = [UIFont: SingleLineUILabelSizer]()
@ -48,7 +48,7 @@ final class SingleLineUILabelSizer {
sizers[font] = newSizer sizers[font] = newSizer
return newSizer return newSizer
} }
// Use this call. Its easiest. // Use this call. Its easiest.
@ -60,5 +60,5 @@ final class SingleLineUILabelSizer {
static func emptyCache() { static func emptyCache() {
sizers = [UIFont: SingleLineUILabelSizer]() sizers = [UIFont: SingleLineUILabelSizer]()
} }
} }

View File

@ -8,11 +8,10 @@
import UIKit import UIKit
class MainTimelineDataSource<SectionIdentifierType, ItemIdentifierType>: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType : Hashable, ItemIdentifierType : Hashable { class MainTimelineDataSource<SectionIdentifierType, ItemIdentifierType>: UITableViewDiffableDataSource<SectionIdentifierType, ItemIdentifierType> where SectionIdentifierType: Hashable, ItemIdentifierType: Hashable {
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return true return true
} }
}
}

View File

@ -24,8 +24,7 @@ class MainTimelineTitleView: UIView {
if let name = label.text { if let name = label.text {
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility") let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility")
return "\(name) \(unreadCountView.unreadCount) \(unreadLabel)" return "\(name) \(unreadCountView.unreadCount) \(unreadLabel)"
} } else {
else {
return nil return nil
} }
} }
@ -36,7 +35,7 @@ class MainTimelineTitleView: UIView {
accessibilityTraits = .button accessibilityTraits = .button
addInteraction(pointerInteraction) addInteraction(pointerInteraction)
} }
func debuttonize() { func debuttonize() {
heightAnchor.constraint(equalToConstant: 40.0).isActive = true heightAnchor.constraint(equalToConstant: 40.0).isActive = true
accessibilityTraits.remove(.button) accessibilityTraits.remove(.button)
@ -45,7 +44,7 @@ class MainTimelineTitleView: UIView {
} }
extension MainTimelineTitleView: UIPointerInteractionDelegate { extension MainTimelineTitleView: UIPointerInteractionDelegate {
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? { func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
var rect = self.frame var rect = self.frame
rect.origin.x = rect.origin.x - 10 rect.origin.x = rect.origin.x - 10

View File

@ -17,11 +17,11 @@ class MainTimelineUnreadCountView: MainFeedUnreadCountView {
override var textColor: UIColor { override var textColor: UIColor {
return UIColor.systemBackground return UIColor.systemBackground
} }
override var intrinsicContentSize: CGSize { override var intrinsicContentSize: CGSize {
return contentSize return contentSize
} }
override func draw(_ dirtyRect: CGRect) { override func draw(_ dirtyRect: CGRect) {
let cornerRadii = CGSize(width: cornerRadius, height: cornerRadius) let cornerRadii = CGSize(width: cornerRadius, height: cornerRadius)
@ -33,7 +33,7 @@ class MainTimelineUnreadCountView: MainFeedUnreadCountView {
if unreadCount > 0 { if unreadCount > 0 {
unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes) unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes)
} }
} }
} }

View File

@ -14,21 +14,20 @@ extension CGRect: MarkAsReadAlertControllerSourceType {}
extension UIView: MarkAsReadAlertControllerSourceType {} extension UIView: MarkAsReadAlertControllerSourceType {}
extension UIBarButtonItem: MarkAsReadAlertControllerSourceType {} extension UIBarButtonItem: MarkAsReadAlertControllerSourceType {}
struct MarkAsReadAlertController { struct MarkAsReadAlertController {
static func confirm<T>(_ controller: UIViewController?, static func confirm<T>(_ controller: UIViewController?,
coordinator: SceneCoordinator?, coordinator: SceneCoordinator?,
confirmTitle: String, confirmTitle: String,
sourceType: T, sourceType: T,
cancelCompletion: (() -> Void)? = nil, cancelCompletion: (() -> Void)? = nil,
completion: @escaping () -> Void) where T: MarkAsReadAlertControllerSourceType { completion: @escaping () -> Void) where T: MarkAsReadAlertControllerSourceType {
guard let controller = controller, let coordinator = coordinator else { guard let controller = controller, let coordinator = coordinator else {
completion() completion()
return return
} }
if AppDefaults.shared.confirmMarkAllAsRead { if AppDefaults.shared.confirmMarkAllAsRead {
let alertController = MarkAsReadAlertController.alert(coordinator: coordinator, confirmTitle: confirmTitle, cancelCompletion: cancelCompletion, sourceType: sourceType) { _ in let alertController = MarkAsReadAlertController.alert(coordinator: coordinator, confirmTitle: confirmTitle, cancelCompletion: cancelCompletion, sourceType: sourceType) { _ in
completion() completion()
@ -38,20 +37,19 @@ struct MarkAsReadAlertController {
completion() completion()
} }
} }
private static func alert<T>(coordinator: SceneCoordinator, private static func alert<T>(coordinator: SceneCoordinator,
confirmTitle: String, confirmTitle: String,
cancelCompletion: (() -> Void)?, cancelCompletion: (() -> Void)?,
sourceType: T, sourceType: T,
completion: @escaping (UIAlertAction) -> Void) -> UIAlertController where T: MarkAsReadAlertControllerSourceType { completion: @escaping (UIAlertAction) -> Void) -> UIAlertController where T: MarkAsReadAlertControllerSourceType {
let title = NSLocalizedString("Mark As Read", comment: "Mark As Read") let title = NSLocalizedString("Mark As Read", comment: "Mark As Read")
let message = NSLocalizedString("You can turn this confirmation off in Settings.", let message = NSLocalizedString("You can turn this confirmation off in Settings.",
comment: "You can turn this confirmation off in Settings.") comment: "You can turn this confirmation off in Settings.")
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
let settingsTitle = NSLocalizedString("Open Settings", comment: "Open Settings") let settingsTitle = NSLocalizedString("Open Settings", comment: "Open Settings")
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet) let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in
cancelCompletion?() cancelCompletion?()
@ -60,24 +58,24 @@ struct MarkAsReadAlertController {
coordinator.showSettings(scrollToArticlesSection: true) coordinator.showSettings(scrollToArticlesSection: true)
} }
let markAction = UIAlertAction(title: confirmTitle, style: .default, handler: completion) let markAction = UIAlertAction(title: confirmTitle, style: .default, handler: completion)
alertController.addAction(markAction) alertController.addAction(markAction)
alertController.addAction(settingsAction) alertController.addAction(settingsAction)
alertController.addAction(cancelAction) alertController.addAction(cancelAction)
if let barButtonItem = sourceType as? UIBarButtonItem { if let barButtonItem = sourceType as? UIBarButtonItem {
alertController.popoverPresentationController?.barButtonItem = barButtonItem alertController.popoverPresentationController?.barButtonItem = barButtonItem
} }
if let rect = sourceType as? CGRect { if let rect = sourceType as? CGRect {
alertController.popoverPresentationController?.sourceRect = rect alertController.popoverPresentationController?.sourceRect = rect
} }
if let view = sourceType as? UIView { if let view = sourceType as? UIView {
alertController.popoverPresentationController?.sourceView = view alertController.popoverPresentationController?.sourceView = view
} }
return alertController return alertController
} }
} }

View File

@ -17,8 +17,8 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
private var numberOfTextLines = 0 private var numberOfTextLines = 0
private var iconSize = IconSize.medium private var iconSize = IconSize.medium
private lazy var feedTapGestureRecognizer = UITapGestureRecognizer(target: self, action:#selector(showFeedInspector(_:))) private lazy var feedTapGestureRecognizer = UITapGestureRecognizer(target: self, action: #selector(showFeedInspector(_:)))
private var refreshProgressView: RefreshProgressView? private var refreshProgressView: RefreshProgressView?
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem! @IBOutlet weak var markAllAsReadButton: UIBarButtonItem!
@ -28,28 +28,28 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
private lazy var dataSource = makeDataSource() private lazy var dataSource = makeDataSource()
private let searchController = UISearchController(searchResultsController: nil) private let searchController = UISearchController(searchResultsController: nil)
weak var coordinator: SceneCoordinator! weak var coordinator: SceneCoordinator!
var undoableCommands = [UndoableCommand]() var undoableCommands = [UndoableCommand]()
let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0) let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0)
private let keyboardManager = KeyboardManager(type: .timeline) private let keyboardManager = KeyboardManager(type: .timeline)
override var keyCommands: [UIKeyCommand]? { override var keyCommands: [UIKeyCommand]? {
// If the first responder is the WKWebView we don't want to supply any keyboard // If the first responder is the WKWebView we don't want to supply any keyboard
// commands that the system is looking for by going up the responder chain. They will interfere with // commands that the system is looking for by going up the responder chain. They will interfere with
// the WKWebViews built in hardware keyboard shortcuts, specifically the up and down arrow keys. // the WKWebViews built in hardware keyboard shortcuts, specifically the up and down arrow keys.
guard let current = UIResponder.currentFirstResponder, !(current is WKWebView) else { return nil } guard let current = UIResponder.currentFirstResponder, !(current is WKWebView) else { return nil }
return keyboardManager.keyCommands return keyboardManager.keyCommands
} }
override var canBecomeFirstResponder: Bool { override var canBecomeFirstResponder: Bool {
return true return true
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
@ -68,11 +68,11 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange), name: .DisplayNameDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange), name: .DisplayNameDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
// Initialize Programmatic Buttons // Initialize Programmatic Buttons
filterButton = UIBarButtonItem(image: AppAssets.filterInactiveImage, style: .plain, target: self, action: #selector(toggleFilter(_:))) filterButton = UIBarButtonItem(image: AppAssets.filterInactiveImage, style: .plain, target: self, action: #selector(toggleFilter(_:)))
firstUnreadButton = UIBarButtonItem(image: AppAssets.nextUnreadArticleImage, style: .plain, target: self, action: #selector(firstUnread(_:))) firstUnreadButton = UIBarButtonItem(image: AppAssets.nextUnreadArticleImage, style: .plain, target: self, action: #selector(firstUnread(_:)))
// Setup the Search Controller // Setup the Search Controller
searchController.delegate = self searchController.delegate = self
searchController.searchResultsUpdater = self searchController.searchResultsUpdater = self
@ -97,27 +97,27 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
if let titleView = Bundle.main.loadNibNamed("MainTimelineTitleView", owner: self, options: nil)?[0] as? MainTimelineTitleView { if let titleView = Bundle.main.loadNibNamed("MainTimelineTitleView", owner: self, options: nil)?[0] as? MainTimelineTitleView {
navigationItem.titleView = titleView navigationItem.titleView = titleView
} }
refreshControl = UIRefreshControl() refreshControl = UIRefreshControl()
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged) refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
configureToolbar() configureToolbar()
resetUI(resetScroll: true) resetUI(resetScroll: true)
// Load the table and then scroll to the saved position if available // Load the table and then scroll to the saved position if available
applyChanges(animated: false) { applyChanges(animated: false) {
if let restoreIndexPath = self.coordinator.timelineMiddleIndexPath { if let restoreIndexPath = self.coordinator.timelineMiddleIndexPath {
self.tableView.scrollToRow(at: restoreIndexPath, at: .middle, animated: false) self.tableView.scrollToRow(at: restoreIndexPath, at: .middle, animated: false)
} }
} }
// Disable swipe back on iPad Mice // Disable swipe back on iPad Mice
guard let gesture = self.navigationController?.interactivePopGestureRecognizer as? UIPanGestureRecognizer else { guard let gesture = self.navigationController?.interactivePopGestureRecognizer as? UIPanGestureRecognizer else {
return return
} }
gesture.allowedScrollTypesMask = [] gesture.allowedScrollTypesMask = []
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
self.navigationController?.isToolbarHidden = false self.navigationController?.isToolbarHidden = false
@ -125,10 +125,10 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
if navigationController?.navigationBar.isHidden ?? false { if navigationController?.navigationBar.isHidden ?? false {
navigationController?.navigationBar.alpha = 0 navigationController?.navigationBar.alpha = 0
} }
super.viewWillAppear(animated) super.viewWillAppear(animated)
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(true) super.viewDidAppear(true)
coordinator.isTimelineViewControllerPending = false coordinator.isTimelineViewControllerPending = false
@ -139,9 +139,9 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
} }
} }
} }
// MARK: Actions // MARK: Actions
@objc func openInBrowser(_ sender: Any?) { @objc func openInBrowser(_ sender: Any?) {
coordinator.showBrowserForCurrentArticle() coordinator.showBrowserForCurrentArticle()
} }
@ -149,35 +149,35 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
@objc func openInAppBrowser(_ sender: Any?) { @objc func openInAppBrowser(_ sender: Any?) {
coordinator.showInAppBrowser() coordinator.showInAppBrowser()
} }
@IBAction func toggleFilter(_ sender: Any) { @IBAction func toggleFilter(_ sender: Any) {
coordinator.toggleReadArticlesFilter() coordinator.toggleReadArticlesFilter()
} }
@IBAction func markAllAsRead(_ sender: Any) { @IBAction func markAllAsRead(_ sender: Any) {
let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read") let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")
if let source = sender as? UIBarButtonItem { if let source = sender as? UIBarButtonItem {
MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: source) { [weak self] in MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: source) { [weak self] in
self?.coordinator.markAllAsReadInTimeline() self?.coordinator.markAllAsReadInTimeline()
} }
} }
if let _ = sender as? UIKeyCommand { if let _ = sender as? UIKeyCommand {
guard let indexPath = tableView.indexPathForSelectedRow, let contentView = tableView.cellForRow(at: indexPath)?.contentView else { guard let indexPath = tableView.indexPathForSelectedRow, let contentView = tableView.cellForRow(at: indexPath)?.contentView else {
return return
} }
MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
self?.coordinator.markAllAsReadInTimeline() self?.coordinator.markAllAsReadInTimeline()
} }
} }
} }
@IBAction func firstUnread(_ sender: Any) { @IBAction func firstUnread(_ sender: Any) {
coordinator.selectFirstUnread() coordinator.selectFirstUnread()
} }
@objc func refreshAccounts(_ sender: Any) { @objc func refreshAccounts(_ sender: Any) {
refreshControl?.endRefreshing() refreshControl?.endRefreshing()
@ -187,9 +187,9 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self)) appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self))
} }
} }
// MARK: Keyboard shortcuts // MARK: Keyboard shortcuts
@objc func selectNextUp(_ sender: Any?) { @objc func selectNextUp(_ sender: Any?) {
coordinator.selectPrevArticle() coordinator.selectPrevArticle()
} }
@ -201,17 +201,17 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
@objc func navigateToSidebar(_ sender: Any?) { @objc func navigateToSidebar(_ sender: Any?) {
coordinator.navigateToFeeds() coordinator.navigateToFeeds()
} }
@objc func navigateToDetail(_ sender: Any?) { @objc func navigateToDetail(_ sender: Any?) {
coordinator.navigateToDetail() coordinator.navigateToDetail()
} }
@objc func showFeedInspector(_ sender: Any?) { @objc func showFeedInspector(_ sender: Any?) {
coordinator.showFeedInspector() coordinator.showFeedInspector()
} }
// MARK: API // MARK: API
func restoreSelectionIfNecessary(adjustScroll: Bool) { func restoreSelectionIfNecessary(adjustScroll: Bool) {
if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) { if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) {
if adjustScroll { if adjustScroll {
@ -225,11 +225,11 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
func reinitializeArticles(resetScroll: Bool) { func reinitializeArticles(resetScroll: Bool) {
resetUI(resetScroll: resetScroll) resetUI(resetScroll: resetScroll)
} }
func reloadArticles(animated: Bool) { func reloadArticles(animated: Bool) {
applyChanges(animated: animated) applyChanges(animated: animated)
} }
func updateArticleSelection(animations: Animations) { func updateArticleSelection(animations: Animations) {
if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) { if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) {
if tableView.indexPathForSelectedRow != indexPath { if tableView.indexPathForSelectedRow != indexPath {
@ -238,7 +238,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
} else { } else {
tableView.selectRow(at: nil, animated: animations.contains(.select), scrollPosition: .none) tableView.selectRow(at: nil, animated: animations.contains(.select), scrollPosition: .none)
} }
updateUI() updateUI()
} }
@ -247,7 +247,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
updateTitleUnreadCount() updateTitleUnreadCount()
updateToolbar() updateToolbar()
} }
func hideSearch() { func hideSearch() {
navigationItem.searchController?.isActive = false navigationItem.searchController?.isActive = false
} }
@ -257,7 +257,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
navigationItem.searchController?.searchBar.selectedScopeButtonIndex = 1 navigationItem.searchController?.searchBar.selectedScopeButtonIndex = 1
navigationItem.searchController?.searchBar.becomeFirstResponder() navigationItem.searchController?.searchBar.becomeFirstResponder()
} }
func focus() { func focus() {
becomeFirstResponder() becomeFirstResponder()
} }
@ -265,7 +265,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
func setRefreshToolbarItemVisibility(visible: Bool) { func setRefreshToolbarItemVisibility(visible: Bool) {
refreshProgressView?.alpha = visible ? 1.0 : 0 refreshProgressView?.alpha = visible ? 1.0 : 0
} }
// MARK: - Table view // MARK: - Table view
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
@ -276,41 +276,41 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
let readTitle = article.status.read ? let readTitle = article.status.read ?
NSLocalizedString("Mark as Unread", comment: "Mark as Unread") : NSLocalizedString("Mark as Unread", comment: "Mark as Unread") :
NSLocalizedString("Mark as Read", comment: "Mark as Read") NSLocalizedString("Mark as Read", comment: "Mark as Read")
let readAction = UIContextualAction(style: .normal, title: readTitle) { [weak self] (action, view, completion) in let readAction = UIContextualAction(style: .normal, title: readTitle) { [weak self] (_, _, completion) in
self?.coordinator.toggleRead(article) self?.coordinator.toggleRead(article)
completion(true) completion(true)
} }
readAction.image = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage readAction.image = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage
readAction.backgroundColor = AppAssets.primaryAccentColor readAction.backgroundColor = AppAssets.primaryAccentColor
return UISwipeActionsConfiguration(actions: [readAction]) return UISwipeActionsConfiguration(actions: [readAction])
} }
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? { override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil } guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil }
// Set up the star action // Set up the star action
let starTitle = article.status.starred ? let starTitle = article.status.starred ?
NSLocalizedString("Unstar", comment: "Unstar") : NSLocalizedString("Unstar", comment: "Unstar") :
NSLocalizedString("Star", comment: "Star") NSLocalizedString("Star", comment: "Star")
let starAction = UIContextualAction(style: .normal, title: starTitle) { [weak self] (action, view, completion) in let starAction = UIContextualAction(style: .normal, title: starTitle) { [weak self] (_, _, completion) in
self?.coordinator.toggleStar(article) self?.coordinator.toggleStar(article)
completion(true) completion(true)
} }
starAction.image = article.status.starred ? AppAssets.starOpenImage : AppAssets.starClosedImage starAction.image = article.status.starred ? AppAssets.starOpenImage : AppAssets.starClosedImage
starAction.backgroundColor = AppAssets.starColor starAction.backgroundColor = AppAssets.starColor
// Set up the read action // Set up the read action
let moreTitle = NSLocalizedString("More", comment: "More") let moreTitle = NSLocalizedString("More", comment: "More")
let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in
if let self = self { if let self = self {
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
if let popoverController = alert.popoverPresentationController { if let popoverController = alert.popoverPresentationController {
popoverController.sourceView = view popoverController.sourceView = view
@ -324,11 +324,11 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
if let action = self.markBelowAsReadAlertAction(article, indexPath: indexPath, completion: completion) { if let action = self.markBelowAsReadAlertAction(article, indexPath: indexPath, completion: completion) {
alert.addAction(action) alert.addAction(action)
} }
if let action = self.discloseFeedAlertAction(article, completion: completion) { if let action = self.discloseFeedAlertAction(article, completion: completion) {
alert.addAction(action) alert.addAction(action)
} }
if let action = self.markAllInFeedAsReadAlertAction(article, indexPath: indexPath, completion: completion) { if let action = self.markAllInFeedAsReadAlertAction(article, indexPath: indexPath, completion: completion) {
alert.addAction(action) alert.addAction(action)
} }
@ -347,28 +347,28 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
}) })
self.present(alert, animated: true) self.present(alert, animated: true)
} }
} }
moreAction.image = AppAssets.moreImage moreAction.image = AppAssets.moreImage
moreAction.backgroundColor = UIColor.systemGray moreAction.backgroundColor = UIColor.systemGray
return UISwipeActionsConfiguration(actions: [starAction, moreAction]) return UISwipeActionsConfiguration(actions: [starAction, moreAction])
} }
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil } guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil }
return UIContextMenuConfiguration(identifier: indexPath.row as NSCopying, previewProvider: nil, actionProvider: { [weak self] suggestedActions in return UIContextMenuConfiguration(identifier: indexPath.row as NSCopying, previewProvider: nil, actionProvider: { [weak self] _ in
guard let self = self else { return nil } guard let self = self else { return nil }
var menuElements = [UIMenuElement]() var menuElements = [UIMenuElement]()
var markActions = [UIAction]() var markActions = [UIAction]()
if let action = self.toggleArticleReadStatusAction(article) { if let action = self.toggleArticleReadStatusAction(article) {
markActions.append(action) markActions.append(action)
@ -381,7 +381,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
markActions.append(action) markActions.append(action)
} }
menuElements.append(UIMenu(title: "", options: .displayInline, children: markActions)) menuElements.append(UIMenu(title: "", options: .displayInline, children: markActions))
var secondaryActions = [UIAction]() var secondaryActions = [UIAction]()
if let action = self.discloseFeedAction(article) { if let action = self.discloseFeedAction(article) {
secondaryActions.append(action) secondaryActions.append(action)
@ -392,7 +392,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
if !secondaryActions.isEmpty { if !secondaryActions.isEmpty {
menuElements.append(UIMenu(title: "", options: .displayInline, children: secondaryActions)) menuElements.append(UIMenu(title: "", options: .displayInline, children: secondaryActions))
} }
var copyActions = [UIAction]() var copyActions = [UIAction]()
if let action = self.copyArticleURLAction(article) { if let action = self.copyArticleURLAction(article) {
copyActions.append(action) copyActions.append(action)
@ -403,19 +403,19 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
if !copyActions.isEmpty { if !copyActions.isEmpty {
menuElements.append(UIMenu(title: "", options: .displayInline, children: copyActions)) menuElements.append(UIMenu(title: "", options: .displayInline, children: copyActions))
} }
if let action = self.openInBrowserAction(article) { if let action = self.openInBrowserAction(article) {
menuElements.append(UIMenu(title: "", options: .displayInline, children: [action])) menuElements.append(UIMenu(title: "", options: .displayInline, children: [action]))
} }
if let action = self.shareAction(article, indexPath: indexPath) { if let action = self.shareAction(article, indexPath: indexPath) {
menuElements.append(UIMenu(title: "", options: .displayInline, children: [action])) menuElements.append(UIMenu(title: "", options: .displayInline, children: [action]))
} }
return UIMenu(title: "", children: menuElements) return UIMenu(title: "", children: menuElements)
}) })
} }
override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? { override func tableView(_ tableView: UITableView, previewForHighlightingContextMenuWithConfiguration configuration: UIContextMenuConfiguration) -> UITargetedPreview? {
@ -423,7 +423,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
let cell = tableView.cellForRow(at: IndexPath(row: row, section: 0)) else { let cell = tableView.cellForRow(at: IndexPath(row: row, section: 0)) else {
return nil return nil
} }
return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell)) return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell))
} }
@ -432,17 +432,17 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
let article = dataSource.itemIdentifier(for: indexPath) let article = dataSource.itemIdentifier(for: indexPath)
coordinator.selectArticle(article, animations: [.scroll, .select, .navigation]) coordinator.selectArticle(article, animations: [.scroll, .select, .navigation])
} }
override func scrollViewDidScroll(_ scrollView: UIScrollView) { override func scrollViewDidScroll(_ scrollView: UIScrollView) {
scrollPositionQueue.add(self, #selector(scrollPositionDidChange)) scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
} }
// MARK: Notifications // MARK: Notifications
@objc dynamic func unreadCountDidChange(_ notification: Notification) { @objc dynamic func unreadCountDidChange(_ notification: Notification) {
updateUI() updateUI()
} }
@objc func statusesDidChange(_ note: Notification) { @objc func statusesDidChange(_ note: Notification) {
guard let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String>, !articleIDs.isEmpty else { guard let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String>, !articleIDs.isEmpty else {
return return
@ -461,11 +461,11 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
} }
@objc func feedIconDidBecomeAvailable(_ note: Notification) { @objc func feedIconDidBecomeAvailable(_ note: Notification) {
if let titleView = navigationItem.titleView as? MainTimelineTitleView { if let titleView = navigationItem.titleView as? MainTimelineTitleView {
titleView.iconView.iconImage = coordinator.timelineIconImage titleView.iconView.iconImage = coordinator.timelineIconImage
} }
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else { guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else {
return return
} }
@ -519,27 +519,27 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
self.updateToolbar() self.updateToolbar()
} }
} }
@objc func contentSizeCategoryDidChange(_ note: Notification) { @objc func contentSizeCategoryDidChange(_ note: Notification) {
reloadAllVisibleCells() reloadAllVisibleCells()
} }
@objc func displayNameDidChange(_ note: Notification) { @objc func displayNameDidChange(_ note: Notification) {
if let titleView = navigationItem.titleView as? MainTimelineTitleView { if let titleView = navigationItem.titleView as? MainTimelineTitleView {
titleView.label.text = coordinator.timelineFeed?.nameForDisplay titleView.label.text = coordinator.timelineFeed?.nameForDisplay
} }
} }
@objc func willEnterForeground(_ note: Notification) { @objc func willEnterForeground(_ note: Notification) {
updateUI() updateUI()
} }
@objc func scrollPositionDidChange() { @objc func scrollPositionDidChange() {
coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow() coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow()
} }
// MARK: Reloading // MARK: Reloading
func queueReloadAvailableCells() { func queueReloadAvailableCells() {
CoalescingQueue.standard.add(self, #selector(reloadAllVisibleCells)) CoalescingQueue.standard.add(self, #selector(reloadAllVisibleCells))
} }
@ -566,7 +566,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: Constants.prototypeText, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status) let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: Constants.prototypeText, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status)
let prototypeCellData = MainTimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Prototype Feed Name", byline: nil, iconImage: nil, showIcon: false, numberOfLines: numberOfTextLines, iconSize: iconSize) let prototypeCellData = MainTimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Prototype Feed Name", byline: nil, iconImage: nil, showIcon: false, numberOfLines: numberOfTextLines, iconSize: iconSize)
if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory { if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory {
let layout = MainTimelineAccessibilityCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData) let layout = MainTimelineAccessibilityCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData)
tableView.estimatedRowHeight = layout.height tableView.estimatedRowHeight = layout.height
@ -574,9 +574,9 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
let layout = MainTimelineDefaultCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData) let layout = MainTimelineDefaultCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData)
tableView.estimatedRowHeight = layout.height tableView.estimatedRowHeight = layout.height
} }
} }
} }
// MARK: Searching // MARK: Searching
@ -619,7 +619,7 @@ private extension TimelineViewController {
guard !(splitViewController?.isCollapsed ?? true) else { guard !(splitViewController?.isCollapsed ?? true) else {
return return
} }
guard let refreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as? RefreshProgressView else { guard let refreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as? RefreshProgressView else {
return return
} }
@ -630,7 +630,7 @@ private extension TimelineViewController {
} }
func resetUI(resetScroll: Bool) { func resetUI(resetScroll: Bool) {
title = coordinator.timelineFeed?.nameForDisplay ?? "Timeline" title = coordinator.timelineFeed?.nameForDisplay ?? "Timeline"
if let titleView = navigationItem.titleView as? MainTimelineTitleView { if let titleView = navigationItem.titleView as? MainTimelineTitleView {
@ -641,7 +641,7 @@ private extension TimelineViewController {
} else { } else {
titleView.iconView.tintColor = nil titleView.iconView.tintColor = nil
} }
titleView.label.text = coordinator.timelineFeed?.nameForDisplay titleView.label.text = coordinator.timelineFeed?.nameForDisplay
updateTitleUnreadCount() updateTitleUnreadCount()
@ -652,7 +652,7 @@ private extension TimelineViewController {
titleView.debuttonize() titleView.debuttonize()
titleView.removeGestureRecognizer(feedTapGestureRecognizer) titleView.removeGestureRecognizer(feedTapGestureRecognizer)
} }
navigationItem.titleView = titleView navigationItem.titleView = titleView
} }
@ -662,7 +662,7 @@ private extension TimelineViewController {
case .alwaysRead: case .alwaysRead:
navigationItem.rightBarButtonItem = nil navigationItem.rightBarButtonItem = nil
} }
if coordinator.isReadArticlesFiltered { if coordinator.isReadArticlesFiltered {
filterButton?.image = AppAssets.filterActiveImage filterButton?.image = AppAssets.filterActiveImage
filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Articles", comment: "Selected - Filter Read Articles") filterButton?.accLabelText = NSLocalizedString("Selected - Filter Read Articles", comment: "Selected - Filter Read Articles")
@ -679,16 +679,16 @@ private extension TimelineViewController {
tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false) tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: false)
} }
} }
updateToolbar() updateToolbar()
} }
func updateToolbar() { func updateToolbar() {
guard firstUnreadButton != nil else { return } guard firstUnreadButton != nil else { return }
markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable
firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable
if coordinator.isRootSplitCollapsed { if coordinator.isRootSplitCollapsed {
if let toolbarItems = toolbarItems, toolbarItems.last != firstUnreadButton { if let toolbarItems = toolbarItems, toolbarItems.last != firstUnreadButton {
var items = toolbarItems var items = toolbarItems
@ -702,20 +702,20 @@ private extension TimelineViewController {
} }
} }
} }
func updateTitleUnreadCount() { func updateTitleUnreadCount() {
if let titleView = navigationItem.titleView as? MainTimelineTitleView { if let titleView = navigationItem.titleView as? MainTimelineTitleView {
titleView.unreadCountView.unreadCount = coordinator.timelineUnreadCount titleView.unreadCountView.unreadCount = coordinator.timelineUnreadCount
} }
} }
func applyChanges(animated: Bool, completion: (() -> Void)? = nil) { func applyChanges(animated: Bool, completion: (() -> Void)? = nil) {
if coordinator.articles.count == 0 { if coordinator.articles.count == 0 {
tableView.rowHeight = tableView.estimatedRowHeight tableView.rowHeight = tableView.estimatedRowHeight
} else { } else {
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
} }
var snapshot = NSDiffableDataSourceSnapshot<Int, Article>() var snapshot = NSDiffableDataSourceSnapshot<Int, Article>()
snapshot.appendSections([0]) snapshot.appendSections([0])
snapshot.appendItems(coordinator.articles, toSection: 0) snapshot.appendItems(coordinator.articles, toSection: 0)
@ -725,7 +725,7 @@ private extension TimelineViewController {
completion?() completion?()
} }
} }
func makeDataSource() -> UITableViewDiffableDataSource<Int, Article> { func makeDataSource() -> UITableViewDiffableDataSource<Int, Article> {
let dataSource: UITableViewDiffableDataSource<Int, Article> = let dataSource: UITableViewDiffableDataSource<Int, Article> =
MainTimelineDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, article in MainTimelineDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, article in
@ -736,7 +736,7 @@ private extension TimelineViewController {
dataSource.defaultRowAnimation = .middle dataSource.defaultRowAnimation = .middle
return dataSource return dataSource
} }
func configure(_ cell: MainTimelineTableViewCell, article: Article) { func configure(_ cell: MainTimelineTableViewCell, article: Article) {
let iconImage = iconImageFor(article) let iconImage = iconImageFor(article)
@ -746,29 +746,29 @@ private extension TimelineViewController {
cell.cellData = MainTimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, byline: article.byline(), iconImage: iconImage, showIcon: showIcon, numberOfLines: numberOfTextLines, iconSize: iconSize) cell.cellData = MainTimelineCellData(article: article, showFeedName: showFeedNames, feedName: article.feed?.nameForDisplay, byline: article.byline(), iconImage: iconImage, showIcon: showIcon, numberOfLines: numberOfTextLines, iconSize: iconSize)
} }
func iconImageFor(_ article: Article) -> IconImage? { func iconImageFor(_ article: Article) -> IconImage? {
if !coordinator.showIcons { if !coordinator.showIcons {
return nil return nil
} }
return article.iconImage() return article.iconImage()
} }
func toggleArticleReadStatusAction(_ article: Article) -> UIAction? { func toggleArticleReadStatusAction(_ article: Article) -> UIAction? {
guard !article.status.read || article.isAvailableToMarkUnread else { return nil } guard !article.status.read || article.isAvailableToMarkUnread else { return nil }
let title = article.status.read ? let title = article.status.read ?
NSLocalizedString("Mark as Unread", comment: "Mark as Unread") : NSLocalizedString("Mark as Unread", comment: "Mark as Unread") :
NSLocalizedString("Mark as Read", comment: "Mark as Read") NSLocalizedString("Mark as Read", comment: "Mark as Read")
let image = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage let image = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage
let action = UIAction(title: title, image: image) { [weak self] action in let action = UIAction(title: title, image: image) { [weak self] _ in
self?.coordinator.toggleRead(article) self?.coordinator.toggleRead(article)
} }
return action return action
} }
func toggleArticleStarStatusAction(_ article: Article) -> UIAction { func toggleArticleStarStatusAction(_ article: Article) -> UIAction {
let title = article.status.starred ? let title = article.status.starred ?
@ -776,10 +776,10 @@ private extension TimelineViewController {
NSLocalizedString("Mark as Starred", comment: "Mark as Starred") NSLocalizedString("Mark as Starred", comment: "Mark as Starred")
let image = article.status.starred ? AppAssets.starOpenImage : AppAssets.starClosedImage let image = article.status.starred ? AppAssets.starOpenImage : AppAssets.starClosedImage
let action = UIAction(title: title, image: image) { [weak self] action in let action = UIAction(title: title, image: image) { [weak self] _ in
self?.coordinator.toggleStar(article) self?.coordinator.toggleStar(article)
} }
return action return action
} }
@ -790,14 +790,14 @@ private extension TimelineViewController {
let title = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read") let title = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read")
let image = AppAssets.markAboveAsReadImage let image = AppAssets.markAboveAsReadImage
let action = UIAction(title: title, image: image) { [weak self] action in let action = UIAction(title: title, image: image) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
self?.coordinator.markAboveAsRead(article) self?.coordinator.markAboveAsRead(article)
} }
} }
return action return action
} }
func markBelowAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? { func markBelowAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
guard coordinator.canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { guard coordinator.canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil return nil
@ -805,14 +805,14 @@ private extension TimelineViewController {
let title = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read") let title = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read")
let image = AppAssets.markBelowAsReadImage let image = AppAssets.markBelowAsReadImage
let action = UIAction(title: title, image: image) { [weak self] action in let action = UIAction(title: title, image: image) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
self?.coordinator.markBelowAsRead(article) self?.coordinator.markBelowAsRead(article)
} }
} }
return action return action
} }
func markAboveAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { func markAboveAsReadAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard coordinator.canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { guard coordinator.canMarkAboveAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil return nil
@ -823,7 +823,7 @@ private extension TimelineViewController {
completion(true) completion(true)
} }
let action = UIAlertAction(title: title, style: .default) { [weak self] action in let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
self?.coordinator.markAboveAsRead(article) self?.coordinator.markAboveAsRead(article)
completion(true) completion(true)
@ -841,8 +841,8 @@ private extension TimelineViewController {
let cancel = { let cancel = {
completion(true) completion(true)
} }
let action = UIAlertAction(title: title, style: .default) { [weak self] action in let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
self?.coordinator.markBelowAsRead(article) self?.coordinator.markBelowAsRead(article)
completion(true) completion(true)
@ -854,26 +854,26 @@ private extension TimelineViewController {
func discloseFeedAction(_ article: Article) -> UIAction? { func discloseFeedAction(_ article: Article) -> UIAction? {
guard let feed = article.feed, guard let feed = article.feed,
!coordinator.timelineFeedIsEqualTo(feed) else { return nil } !coordinator.timelineFeedIsEqualTo(feed) else { return nil }
let title = NSLocalizedString("Go to Feed", comment: "Go to Feed") let title = NSLocalizedString("Go to Feed", comment: "Go to Feed")
let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.openInSidebarImage) { [weak self] _ in
self?.coordinator.discloseFeed(feed, animations: [.scroll, .navigation]) self?.coordinator.discloseFeed(feed, animations: [.scroll, .navigation])
} }
return action return action
} }
func discloseFeedAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? { func discloseFeedAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let feed = article.feed, guard let feed = article.feed,
!coordinator.timelineFeedIsEqualTo(feed) else { return nil } !coordinator.timelineFeedIsEqualTo(feed) else { return nil }
let title = NSLocalizedString("Go to Feed", comment: "Go to Feed") let title = NSLocalizedString("Go to Feed", comment: "Go to Feed")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
self?.coordinator.discloseFeed(feed, animations: [.scroll, .navigation]) self?.coordinator.discloseFeed(feed, animations: [.scroll, .navigation])
completion(true) completion(true)
} }
return action return action
} }
func markAllInFeedAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? { func markAllInFeedAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
guard let feed = article.feed else { return nil } guard let feed = article.feed else { return nil }
guard let fetchedArticles = try? feed.fetchArticles() else { guard let fetchedArticles = try? feed.fetchArticles() else {
@ -884,12 +884,11 @@ private extension TimelineViewController {
guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil return nil
} }
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command") let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.markAllAsReadImage) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
self?.coordinator.markAllAsRead(articles) self?.coordinator.markAllAsRead(articles)
} }
@ -902,19 +901,19 @@ private extension TimelineViewController {
guard let fetchedArticles = try? feed.fetchArticles() else { guard let fetchedArticles = try? feed.fetchArticles() else {
return nil return nil
} }
let articles = Array(fetchedArticles) let articles = Array(fetchedArticles)
guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else { guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
return nil return nil
} }
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Mark All as Read in Feed") let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Mark All as Read in Feed")
let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String let title = NSString.localizedStringWithFormat(localizedMenuText as NSString, feed.nameForDisplay) as String
let cancel = { let cancel = {
completion(true) completion(true)
} }
let action = UIAlertAction(title: title, style: .default) { [weak self] action in let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in MarkAsReadAlertController.confirm(self, coordinator: self?.coordinator, confirmTitle: title, sourceType: contentView, cancelCompletion: cancel) { [weak self] in
self?.coordinator.markAllAsRead(articles) self?.coordinator.markAllAsRead(articles)
completion(true) completion(true)
@ -922,30 +921,29 @@ private extension TimelineViewController {
} }
return action return action
} }
func copyArticleURLAction(_ article: Article) -> UIAction? { func copyArticleURLAction(_ article: Article) -> UIAction? {
guard let url = article.preferredURL else { return nil } guard let url = article.preferredURL else { return nil }
let title = NSLocalizedString("Copy Article URL", comment: "Copy Article URL") let title = NSLocalizedString("Copy Article URL", comment: "Copy Article URL")
let action = UIAction(title: title, image: AppAssets.copyImage) { action in let action = UIAction(title: title, image: AppAssets.copyImage) { _ in
UIPasteboard.general.url = url
}
return action
}
func copyExternalURLAction(_ article: Article) -> UIAction? {
guard let externalLink = article.externalLink, externalLink != article.preferredLink, let url = URL(string: externalLink) else { return nil }
let title = NSLocalizedString("Copy External URL", comment: "Copy External URL")
let action = UIAction(title: title, image: AppAssets.copyImage) { action in
UIPasteboard.general.url = url UIPasteboard.general.url = url
} }
return action return action
} }
func copyExternalURLAction(_ article: Article) -> UIAction? {
guard let externalLink = article.externalLink, externalLink != article.preferredLink, let url = URL(string: externalLink) else { return nil }
let title = NSLocalizedString("Copy External URL", comment: "Copy External URL")
let action = UIAction(title: title, image: AppAssets.copyImage) { _ in
UIPasteboard.general.url = url
}
return action
}
func openInBrowserAction(_ article: Article) -> UIAction? { func openInBrowserAction(_ article: Article) -> UIAction? {
guard let _ = article.preferredURL else { return nil } guard let _ = article.preferredURL else { return nil }
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser") let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.safariImage) { [weak self] _ in
self?.coordinator.showBrowserForArticle(article) self?.coordinator.showBrowserForArticle(article)
} }
return action return action
@ -955,41 +953,41 @@ private extension TimelineViewController {
guard let _ = article.preferredURL else { return nil } guard let _ = article.preferredURL else { return nil }
let title = NSLocalizedString("Open in Browser", comment: "Open in Browser") let title = NSLocalizedString("Open in Browser", comment: "Open in Browser")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
self?.coordinator.showBrowserForArticle(article) self?.coordinator.showBrowserForArticle(article)
completion(true) completion(true)
} }
return action return action
} }
func shareDialogForTableCell(indexPath: IndexPath, url: URL, title: String?) { func shareDialogForTableCell(indexPath: IndexPath, url: URL, title: String?) {
let activityViewController = UIActivityViewController(url: url, title: title, applicationActivities: nil) let activityViewController = UIActivityViewController(url: url, title: title, applicationActivities: nil)
guard let cell = tableView.cellForRow(at: indexPath) else { return } guard let cell = tableView.cellForRow(at: indexPath) else { return }
let popoverController = activityViewController.popoverPresentationController let popoverController = activityViewController.popoverPresentationController
popoverController?.sourceView = cell popoverController?.sourceView = cell
popoverController?.sourceRect = CGRect(x: 0, y: 0, width: cell.frame.size.width, height: cell.frame.size.height) popoverController?.sourceRect = CGRect(x: 0, y: 0, width: cell.frame.size.width, height: cell.frame.size.height)
present(activityViewController, animated: true) present(activityViewController, animated: true)
} }
func shareAction(_ article: Article, indexPath: IndexPath) -> UIAction? { func shareAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
guard let url = article.preferredURL else { return nil } guard let url = article.preferredURL else { return nil }
let title = NSLocalizedString("Share", comment: "Share") let title = NSLocalizedString("Share", comment: "Share")
let action = UIAction(title: title, image: AppAssets.shareImage) { [weak self] action in let action = UIAction(title: title, image: AppAssets.shareImage) { [weak self] _ in
self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title) self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title)
} }
return action return action
} }
func shareAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? { func shareAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
guard let url = article.preferredURL else { return nil } guard let url = article.preferredURL else { return nil }
let title = NSLocalizedString("Share", comment: "Share") let title = NSLocalizedString("Share", comment: "Share")
let action = UIAlertAction(title: title, style: .default) { [weak self] action in let action = UIAlertAction(title: title, style: .default) { [weak self] _ in
completion(true) completion(true)
self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title) self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title)
} }
return action return action
} }
} }

View File

@ -10,28 +10,28 @@ import UIKit
import Account import Account
class RootSplitViewController: UISplitViewController { class RootSplitViewController: UISplitViewController {
var coordinator: SceneCoordinator! var coordinator: SceneCoordinator!
override var prefersStatusBarHidden: Bool { override var prefersStatusBarHidden: Bool {
return coordinator.prefersStatusBarHidden return coordinator.prefersStatusBarHidden
} }
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation { override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
return .slide return .slide
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
coordinator.resetFocus() coordinator.resetFocus()
} }
override func show(_ column: UISplitViewController.Column) { override func show(_ column: UISplitViewController.Column) {
guard !coordinator.isNavigationDisabled else { return } guard !coordinator.isNavigationDisabled else { return }
super.show(column) super.show(column)
} }
// MARK: Keyboard Shortcuts // MARK: Keyboard Shortcuts
@objc func scrollOrGoToNextUnread(_ sender: Any?) { @objc func scrollOrGoToNextUnread(_ sender: Any?) {
coordinator.scrollOrGoToNextUnread() coordinator.scrollOrGoToNextUnread()
} }
@ -39,26 +39,26 @@ class RootSplitViewController: UISplitViewController {
@objc func scrollUp(_ sender: Any?) { @objc func scrollUp(_ sender: Any?) {
coordinator.scrollUp() coordinator.scrollUp()
} }
@objc func goToPreviousUnread(_ sender: Any?) { @objc func goToPreviousUnread(_ sender: Any?) {
coordinator.selectPrevUnread() coordinator.selectPrevUnread()
} }
@objc func nextUnread(_ sender: Any?) { @objc func nextUnread(_ sender: Any?) {
coordinator.selectNextUnread() coordinator.selectNextUnread()
} }
@objc func markRead(_ sender: Any?) { @objc func markRead(_ sender: Any?) {
coordinator.markAsReadForCurrentArticle() coordinator.markAsReadForCurrentArticle()
} }
@objc func markUnreadAndGoToNextUnread(_ sender: Any?) { @objc func markUnreadAndGoToNextUnread(_ sender: Any?) {
coordinator.markAsUnreadForCurrentArticle() coordinator.markAsUnreadForCurrentArticle()
coordinator.selectNextUnread() coordinator.selectNextUnread()
} }
@objc func markAllAsReadAndGoToNextUnread(_ sender: Any?) { @objc func markAllAsReadAndGoToNextUnread(_ sender: Any?) {
coordinator.markAllAsReadInTimeline() { coordinator.markAllAsReadInTimeline {
self.coordinator.selectNextUnread() self.coordinator.selectNextUnread()
} }
} }
@ -66,23 +66,23 @@ class RootSplitViewController: UISplitViewController {
@objc func markAboveAsRead(_ sender: Any?) { @objc func markAboveAsRead(_ sender: Any?) {
coordinator.markAboveAsRead() coordinator.markAboveAsRead()
} }
@objc func markBelowAsRead(_ sender: Any?) { @objc func markBelowAsRead(_ sender: Any?) {
coordinator.markBelowAsRead() coordinator.markBelowAsRead()
} }
@objc func markUnread(_ sender: Any?) { @objc func markUnread(_ sender: Any?) {
coordinator.markAsUnreadForCurrentArticle() coordinator.markAsUnreadForCurrentArticle()
} }
@objc func goToPreviousSubscription(_ sender: Any?) { @objc func goToPreviousSubscription(_ sender: Any?) {
coordinator.selectPrevFeed() coordinator.selectPrevFeed()
} }
@objc func goToNextSubscription(_ sender: Any?) { @objc func goToNextSubscription(_ sender: Any?) {
coordinator.selectNextFeed() coordinator.selectNextFeed()
} }
@objc func openInBrowser(_ sender: Any?) { @objc func openInBrowser(_ sender: Any?) {
coordinator.showBrowserForCurrentArticle() coordinator.showBrowserForCurrentArticle()
} }
@ -90,11 +90,11 @@ class RootSplitViewController: UISplitViewController {
@objc func openInAppBrowser(_ sender: Any?) { @objc func openInAppBrowser(_ sender: Any?) {
coordinator.showInAppBrowser() coordinator.showInAppBrowser()
} }
@objc func articleSearch(_ sender: Any?) { @objc func articleSearch(_ sender: Any?) {
coordinator.showSearch() coordinator.showSearch()
} }
@objc func addNewFeed(_ sender: Any?) { @objc func addNewFeed(_ sender: Any?) {
coordinator.showAddFeed() coordinator.showAddFeed()
} }
@ -106,27 +106,27 @@ class RootSplitViewController: UISplitViewController {
@objc func cleanUp(_ sender: Any?) { @objc func cleanUp(_ sender: Any?) {
coordinator.cleanUp(conditional: false) coordinator.cleanUp(conditional: false)
} }
@objc func toggleReadFeedsFilter(_ sender: Any?) { @objc func toggleReadFeedsFilter(_ sender: Any?) {
coordinator.toggleReadFeedsFilter() coordinator.toggleReadFeedsFilter()
} }
@objc func toggleReadArticlesFilter(_ sender: Any?) { @objc func toggleReadArticlesFilter(_ sender: Any?) {
coordinator.toggleReadArticlesFilter() coordinator.toggleReadArticlesFilter()
} }
@objc func refresh(_ sender: Any?) { @objc func refresh(_ sender: Any?) {
appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self)) appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self))
} }
@objc func goToToday(_ sender: Any?) { @objc func goToToday(_ sender: Any?) {
coordinator.selectTodayFeed() coordinator.selectTodayFeed()
} }
@objc func goToAllUnread(_ sender: Any?) { @objc func goToAllUnread(_ sender: Any?) {
coordinator.selectAllUnreadFeed() coordinator.selectAllUnreadFeed()
} }
@objc func goToStarred(_ sender: Any?) { @objc func goToStarred(_ sender: Any?) {
coordinator.selectStarredFeed() coordinator.selectStarredFeed()
} }
@ -138,7 +138,7 @@ class RootSplitViewController: UISplitViewController {
@objc func toggleRead(_ sender: Any?) { @objc func toggleRead(_ sender: Any?) {
coordinator.toggleReadForCurrentArticle() coordinator.toggleReadForCurrentArticle()
} }
@objc func toggleStarred(_ sender: Any?) { @objc func toggleStarred(_ sender: Any?) {
coordinator.toggleStarredForCurrentArticle() coordinator.toggleStarredForCurrentArticle()
} }

File diff suppressed because it is too large Load Diff

View File

@ -11,18 +11,18 @@ import UserNotifications
import Account import Account
class SceneDelegate: UIResponder, UIWindowSceneDelegate { class SceneDelegate: UIResponder, UIWindowSceneDelegate {
var window: UIWindow? var window: UIWindow?
var coordinator: SceneCoordinator! var coordinator: SceneCoordinator!
// UIWindowScene delegate // UIWindowScene delegate
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
window!.tintColor = AppAssets.primaryAccentColor window!.tintColor = AppAssets.primaryAccentColor
updateUserInterfaceStyle() updateUserInterfaceStyle()
UINavigationBar.appearance().scrollEdgeAppearance = UINavigationBarAppearance() UINavigationBar.appearance().scrollEdgeAppearance = UINavigationBarAppearance()
let rootViewController = window!.rootViewController as! RootSplitViewController let rootViewController = window!.rootViewController as! RootSplitViewController
rootViewController.presentsWithGesture = true rootViewController.presentsWithGesture = true
rootViewController.showsSecondaryOnlyButton = true rootViewController.showsSecondaryOnlyButton = true
@ -38,43 +38,43 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
coordinator = SceneCoordinator(rootSplitViewController: rootViewController) coordinator = SceneCoordinator(rootSplitViewController: rootViewController)
rootViewController.coordinator = coordinator rootViewController.coordinator = coordinator
rootViewController.delegate = coordinator rootViewController.delegate = coordinator
coordinator.restoreWindowState(session.stateRestorationActivity) coordinator.restoreWindowState(session.stateRestorationActivity)
NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange), name: UserDefaults.didChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange), name: UserDefaults.didChangeNotification, object: nil)
if let _ = connectionOptions.urlContexts.first?.url { if let _ = connectionOptions.urlContexts.first?.url {
self.scene(scene, openURLContexts: connectionOptions.urlContexts) self.scene(scene, openURLContexts: connectionOptions.urlContexts)
return return
} }
if let shortcutItem = connectionOptions.shortcutItem { if let shortcutItem = connectionOptions.shortcutItem {
handleShortcutItem(shortcutItem) handleShortcutItem(shortcutItem)
return return
} }
if let notificationResponse = connectionOptions.notificationResponse { if let notificationResponse = connectionOptions.notificationResponse {
coordinator.handle(notificationResponse) coordinator.handle(notificationResponse)
return return
} }
if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
coordinator.handle(userActivity) coordinator.handle(userActivity)
} }
} }
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
appDelegate.resumeDatabaseProcessingIfNecessary() appDelegate.resumeDatabaseProcessingIfNecessary()
handleShortcutItem(shortcutItem) handleShortcutItem(shortcutItem)
completionHandler(true) completionHandler(true)
} }
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) { func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
appDelegate.resumeDatabaseProcessingIfNecessary() appDelegate.resumeDatabaseProcessingIfNecessary()
coordinator.handle(userActivity) coordinator.handle(userActivity)
} }
func sceneDidEnterBackground(_ scene: UIScene) { func sceneDidEnterBackground(_ scene: UIScene) {
ArticleStringFormatter.emptyCaches() ArticleStringFormatter.emptyCaches()
appDelegate.prepareAccountsForBackground() appDelegate.prepareAccountsForBackground()
@ -85,39 +85,39 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
appDelegate.prepareAccountsForForeground() appDelegate.prepareAccountsForForeground()
coordinator.resetFocus() coordinator.resetFocus()
} }
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
return coordinator.stateRestorationActivity return coordinator.stateRestorationActivity
} }
// API // API
func handle(_ response: UNNotificationResponse) { func handle(_ response: UNNotificationResponse) {
appDelegate.resumeDatabaseProcessingIfNecessary() appDelegate.resumeDatabaseProcessingIfNecessary()
coordinator.handle(response) coordinator.handle(response)
} }
func suspend() { func suspend() {
coordinator.suspend() coordinator.suspend()
} }
func cleanUp(conditional: Bool) { func cleanUp(conditional: Bool) {
coordinator.cleanUp(conditional: conditional) coordinator.cleanUp(conditional: conditional)
} }
// Handle Opening of URLs // Handle Opening of URLs
func scene(_ scene: UIScene, openURLContexts urlContexts: Set<UIOpenURLContext>) { func scene(_ scene: UIScene, openURLContexts urlContexts: Set<UIOpenURLContext>) {
guard let context = urlContexts.first else { return } guard let context = urlContexts.first else { return }
DispatchQueue.main.async { DispatchQueue.main.async {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
self.coordinator.dismissIfLaunchingFromExternalAction() self.coordinator.dismissIfLaunchingFromExternalAction()
} }
let urlString = context.url.absoluteString let urlString = context.url.absoluteString
// Handle the feed: and feeds: schemes // Handle the feed: and feeds: schemes
if urlString.starts(with: "feed:") || urlString.starts(with: "feeds:") { if urlString.starts(with: "feed:") || urlString.starts(with: "feeds:") {
let normalizedURLString = urlString.normalizedURL let normalizedURLString = urlString.normalizedURL
@ -125,7 +125,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
self.coordinator.showAddFeed(initialFeed: normalizedURLString, initialFeedName: nil) self.coordinator.showAddFeed(initialFeed: normalizedURLString, initialFeedName: nil)
} }
} }
// Show Unread View or Article // Show Unread View or Article
if urlString.contains(WidgetDeepLink.unread.url.absoluteString) { if urlString.contains(WidgetDeepLink.unread.url.absoluteString) {
guard let comps = URLComponents(string: urlString ) else { return } guard let comps = URLComponents(string: urlString ) else { return }
@ -134,14 +134,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
if AccountManager.shared.isSuspended { if AccountManager.shared.isSuspended {
AccountManager.shared.resumeAll() AccountManager.shared.resumeAll()
} }
self.coordinator.selectAllUnreadFeed() { self.coordinator.selectAllUnreadFeed {
self.coordinator.selectArticleInCurrentFeed(id!) self.coordinator.selectArticleInCurrentFeed(id!)
} }
} else { } else {
self.coordinator.selectAllUnreadFeed() self.coordinator.selectAllUnreadFeed()
} }
} }
// Show Today View or Article // Show Today View or Article
if urlString.contains(WidgetDeepLink.today.url.absoluteString) { if urlString.contains(WidgetDeepLink.today.url.absoluteString) {
guard let comps = URLComponents(string: urlString ) else { return } guard let comps = URLComponents(string: urlString ) else { return }
@ -150,14 +150,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
if AccountManager.shared.isSuspended { if AccountManager.shared.isSuspended {
AccountManager.shared.resumeAll() AccountManager.shared.resumeAll()
} }
self.coordinator.selectTodayFeed() { self.coordinator.selectTodayFeed {
self.coordinator.selectArticleInCurrentFeed(id!) self.coordinator.selectArticleInCurrentFeed(id!)
} }
} else { } else {
self.coordinator.selectTodayFeed() self.coordinator.selectTodayFeed()
} }
} }
// Show Starred View or Article // Show Starred View or Article
if urlString.contains(WidgetDeepLink.starred.url.absoluteString) { if urlString.contains(WidgetDeepLink.starred.url.absoluteString) {
guard let comps = URLComponents(string: urlString ) else { return } guard let comps = URLComponents(string: urlString ) else { return }
@ -166,38 +166,38 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
if AccountManager.shared.isSuspended { if AccountManager.shared.isSuspended {
AccountManager.shared.resumeAll() AccountManager.shared.resumeAll()
} }
self.coordinator.selectStarredFeed() { self.coordinator.selectStarredFeed {
self.coordinator.selectArticleInCurrentFeed(id!) self.coordinator.selectArticleInCurrentFeed(id!)
} }
} else { } else {
self.coordinator.selectStarredFeed() self.coordinator.selectStarredFeed()
} }
} }
let filename = context.url.standardizedFileURL.path let filename = context.url.standardizedFileURL.path
if filename.hasSuffix(ArticleTheme.nnwThemeSuffix) { if filename.hasSuffix(ArticleTheme.nnwThemeSuffix) {
self.coordinator.importTheme(filename: filename) self.coordinator.importTheme(filename: filename)
return return
} }
// Handle theme URLs: netnewswire://theme/add?url={url} // Handle theme URLs: netnewswire://theme/add?url={url}
guard let comps = URLComponents(url: context.url, resolvingAgainstBaseURL: false), guard let comps = URLComponents(url: context.url, resolvingAgainstBaseURL: false),
"theme" == comps.host, "theme" == comps.host,
let queryItems = comps.queryItems else { let queryItems = comps.queryItems else {
return return
} }
if let providedThemeURL = queryItems.first(where: { $0.name == "url" })?.value { if let providedThemeURL = queryItems.first(where: { $0.name == "url" })?.value {
if let themeURL = URL(string: providedThemeURL) { if let themeURL = URL(string: providedThemeURL) {
let request = URLRequest(url: themeURL) let request = URLRequest(url: themeURL)
DispatchQueue.main.async { DispatchQueue.main.async {
NotificationCenter.default.post(name: .didBeginDownloadingTheme, object: nil) NotificationCenter.default.post(name: .didBeginDownloadingTheme, object: nil)
} }
let task = URLSession.shared.downloadTask(with: request) { location, response, error in let task = URLSession.shared.downloadTask(with: request) { location, _, error in
guard guard
let location = location else { return } let location = location else { return }
do { do {
try ArticleThemeDownloader.shared.handleFile(at: location) try ArticleThemeDownloader.shared.handleFile(at: location)
} catch { } catch {
@ -212,14 +212,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
} else { } else {
return return
} }
} }
} }
} }
private extension SceneDelegate { private extension SceneDelegate {
func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) { func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) {
switch shortcutItem.type { switch shortcutItem.type {
case "com.ranchero.NetNewsWire.FirstUnread": case "com.ranchero.NetNewsWire.FirstUnread":
@ -232,11 +231,11 @@ private extension SceneDelegate {
break break
} }
} }
@objc func userDefaultsDidChange() { @objc func userDefaultsDidChange() {
updateUserInterfaceStyle() updateUserInterfaceStyle()
} }
func updateUserInterfaceStyle() { func updateUserInterfaceStyle() {
DispatchQueue.main.async { DispatchQueue.main.async {
switch AppDefaults.userInterfaceColorPalette { switch AppDefaults.userInterfaceColorPalette {
@ -249,7 +248,5 @@ private extension SceneDelegate {
} }
} }
} }
} }

View File

@ -15,11 +15,11 @@ class AboutViewController: UITableViewController {
@IBOutlet weak var acknowledgmentsTextView: UITextView! @IBOutlet weak var acknowledgmentsTextView: UITextView!
@IBOutlet weak var thanksTextView: UITextView! @IBOutlet weak var thanksTextView: UITextView!
@IBOutlet weak var dedicationTextView: UITextView! @IBOutlet weak var dedicationTextView: UITextView!
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
configureCell(file: "About", textView: aboutTextView) configureCell(file: "About", textView: aboutTextView)
configureCell(file: "Credits", textView: creditsTextView) configureCell(file: "Credits", textView: creditsTextView)
configureCell(file: "Thanks", textView: thanksTextView) configureCell(file: "Thanks", textView: thanksTextView)
@ -32,7 +32,7 @@ class AboutViewController: UITableViewController {
buildLabel.numberOfLines = 0 buildLabel.numberOfLines = 0
buildLabel.sizeToFit() buildLabel.sizeToFit()
buildLabel.translatesAutoresizingMaskIntoConstraints = false buildLabel.translatesAutoresizingMaskIntoConstraints = false
let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0)) let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0))
wrapperView.translatesAutoresizingMaskIntoConstraints = false wrapperView.translatesAutoresizingMaskIntoConstraints = false
wrapperView.addSubview(buildLabel) wrapperView.addSubview(buildLabel)
@ -42,11 +42,11 @@ class AboutViewController: UITableViewController {
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension return UITableView.automaticDimension
} }
} }
private extension AboutViewController { private extension AboutViewController {
func configureCell(file: String, textView: UITextView) { func configureCell(file: String, textView: UITextView) {
let url = Bundle.main.url(forResource: file, withExtension: "rtf")! let url = Bundle.main.url(forResource: file, withExtension: "rtf")!
let string = try! NSAttributedString(url: url, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil) let string = try! NSAttributedString(url: url, options: [NSAttributedString.DocumentReadingOptionKey.documentType: NSAttributedString.DocumentType.rtf], documentAttributes: nil)
@ -55,5 +55,5 @@ private extension AboutViewController {
textView.adjustsFontForContentSizeCategory = true textView.adjustsFontForContentSizeCategory = true
textView.font = .preferredFont(forTextStyle: .body) textView.font = .preferredFont(forTextStyle: .body)
} }
} }

View File

@ -21,7 +21,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
case icloud case icloud
case web case web
case selfhosted case selfhosted
var sectionHeader: String { var sectionHeader: String {
switch self { switch self {
case .local: case .local:
@ -34,7 +34,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
return NSLocalizedString("Self-hosted", comment: "Self hosted Account") return NSLocalizedString("Self-hosted", comment: "Self hosted Account")
} }
} }
var sectionFooter: String { var sectionFooter: String {
switch self { switch self {
case .local: case .local:
@ -47,7 +47,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
return NSLocalizedString("Self-hosted accounts sync your feeds across all your devices", comment: "Self hosted Account") return NSLocalizedString("Self-hosted accounts sync your feeds across all your devices", comment: "Self hosted Account")
} }
} }
var sectionContent: [AccountType] { var sectionContent: [AccountType] {
switch self { switch self {
case .local: case .local:
@ -65,35 +65,31 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
} }
} }
} }
override func viewDidLoad() {
super.viewDidLoad()
}
override func numberOfSections(in tableView: UITableView) -> Int { override func numberOfSections(in tableView: UITableView) -> Int {
return AddAccountSections.allCases.count return AddAccountSections.allCases.count
} }
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == AddAccountSections.local.rawValue { if section == AddAccountSections.local.rawValue {
return AddAccountSections.local.sectionContent.count return AddAccountSections.local.sectionContent.count
} }
if section == AddAccountSections.icloud.rawValue { if section == AddAccountSections.icloud.rawValue {
return AddAccountSections.icloud.sectionContent.count return AddAccountSections.icloud.sectionContent.count
} }
if section == AddAccountSections.web.rawValue { if section == AddAccountSections.web.rawValue {
return AddAccountSections.web.sectionContent.count return AddAccountSections.web.sectionContent.count
} }
if section == AddAccountSections.selfhosted.rawValue { if section == AddAccountSections.selfhosted.rawValue {
return AddAccountSections.selfhosted.sectionContent.count return AddAccountSections.selfhosted.sectionContent.count
} }
return 0 return 0
} }
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
switch section { switch section {
case AddAccountSections.local.rawValue: case AddAccountSections.local.rawValue:
@ -108,7 +104,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
return nil return nil
} }
} }
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? { override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
switch section { switch section {
case AddAccountSections.local.rawValue: case AddAccountSections.local.rawValue:
@ -123,10 +119,10 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
return nil return nil
} }
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsAccountTableViewCell", for: indexPath) as! SettingsComboTableViewCell let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsAccountTableViewCell", for: indexPath) as! SettingsComboTableViewCell
switch indexPath.section { switch indexPath.section {
case AddAccountSections.local.rawValue: case AddAccountSections.local.rawValue:
cell.comboNameLabel?.text = AddAccountSections.local.sectionContent[indexPath.row].localizedAccountName() cell.comboNameLabel?.text = AddAccountSections.local.sectionContent[indexPath.row].localizedAccountName()
@ -149,15 +145,15 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
case AddAccountSections.selfhosted.rawValue: case AddAccountSections.selfhosted.rawValue:
cell.comboNameLabel?.text = AddAccountSections.selfhosted.sectionContent[indexPath.row].localizedAccountName() cell.comboNameLabel?.text = AddAccountSections.selfhosted.sectionContent[indexPath.row].localizedAccountName()
cell.comboImage?.image = AppAssets.image(for: AddAccountSections.selfhosted.sectionContent[indexPath.row]) cell.comboImage?.image = AppAssets.image(for: AddAccountSections.selfhosted.sectionContent[indexPath.row])
default: default:
return cell return cell
} }
return cell return cell
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch indexPath.section { switch indexPath.section {
case AddAccountSections.local.rawValue: case AddAccountSections.local.rawValue:
let type = AddAccountSections.local.sectionContent[indexPath.row] let type = AddAccountSections.local.sectionContent[indexPath.row]
@ -175,7 +171,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
return return
} }
} }
private func presentController(for accountType: AccountType) { private func presentController(for accountType: AccountType) {
switch accountType { switch accountType {
case .onMyMac: case .onMyMac:
@ -216,18 +212,18 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
present(navController, animated: true) present(navController, animated: true)
} }
} }
func dismiss() { func dismiss() {
navigationController?.popViewController(animated: false) navigationController?.popViewController(animated: false)
} }
} }
extension AddAccountViewController: OAuthAccountAuthorizationOperationDelegate { extension AddAccountViewController: OAuthAccountAuthorizationOperationDelegate {
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) { func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) {
let rootViewController = view.window?.rootViewController let rootViewController = view.window?.rootViewController
account.refreshAll { result in account.refreshAll { result in
switch result { switch result {
case .success: case .success:
@ -239,10 +235,10 @@ extension AddAccountViewController: OAuthAccountAuthorizationOperationDelegate {
viewController.presentError(error) viewController.presentError(error)
} }
} }
dismiss() dismiss()
} }
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) { func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) {
presentError(error) presentError(error)
} }

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
struct ArticleThemeImporter { struct ArticleThemeImporter {
static func importTheme(controller: UIViewController, url: URL) throws { static func importTheme(controller: UIViewController, url: URL) throws {
let theme = try ArticleTheme(url: url, isAppTheme: false) let theme = try ArticleTheme(url: url, isAppTheme: false)
@ -20,13 +20,13 @@ struct ArticleThemeImporter {
let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.creatorHomePage) as String let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.creatorHomePage) as String
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
if let websiteURL = URL(string: theme.creatorHomePage) { if let websiteURL = URL(string: theme.creatorHomePage) {
let visitSiteTitle = NSLocalizedString("Show Website", comment: "Show Website") let visitSiteTitle = NSLocalizedString("Show Website", comment: "Show Website")
let visitSiteAction = UIAlertAction(title: visitSiteTitle, style: .default) { action in let visitSiteAction = UIAlertAction(title: visitSiteTitle, style: .default) { _ in
UIApplication.shared.open(websiteURL) UIApplication.shared.open(websiteURL)
try? Self.importTheme(controller: controller, url: url) try? Self.importTheme(controller: controller, url: url)
} }
@ -49,7 +49,7 @@ struct ArticleThemeImporter {
} }
let installThemeTitle = NSLocalizedString("Install Theme", comment: "Install Theme") let installThemeTitle = NSLocalizedString("Install Theme", comment: "Install Theme")
let installThemeAction = UIAlertAction(title: installThemeTitle, style: .default) { action in let installThemeAction = UIAlertAction(title: installThemeTitle, style: .default) { _ in
if ArticleThemesManager.shared.themeExists(filename: url.path) { if ArticleThemesManager.shared.themeExists(filename: url.path) {
let title = NSLocalizedString("Duplicate Theme", comment: "Duplicate Theme") let title = NSLocalizedString("Duplicate Theme", comment: "Duplicate Theme")
@ -61,7 +61,7 @@ struct ArticleThemeImporter {
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel)) alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
let overwriteAction = UIAlertAction(title: NSLocalizedString("Overwrite", comment: "Overwrite"), style: .default) { action in let overwriteAction = UIAlertAction(title: NSLocalizedString("Overwrite", comment: "Overwrite"), style: .default) { _ in
importTheme() importTheme()
} }
alertController.addAction(overwriteAction) alertController.addAction(overwriteAction)
@ -71,32 +71,32 @@ struct ArticleThemeImporter {
} else { } else {
importTheme() importTheme()
} }
} }
alertController.addAction(installThemeAction) alertController.addAction(installThemeAction)
alertController.preferredAction = installThemeAction alertController.preferredAction = installThemeAction
controller.present(alertController, animated: true) controller.present(alertController, animated: true)
} }
} }
private extension ArticleThemeImporter { private extension ArticleThemeImporter {
static func confirmImportSuccess(controller: UIViewController, themeName: String) { static func confirmImportSuccess(controller: UIViewController, themeName: String) {
let title = NSLocalizedString("Theme installed", comment: "Theme installed") let title = NSLocalizedString("Theme installed", comment: "Theme installed")
let localizedMessageText = NSLocalizedString("The theme “%@” has been installed.", comment: "Theme installed") let localizedMessageText = NSLocalizedString("The theme “%@” has been installed.", comment: "Theme installed")
let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, themeName) as String let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, themeName) as String
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let doneTitle = NSLocalizedString("Done", comment: "Done") let doneTitle = NSLocalizedString("Done", comment: "Done")
alertController.addAction(UIAlertAction(title: doneTitle, style: .default)) alertController.addAction(UIAlertAction(title: doneTitle, style: .default))
controller.present(alertController, animated: true) controller.present(alertController, animated: true)
} }
} }

View File

@ -17,15 +17,15 @@ extension UTType {
class ArticleThemesTableViewController: UITableViewController { class ArticleThemesTableViewController: UITableViewController {
override func viewDidLoad() { override func viewDidLoad() {
let importBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(importTheme(_:))); let importBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(importTheme(_:)))
importBarButtonItem.title = NSLocalizedString("Import Theme", comment: "Import Theme"); importBarButtonItem.title = NSLocalizedString("Import Theme", comment: "Import Theme")
navigationItem.rightBarButtonItem = importBarButtonItem navigationItem.rightBarButtonItem = importBarButtonItem
NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil)
} }
// MARK: Notifications // MARK: Notifications
@objc func articleThemeNamesDidChangeNotification(_ note: Notification) { @objc func articleThemeNamesDidChangeNotification(_ note: Notification) {
tableView.reloadData() tableView.reloadData()
} }
@ -49,21 +49,21 @@ class ArticleThemesTableViewController: UITableViewController {
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath) let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let themeName: String let themeName: String
if indexPath.row == 0 { if indexPath.row == 0 {
themeName = ArticleTheme.defaultTheme.name themeName = ArticleTheme.defaultTheme.name
} else { } else {
themeName = ArticleThemesManager.shared.themeNames[indexPath.row - 1] themeName = ArticleThemesManager.shared.themeNames[indexPath.row - 1]
} }
cell.textLabel?.text = themeName cell.textLabel?.text = themeName
if themeName == ArticleThemesManager.shared.currentTheme.name { if themeName == ArticleThemesManager.shared.currentTheme.name {
cell.accessoryType = .checkmark cell.accessoryType = .checkmark
} else { } else {
cell.accessoryType = .none cell.accessoryType = .none
} }
return cell return cell
} }
@ -80,33 +80,33 @@ class ArticleThemesTableViewController: UITableViewController {
!theme.isAppTheme else { return nil } !theme.isAppTheme else { return nil }
let deleteTitle = NSLocalizedString("Delete", comment: "Delete") let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, completion) in let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (_, _, completion) in
let title = NSLocalizedString("Delete Theme?", comment: "Delete Theme") let title = NSLocalizedString("Delete Theme?", comment: "Delete Theme")
let localizedMessageText = NSLocalizedString("Are you sure you want to delete the theme “%@”?.", comment: "Delete Theme Message") let localizedMessageText = NSLocalizedString("Are you sure you want to delete the theme “%@”?.", comment: "Delete Theme Message")
let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, themeName) as String let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, themeName) as String
let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert) let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel") let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { action in let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in
completion(true) completion(true)
} }
alertController.addAction(cancelAction) alertController.addAction(cancelAction)
let deleteTitle = NSLocalizedString("Delete", comment: "Delete") let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { action in let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { _ in
ArticleThemesManager.shared.deleteTheme(themeName: themeName) ArticleThemesManager.shared.deleteTheme(themeName: themeName)
completion(true) completion(true)
} }
alertController.addAction(deleteAction) alertController.addAction(deleteAction)
self?.present(alertController, animated: true) self?.present(alertController, animated: true)
} }
deleteAction.image = AppAssets.trashImage deleteAction.image = AppAssets.trashImage
deleteAction.backgroundColor = UIColor.systemRed deleteAction.backgroundColor = UIColor.systemRed
return UISwipeActionsConfiguration(actions: [deleteAction]) return UISwipeActionsConfiguration(actions: [deleteAction])
} }
} }
@ -114,12 +114,12 @@ class ArticleThemesTableViewController: UITableViewController {
// MARK: UIDocumentPickerDelegate // MARK: UIDocumentPickerDelegate
extension ArticleThemesTableViewController: UIDocumentPickerDelegate { extension ArticleThemesTableViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
guard let url = urls.first else { return } guard let url = urls.first else { return }
if url.startAccessingSecurityScopedResource() { if url.startAccessingSecurityScopedResource() {
defer { defer {
url.stopAccessingSecurityScopedResource() url.stopAccessingSecurityScopedResource()
} }

View File

@ -16,7 +16,7 @@ class SettingsComboTableViewCell: VibrantTableViewCell {
override func updateVibrancy(animated: Bool) { override func updateVibrancy(animated: Bool) {
super.updateVibrancy(animated: animated) super.updateVibrancy(animated: animated)
updateLabelVibrancy(comboNameLabel, color: labelColor, animated: animated) updateLabelVibrancy(comboNameLabel, color: labelColor, animated: animated)
let tintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label let tintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label
if animated { if animated {
UIView.animate(withDuration: Self.duration) { UIView.animate(withDuration: Self.duration) {
@ -26,5 +26,5 @@ class SettingsComboTableViewCell: VibrantTableViewCell {
self.comboImage?.tintColor = tintColor self.comboImage?.tintColor = tintColor
} }
} }
} }

View File

@ -16,7 +16,7 @@ import UniformTypeIdentifiers
class SettingsViewController: UITableViewController { class SettingsViewController: UITableViewController {
private weak var opmlAccount: Account? private weak var opmlAccount: Account?
@IBOutlet weak var timelineSortOrderSwitch: UISwitch! @IBOutlet weak var timelineSortOrderSwitch: UISwitch!
@IBOutlet weak var groupByFeedSwitch: UISwitch! @IBOutlet weak var groupByFeedSwitch: UISwitch!
@IBOutlet weak var refreshClearsReadArticlesSwitch: UISwitch! @IBOutlet weak var refreshClearsReadArticlesSwitch: UISwitch!
@ -25,10 +25,10 @@ class SettingsViewController: UITableViewController {
@IBOutlet weak var showFullscreenArticlesSwitch: UISwitch! @IBOutlet weak var showFullscreenArticlesSwitch: UISwitch!
@IBOutlet weak var colorPaletteDetailLabel: UILabel! @IBOutlet weak var colorPaletteDetailLabel: UILabel!
@IBOutlet weak var openLinksInNetNewsWire: UISwitch! @IBOutlet weak var openLinksInNetNewsWire: UISwitch!
var scrollToArticlesSection = false var scrollToArticlesSection = false
weak var presentingParentController: UIViewController? weak var presentingParentController: UIViewController?
override func viewDidLoad() { override func viewDidLoad() {
// This hack mostly works around a bug in static tables with dynamic type. See: https://spin.atomicobject.com/2018/10/15/dynamic-type-static-uitableview/ // This hack mostly works around a bug in static tables with dynamic type. See: https://spin.atomicobject.com/2018/10/15/dynamic-type-static-uitableview/
NotificationCenter.default.removeObserver(tableView!, name: UIContentSizeCategory.didChangeNotification, object: nil) NotificationCenter.default.removeObserver(tableView!, name: UIContentSizeCategory.didChangeNotification, object: nil)
@ -40,14 +40,14 @@ class SettingsViewController: UITableViewController {
tableView.register(UINib(nibName: "SettingsComboTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsComboTableViewCell") tableView.register(UINib(nibName: "SettingsComboTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsComboTableViewCell")
tableView.register(UINib(nibName: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell") tableView.register(UINib(nibName: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell")
tableView.rowHeight = UITableView.automaticDimension tableView.rowHeight = UITableView.automaticDimension
tableView.estimatedRowHeight = 44 tableView.estimatedRowHeight = 44
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
if AppDefaults.shared.timelineSortDirection == .orderedAscending { if AppDefaults.shared.timelineSortDirection == .orderedAscending {
timelineSortOrderSwitch.isOn = true timelineSortOrderSwitch.isOn = true
} else { } else {
@ -66,7 +66,6 @@ class SettingsViewController: UITableViewController {
refreshClearsReadArticlesSwitch.isOn = false refreshClearsReadArticlesSwitch.isOn = false
} }
articleThemeDetailLabel.text = ArticleThemesManager.shared.currentTheme.name articleThemeDetailLabel.text = ArticleThemesManager.shared.currentTheme.name
if AppDefaults.shared.confirmMarkAllAsRead { if AppDefaults.shared.confirmMarkAllAsRead {
@ -80,11 +79,10 @@ class SettingsViewController: UITableViewController {
} else { } else {
showFullscreenArticlesSwitch.isOn = false showFullscreenArticlesSwitch.isOn = false
} }
colorPaletteDetailLabel.text = String(describing: AppDefaults.userInterfaceColorPalette) colorPaletteDetailLabel.text = String(describing: AppDefaults.userInterfaceColorPalette)
openLinksInNetNewsWire.isOn = !AppDefaults.shared.useSystemBrowser openLinksInNetNewsWire.isOn = !AppDefaults.shared.useSystemBrowser
let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.0)) let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.0))
buildLabel.font = UIFont.systemFont(ofSize: 11.0) buildLabel.font = UIFont.systemFont(ofSize: 11.0)
@ -92,27 +90,27 @@ class SettingsViewController: UITableViewController {
buildLabel.text = "\(Bundle.main.appName) \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))" buildLabel.text = "\(Bundle.main.appName) \(Bundle.main.versionNumber) (Build \(Bundle.main.buildNumber))"
buildLabel.sizeToFit() buildLabel.sizeToFit()
buildLabel.translatesAutoresizingMaskIntoConstraints = false buildLabel.translatesAutoresizingMaskIntoConstraints = false
let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0)) let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0))
wrapperView.translatesAutoresizingMaskIntoConstraints = false wrapperView.translatesAutoresizingMaskIntoConstraints = false
wrapperView.addSubview(buildLabel) wrapperView.addSubview(buildLabel)
tableView.tableFooterView = wrapperView tableView.tableFooterView = wrapperView
} }
override func viewDidAppear(_ animated: Bool) { override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated) super.viewDidAppear(animated)
self.tableView.selectRow(at: nil, animated: true, scrollPosition: .none) self.tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
if scrollToArticlesSection { if scrollToArticlesSection {
tableView.scrollToRow(at: IndexPath(row: 0, section: 4), at: .top, animated: true) tableView.scrollToRow(at: IndexPath(row: 0, section: 4), at: .top, animated: true)
scrollToArticlesSection = false scrollToArticlesSection = false
} }
} }
// MARK: UITableView // MARK: UITableView
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section { switch section {
@ -237,7 +235,7 @@ class SettingsViewController: UITableViewController {
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool { override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
return false return false
} }
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool { override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
return false return false
} }
@ -245,21 +243,21 @@ class SettingsViewController: UITableViewController {
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle { override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
return .none return .none
} }
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
return UITableView.automaticDimension return UITableView.automaticDimension
} }
override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int { override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1)) return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1))
} }
// MARK: Actions // MARK: Actions
@IBAction func done(_ sender: Any) { @IBAction func done(_ sender: Any) {
dismiss(animated: true) dismiss(animated: true)
} }
@IBAction func switchTimelineOrder(_ sender: Any) { @IBAction func switchTimelineOrder(_ sender: Any) {
if timelineSortOrderSwitch.isOn { if timelineSortOrderSwitch.isOn {
AppDefaults.shared.timelineSortDirection = .orderedAscending AppDefaults.shared.timelineSortDirection = .orderedAscending
@ -267,7 +265,7 @@ class SettingsViewController: UITableViewController {
AppDefaults.shared.timelineSortDirection = .orderedDescending AppDefaults.shared.timelineSortDirection = .orderedDescending
} }
} }
@IBAction func switchGroupByFeed(_ sender: Any) { @IBAction func switchGroupByFeed(_ sender: Any) {
if groupByFeedSwitch.isOn { if groupByFeedSwitch.isOn {
AppDefaults.shared.timelineGroupByFeed = true AppDefaults.shared.timelineGroupByFeed = true
@ -275,7 +273,7 @@ class SettingsViewController: UITableViewController {
AppDefaults.shared.timelineGroupByFeed = false AppDefaults.shared.timelineGroupByFeed = false
} }
} }
@IBAction func switchClearsReadArticles(_ sender: Any) { @IBAction func switchClearsReadArticles(_ sender: Any) {
if refreshClearsReadArticlesSwitch.isOn { if refreshClearsReadArticlesSwitch.isOn {
AppDefaults.shared.refreshClearsReadArticles = true AppDefaults.shared.refreshClearsReadArticles = true
@ -283,7 +281,7 @@ class SettingsViewController: UITableViewController {
AppDefaults.shared.refreshClearsReadArticles = false AppDefaults.shared.refreshClearsReadArticles = false
} }
} }
@IBAction func switchConfirmMarkAllAsRead(_ sender: Any) { @IBAction func switchConfirmMarkAllAsRead(_ sender: Any) {
if confirmMarkAllAsReadSwitch.isOn { if confirmMarkAllAsReadSwitch.isOn {
AppDefaults.shared.confirmMarkAllAsRead = true AppDefaults.shared.confirmMarkAllAsRead = true
@ -291,7 +289,7 @@ class SettingsViewController: UITableViewController {
AppDefaults.shared.confirmMarkAllAsRead = false AppDefaults.shared.confirmMarkAllAsRead = false
} }
} }
@IBAction func switchFullscreenArticles(_ sender: Any) { @IBAction func switchFullscreenArticles(_ sender: Any) {
if showFullscreenArticlesSwitch.isOn { if showFullscreenArticlesSwitch.isOn {
AppDefaults.shared.articleFullscreenAvailable = true AppDefaults.shared.articleFullscreenAvailable = true
@ -299,7 +297,7 @@ class SettingsViewController: UITableViewController {
AppDefaults.shared.articleFullscreenAvailable = false AppDefaults.shared.articleFullscreenAvailable = false
} }
} }
@IBAction func switchBrowserPreference(_ sender: Any) { @IBAction func switchBrowserPreference(_ sender: Any) {
if openLinksInNetNewsWire.isOn { if openLinksInNetNewsWire.isOn {
AppDefaults.shared.useSystemBrowser = false AppDefaults.shared.useSystemBrowser = false
@ -307,14 +305,13 @@ class SettingsViewController: UITableViewController {
AppDefaults.shared.useSystemBrowser = true AppDefaults.shared.useSystemBrowser = true
} }
} }
// MARK: Notifications // MARK: Notifications
@objc func contentSizeCategoryDidChange() { @objc func contentSizeCategoryDidChange() {
tableView.reloadData() tableView.reloadData()
} }
@objc func accountsDidChange() { @objc func accountsDidChange() {
tableView.reloadData() tableView.reloadData()
} }
@ -322,17 +319,17 @@ class SettingsViewController: UITableViewController {
@objc func displayNameDidChange() { @objc func displayNameDidChange() {
tableView.reloadData() tableView.reloadData()
} }
@objc func browserPreferenceDidChange() { @objc func browserPreferenceDidChange() {
tableView.reloadData() tableView.reloadData()
} }
} }
// MARK: OPML Document Picker // MARK: OPML Document Picker
extension SettingsViewController: UIDocumentPickerDelegate { extension SettingsViewController: UIDocumentPickerDelegate {
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
for url in urls { for url in urls {
opmlAccount?.importOPML(url) { result in opmlAccount?.importOPML(url) { result in
@ -347,13 +344,13 @@ extension SettingsViewController: UIDocumentPickerDelegate {
} }
} }
} }
} }
// MARK: Private // MARK: Private
private extension SettingsViewController { private extension SettingsViewController {
func addFeed() { func addFeed() {
self.dismiss(animated: true) self.dismiss(animated: true)
@ -363,10 +360,10 @@ private extension SettingsViewController {
addViewController.initialFeedName = NSLocalizedString("NetNewsWire News", comment: "NetNewsWire News") addViewController.initialFeedName = NSLocalizedString("NetNewsWire News", comment: "NetNewsWire News")
addNavViewController.modalPresentationStyle = .formSheet addNavViewController.modalPresentationStyle = .formSheet
addNavViewController.preferredContentSize = AddFeedViewController.preferredContentSizeForFormSheetDisplay addNavViewController.preferredContentSize = AddFeedViewController.preferredContentSizeForFormSheetDisplay
presentingParentController?.present(addNavViewController, animated: true) presentingParentController?.present(addNavViewController, animated: true)
} }
func importOPML(sourceView: UIView, sourceRect: CGRect) { func importOPML(sourceView: UIView, sourceRect: CGRect) {
switch AccountManager.shared.activeAccounts.count { switch AccountManager.shared.activeAccounts.count {
case 0: case 0:
@ -378,18 +375,18 @@ private extension SettingsViewController {
importOPMLAccountPicker(sourceView: sourceView, sourceRect: sourceRect) importOPMLAccountPicker(sourceView: sourceView, sourceRect: sourceRect)
} }
} }
func importOPMLAccountPicker(sourceView: UIView, sourceRect: CGRect) { func importOPMLAccountPicker(sourceView: UIView, sourceRect: CGRect) {
let title = NSLocalizedString("Choose an account to receive the imported feeds and folders", comment: "Import Account") let title = NSLocalizedString("Choose an account to receive the imported feeds and folders", comment: "Import Account")
let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
if let popoverController = alert.popoverPresentationController { if let popoverController = alert.popoverPresentationController {
popoverController.sourceView = view popoverController.sourceView = view
popoverController.sourceRect = sourceRect popoverController.sourceRect = sourceRect
} }
for account in AccountManager.shared.sortedActiveAccounts { for account in AccountManager.shared.sortedActiveAccounts {
let action = UIAlertAction(title: account.nameForDisplay, style: .default) { [weak self] action in let action = UIAlertAction(title: account.nameForDisplay, style: .default) { [weak self] _ in
self?.opmlAccount = account self?.opmlAccount = account
self?.importOPMLDocumentPicker() self?.importOPMLDocumentPicker()
} }
@ -401,15 +398,15 @@ private extension SettingsViewController {
self.present(alert, animated: true) self.present(alert, animated: true)
} }
func importOPMLDocumentPicker() { func importOPMLDocumentPicker() {
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.opml, UTType.xml], asCopy: true) let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.opml, UTType.xml], asCopy: true)
documentPicker.delegate = self documentPicker.delegate = self
documentPicker.modalPresentationStyle = .formSheet documentPicker.modalPresentationStyle = .formSheet
self.present(documentPicker, animated: true) self.present(documentPicker, animated: true)
} }
func exportOPML(sourceView: UIView, sourceRect: CGRect) { func exportOPML(sourceView: UIView, sourceRect: CGRect) {
if AccountManager.shared.accounts.count == 1 { if AccountManager.shared.accounts.count == 1 {
opmlAccount = AccountManager.shared.accounts.first! opmlAccount = AccountManager.shared.accounts.first!
@ -418,18 +415,18 @@ private extension SettingsViewController {
exportOPMLAccountPicker(sourceView: sourceView, sourceRect: sourceRect) exportOPMLAccountPicker(sourceView: sourceView, sourceRect: sourceRect)
} }
} }
func exportOPMLAccountPicker(sourceView: UIView, sourceRect: CGRect) { func exportOPMLAccountPicker(sourceView: UIView, sourceRect: CGRect) {
let title = NSLocalizedString("Choose an account with the subscriptions to export", comment: "Export Account") let title = NSLocalizedString("Choose an account with the subscriptions to export", comment: "Export Account")
let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet) let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
if let popoverController = alert.popoverPresentationController { if let popoverController = alert.popoverPresentationController {
popoverController.sourceView = view popoverController.sourceView = view
popoverController.sourceRect = sourceRect popoverController.sourceRect = sourceRect
} }
for account in AccountManager.shared.sortedAccounts { for account in AccountManager.shared.sortedAccounts {
let action = UIAlertAction(title: account.nameForDisplay, style: .default) { [weak self] action in let action = UIAlertAction(title: account.nameForDisplay, style: .default) { [weak self] _ in
self?.opmlAccount = account self?.opmlAccount = account
self?.exportOPMLDocumentPicker() self?.exportOPMLDocumentPicker()
} }
@ -441,10 +438,10 @@ private extension SettingsViewController {
self.present(alert, animated: true) self.present(alert, animated: true)
} }
func exportOPMLDocumentPicker() { func exportOPMLDocumentPicker() {
guard let account = opmlAccount else { return } guard let account = opmlAccount else { return }
let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces) let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces)
let filename = "Subscriptions-\(accountName).opml" let filename = "Subscriptions-\(accountName).opml"
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename) let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
@ -454,16 +451,16 @@ private extension SettingsViewController {
} catch { } catch {
self.presentError(title: "OPML Export Error", message: error.localizedDescription) self.presentError(title: "OPML Export Error", message: error.localizedDescription)
} }
let documentPicker = UIDocumentPickerViewController(forExporting: [tempFile]) let documentPicker = UIDocumentPickerViewController(forExporting: [tempFile])
documentPicker.modalPresentationStyle = .formSheet documentPicker.modalPresentationStyle = .formSheet
self.present(documentPicker, animated: true) self.present(documentPicker, animated: true)
} }
func openURL(_ urlString: String) { func openURL(_ urlString: String) {
let vc = SFSafariViewController(url: URL(string: urlString)!) let vc = SFSafariViewController(url: URL(string: urlString)!)
vc.modalPresentationStyle = .pageSheet vc.modalPresentationStyle = .pageSheet
present(vc, animated: true) present(vc, animated: true)
} }
} }

View File

@ -14,15 +14,15 @@ class TimelineCustomizerViewController: UIViewController {
@IBOutlet weak var iconSizeSlider: TickMarkSlider! @IBOutlet weak var iconSizeSlider: TickMarkSlider!
@IBOutlet weak var numberOfLinesSliderContainerView: UIView! @IBOutlet weak var numberOfLinesSliderContainerView: UIView!
@IBOutlet weak var numberOfLinesSlider: TickMarkSlider! @IBOutlet weak var numberOfLinesSlider: TickMarkSlider!
@IBOutlet weak var previewWidthConstraint: NSLayoutConstraint! @IBOutlet weak var previewWidthConstraint: NSLayoutConstraint!
@IBOutlet weak var previewHeightConstraint: NSLayoutConstraint! @IBOutlet weak var previewHeightConstraint: NSLayoutConstraint!
@IBOutlet weak var previewContainerView: UIView! @IBOutlet weak var previewContainerView: UIView!
var previewController: TimelinePreviewTableViewController { var previewController: TimelinePreviewTableViewController {
return children.first as! TimelinePreviewTableViewController return children.first as! TimelinePreviewTableViewController
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
@ -34,13 +34,13 @@ class TimelineCustomizerViewController: UIViewController {
numberOfLinesSlider.value = Float(AppDefaults.shared.timelineNumberOfLines) numberOfLinesSlider.value = Float(AppDefaults.shared.timelineNumberOfLines)
numberOfLinesSlider.addTickMarks() numberOfLinesSlider.addTickMarks()
} }
override func viewWillAppear(_ animated: Bool) { override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated) super.viewWillAppear(animated)
updatePreviewBorder() updatePreviewBorder()
updatePreview() updatePreview()
} }
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) { override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
updatePreviewBorder() updatePreviewBorder()
updatePreview() updatePreview()
@ -51,18 +51,18 @@ class TimelineCustomizerViewController: UIViewController {
AppDefaults.shared.timelineIconSize = iconSize AppDefaults.shared.timelineIconSize = iconSize
updatePreview() updatePreview()
} }
@IBAction func numberOfLinesChanged(_ sender: Any) { @IBAction func numberOfLinesChanged(_ sender: Any) {
AppDefaults.shared.timelineNumberOfLines = Int(numberOfLinesSlider.value.rounded()) AppDefaults.shared.timelineNumberOfLines = Int(numberOfLinesSlider.value.rounded())
updatePreview() updatePreview()
} }
} }
// MARK: Private // MARK: Private
private extension TimelineCustomizerViewController { private extension TimelineCustomizerViewController {
func updatePreview() { func updatePreview() {
let previewWidth: CGFloat = { let previewWidth: CGFloat = {
if traitCollection.userInterfaceIdiom == .phone { if traitCollection.userInterfaceIdiom == .phone {
@ -71,13 +71,13 @@ private extension TimelineCustomizerViewController {
return view.bounds.width / 1.5 return view.bounds.width / 1.5
} }
}() }()
previewWidthConstraint.constant = previewWidth previewWidthConstraint.constant = previewWidth
previewHeightConstraint.constant = previewController.heightFor(width: previewWidth) previewHeightConstraint.constant = previewController.heightFor(width: previewWidth)
previewController.reload() previewController.reload()
} }
func updatePreviewBorder() { func updatePreviewBorder() {
if traitCollection.userInterfaceStyle == .dark { if traitCollection.userInterfaceStyle == .dark {
previewContainerView.layer.borderColor = UIColor.black.cgColor previewContainerView.layer.borderColor = UIColor.black.cgColor
@ -86,5 +86,5 @@ private extension TimelineCustomizerViewController {
previewContainerView.layer.borderWidth = 0 previewContainerView.layer.borderWidth = 0
} }
} }
} }

View File

@ -12,13 +12,13 @@ import Articles
class TimelinePreviewTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource { class TimelinePreviewTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
@IBOutlet weak var tableView: UITableView! @IBOutlet weak var tableView: UITableView!
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
tableView.delegate = self tableView.delegate = self
tableView.dataSource = self tableView.dataSource = self
} }
func heightFor(width: CGFloat) -> CGFloat { func heightFor(width: CGFloat) -> CGFloat {
@ -46,7 +46,7 @@ class TimelinePreviewTableViewController: UIViewController, UITableViewDelegate,
cell.cellData = prototypeCellData cell.cellData = prototypeCellData
return cell return cell
} }
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.selectRow(at: nil, animated: true, scrollPosition: .none) tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
} }
@ -64,14 +64,14 @@ private extension TimelinePreviewTableViewController {
var prototypeCellData: MainTimelineCellData { var prototypeCellData: MainTimelineCellData {
let longTitle = "Enim ut tellus elementum sagittis vitae et. Nibh praesent tristique magna sit amet purus gravida quis blandit. Neque volutpat ac tincidunt vitae semper quis lectus nulla. Massa id neque aliquam vestibulum morbi blandit. Ultrices vitae auctor eu augue. Enim eu turpis egestas pretium aenean pharetra magna. Eget gravida cum sociis natoque. Sit amet consectetur adipiscing elit. Auctor eu augue ut lectus arcu bibendum. Maecenas volutpat blandit aliquam etiam erat velit. Ut pharetra sit amet aliquam id diam maecenas ultricies. In hac habitasse platea dictumst quisque sagittis purus sit amet." let longTitle = "Enim ut tellus elementum sagittis vitae et. Nibh praesent tristique magna sit amet purus gravida quis blandit. Neque volutpat ac tincidunt vitae semper quis lectus nulla. Massa id neque aliquam vestibulum morbi blandit. Ultrices vitae auctor eu augue. Enim eu turpis egestas pretium aenean pharetra magna. Eget gravida cum sociis natoque. Sit amet consectetur adipiscing elit. Auctor eu augue ut lectus arcu bibendum. Maecenas volutpat blandit aliquam etiam erat velit. Ut pharetra sit amet aliquam id diam maecenas ultricies. In hac habitasse platea dictumst quisque sagittis purus sit amet."
let prototypeID = "prototype" let prototypeID = "prototype"
let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, dateArrived: Date()) let status = ArticleStatus(articleID: prototypeID, read: false, starred: false, dateArrived: Date())
let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status) let prototypeArticle = Article(accountID: prototypeID, articleID: prototypeID, feedID: prototypeID, uniqueID: prototypeID, title: longTitle, contentHTML: nil, contentText: nil, url: nil, externalURL: nil, summary: nil, imageURL: nil, datePublished: nil, dateModified: nil, authors: nil, status: status)
let iconImage = IconImage(AppAssets.faviconTemplateImage.withTintColor(AppAssets.secondaryAccentColor)) let iconImage = IconImage(AppAssets.faviconTemplateImage.withTintColor(AppAssets.secondaryAccentColor))
return MainTimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Feed Name", byline: nil, iconImage: iconImage, showIcon: true, numberOfLines: AppDefaults.shared.timelineNumberOfLines, iconSize: AppDefaults.shared.timelineIconSize) return MainTimelineCellData(article: prototypeArticle, showFeedName: .feed, feedName: "Feed Name", byline: nil, iconImage: iconImage, showIcon: true, numberOfLines: AppDefaults.shared.timelineNumberOfLines, iconSize: AppDefaults.shared.timelineIconSize)
} }
} }

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
class ShareFolderPickerCell: UITableViewCell { class ShareFolderPickerCell: UITableViewCell {
@IBOutlet weak var icon: UIImageView! @IBOutlet weak var icon: UIImageView!
@IBOutlet weak var label: UILabel! @IBOutlet weak var label: UILabel!
} }

View File

@ -20,13 +20,13 @@ class ShareFolderPickerController: UITableViewController {
var selectedContainerID: ContainerIdentifier? var selectedContainerID: ContainerIdentifier?
weak var delegate: ShareFolderPickerControllerDelegate? weak var delegate: ShareFolderPickerControllerDelegate?
override func viewDidLoad() { override func viewDidLoad() {
tableView.register(UINib(nibName: "ShareFolderPickerAccountCell", bundle: Bundle.main), forCellReuseIdentifier: "AccountCell") tableView.register(UINib(nibName: "ShareFolderPickerAccountCell", bundle: Bundle.main), forCellReuseIdentifier: "AccountCell")
tableView.register(UINib(nibName: "ShareFolderPickerFolderCell", bundle: Bundle.main), forCellReuseIdentifier: "FolderCell") tableView.register(UINib(nibName: "ShareFolderPickerFolderCell", bundle: Bundle.main), forCellReuseIdentifier: "FolderCell")
} }
override func numberOfSections(in tableView: UITableView) -> Int { override func numberOfSections(in tableView: UITableView) -> Int {
return 1 return 1
} }
@ -34,7 +34,7 @@ class ShareFolderPickerController: UITableViewController {
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return containers?.count ?? 0 return containers?.count ?? 0
} }
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let container = containers?[indexPath.row] let container = containers?[indexPath.row]
let cell: ShareFolderPickerCell = { let cell: ShareFolderPickerCell = {
@ -44,7 +44,7 @@ class ShareFolderPickerController: UITableViewController {
return tableView.dequeueReusableCell(withIdentifier: "FolderCell", for: indexPath) as! ShareFolderPickerCell return tableView.dequeueReusableCell(withIdentifier: "FolderCell", for: indexPath) as! ShareFolderPickerCell
} }
}() }()
if let account = container as? ExtensionAccount { if let account = container as? ExtensionAccount {
cell.icon.image = AppAssets.image(for: account.type) cell.icon.image = AppAssets.image(for: account.type)
} else { } else {
@ -58,13 +58,13 @@ class ShareFolderPickerController: UITableViewController {
} else { } else {
cell.accessoryType = .none cell.accessoryType = .none
} }
return cell return cell
} }
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
guard let container = containers?[indexPath.row] else { return } guard let container = containers?[indexPath.row] else { return }
if let account = container as? ExtensionAccount, account.disallowFeedInRootFolder { if let account = container as? ExtensionAccount, account.disallowFeedInRootFolder {
tableView.selectRow(at: nil, animated: false, scrollPosition: .none) tableView.selectRow(at: nil, animated: false, scrollPosition: .none)
} else { } else {
@ -73,5 +73,5 @@ class ShareFolderPickerController: UITableViewController {
delegate?.shareFolderPickerDidSelect(container) delegate?.shareFolderPickerDidSelect(container)
} }
} }
} }

View File

@ -15,15 +15,15 @@ import RSTree
import UniformTypeIdentifiers import UniformTypeIdentifiers
class ShareViewController: SLComposeServiceViewController, ShareFolderPickerControllerDelegate { class ShareViewController: SLComposeServiceViewController, ShareFolderPickerControllerDelegate {
private var url: URL? private var url: URL?
private var extensionContainers: ExtensionContainers? private var extensionContainers: ExtensionContainers?
private var flattenedContainers: [ExtensionContainer]! private var flattenedContainers: [ExtensionContainer]!
private var selectedContainer: ExtensionContainer? private var selectedContainer: ExtensionContainer?
private var folderItem: SLComposeSheetConfigurationItem! private var folderItem: SLComposeSheetConfigurationItem!
override func viewDidLoad() { override func viewDidLoad() {
extensionContainers = ExtensionContainersFile.read() extensionContainers = ExtensionContainersFile.read()
flattenedContainers = extensionContainers?.flattened ?? [ExtensionContainer]() flattenedContainers = extensionContainers?.flattened ?? [ExtensionContainer]()
if let extensionContainers = extensionContainers { if let extensionContainers = extensionContainers {
@ -42,7 +42,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
tableView.rowHeight = 38 tableView.rowHeight = 38
} }
var provider: NSItemProvider? = nil var provider: NSItemProvider?
// Try to get any HTML that is maybe passed in // Try to get any HTML that is maybe passed in
for item in self.extensionContext!.inputItems as! [NSExtensionItem] { for item in self.extensionContext!.inputItems as! [NSExtensionItem] {
@ -53,7 +53,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
} }
} }
if provider != nil { if provider != nil {
provider!.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { [weak self] (pList, error) in provider!.loadItem(forTypeIdentifier: UTType.propertyList.identifier, options: nil, completionHandler: { [weak self] (pList, error) in
if error != nil { if error != nil {
return return
@ -80,7 +80,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
} }
} }
if provider != nil { if provider != nil {
provider!.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil, completionHandler: { [weak self] (urlCoded, error) in provider!.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil, completionHandler: { [weak self] (urlCoded, error) in
if error != nil { if error != nil {
return return
@ -92,32 +92,32 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
return return
}) })
} }
// Reddit in particular doesn't pass the URL correctly and instead puts it in the contentText // Reddit in particular doesn't pass the URL correctly and instead puts it in the contentText
url = URL(string: contentText) url = URL(string: contentText)
} }
override func isContentValid() -> Bool { override func isContentValid() -> Bool {
return url != nil && selectedContainer != nil return url != nil && selectedContainer != nil
} }
override func didSelectPost() { override func didSelectPost() {
guard let url = url, let selectedContainer = selectedContainer, let containerID = selectedContainer.containerID else { guard let url = url, let selectedContainer = selectedContainer, let containerID = selectedContainer.containerID else {
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
return return
} }
var name: String? = nil var name: String?
if !contentText.mayBeURL { if !contentText.mayBeURL {
name = contentText.isEmpty ? nil : contentText name = contentText.isEmpty ? nil : contentText
} }
let request = ExtensionFeedAddRequest(name: name, feedURL: url, destinationContainerID: containerID) let request = ExtensionFeedAddRequest(name: name, feedURL: url, destinationContainerID: containerID)
ExtensionFeedAddRequestFile.save(request) ExtensionFeedAddRequestFile.save(request)
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil) self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
} }
func shareFolderPickerDidSelect(_ container: ExtensionContainer) { func shareFolderPickerDidSelect(_ container: ExtensionContainer) {
ShareDefaultContainer.saveDefaultContainer(container) ShareDefaultContainer.saveDefaultContainer(container)
self.selectedContainer = container self.selectedContainer = container
@ -126,37 +126,37 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
} }
override func configurationItems() -> [Any]! { override func configurationItems() -> [Any]! {
// To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here. // To add configuration options via table cells at the bottom of the sheet, return an array of SLComposeSheetConfigurationItem here.
guard let urlItem = SLComposeSheetConfigurationItem() else { return nil } guard let urlItem = SLComposeSheetConfigurationItem() else { return nil }
urlItem.title = "URL" urlItem.title = "URL"
urlItem.value = url?.absoluteString ?? "" urlItem.value = url?.absoluteString ?? ""
folderItem = SLComposeSheetConfigurationItem() folderItem = SLComposeSheetConfigurationItem()
folderItem.title = "Folder" folderItem.title = "Folder"
updateFolderItemValue() updateFolderItemValue()
folderItem.tapHandler = { folderItem.tapHandler = {
let folderPickerController = ShareFolderPickerController() let folderPickerController = ShareFolderPickerController()
folderPickerController.navigationController?.title = NSLocalizedString("Folder", comment: "Folder") folderPickerController.navigationController?.title = NSLocalizedString("Folder", comment: "Folder")
folderPickerController.delegate = self folderPickerController.delegate = self
folderPickerController.containers = self.flattenedContainers folderPickerController.containers = self.flattenedContainers
folderPickerController.selectedContainerID = self.selectedContainer?.containerID folderPickerController.selectedContainerID = self.selectedContainer?.containerID
self.pushConfigurationViewController(folderPickerController) self.pushConfigurationViewController(folderPickerController)
} }
return [folderItem!, urlItem] return [folderItem!, urlItem]
} }
} }
private extension ShareViewController { private extension ShareViewController {
func updateFolderItemValue() { func updateFolderItemValue() {
if let account = selectedContainer as? ExtensionAccount { if let account = selectedContainer as? ExtensionAccount {
self.folderItem.value = account.name self.folderItem.value = account.name
@ -164,5 +164,5 @@ private extension ShareViewController {
self.folderItem.value = "\(folder.accountName) / \(folder.name)" self.folderItem.value = "\(folder.accountName) / \(folder.name)"
} }
} }
} }

View File

@ -37,5 +37,5 @@ class TitleActivityItemSource: NSObject, UIActivityItemSource {
return NSNull() return NSNull()
} }
} }
} }

View File

@ -10,16 +10,16 @@ import Foundation
/// Used to select which animations should be performed /// Used to select which animations should be performed
public struct Animations: OptionSet { public struct Animations: OptionSet {
/// Selections and deselections will be animated. /// Selections and deselections will be animated.
public static let select = Animations(rawValue: 1) public static let select = Animations(rawValue: 1)
/// Scrolling will be animated /// Scrolling will be animated
public static let scroll = Animations(rawValue: 2) public static let scroll = Animations(rawValue: 2)
/// Pushing and popping navigation view controllers will be animated /// Pushing and popping navigation view controllers will be animated
public static let navigation = Animations(rawValue: 4) public static let navigation = Animations(rawValue: 4)
public let rawValue: Int public let rawValue: Int
public init(rawValue: Int) { public init(rawValue: Int) {
self.rawValue = rawValue self.rawValue = rawValue

View File

@ -9,14 +9,14 @@
import UIKit import UIKit
extension Array where Element == CGRect { extension Array where Element == CGRect {
func maxY() -> CGFloat { func maxY() -> CGFloat {
var y: CGFloat = 0.0 var y: CGFloat = 0.0
for oneRect in self { for oneRect in self {
y = Swift.max(y, oneRect.maxY) y = Swift.max(y, oneRect.maxY)
} }
return y return y
} }
} }

View File

@ -9,17 +9,17 @@
import Foundation import Foundation
extension Bundle { extension Bundle {
var appName: String { var appName: String {
return infoDictionary?["CFBundleName"] as! String return infoDictionary?["CFBundleName"] as! String
} }
var versionNumber: String { var versionNumber: String {
return infoDictionary?["CFBundleShortVersionString"] as! String return infoDictionary?["CFBundleShortVersionString"] as! String
} }
var buildNumber: String { var buildNumber: String {
return infoDictionary?["CFBundleVersion"] as! String return infoDictionary?["CFBundleVersion"] as! String
} }
} }

View File

@ -9,11 +9,11 @@
import UIKit import UIKit
class CroppingPreviewParameters: UIPreviewParameters { class CroppingPreviewParameters: UIPreviewParameters {
override init() { override init() {
super.init() super.init()
} }
init(view: UIView) { init(view: UIView) {
super.init() super.init()
let newBounds = CGRect(x: 1, y: 1, width: view.bounds.width - 2, height: view.bounds.height - 2) let newBounds = CGRect(x: 1, y: 1, width: view.bounds.width - 2, height: view.bounds.height - 2)

View File

@ -11,19 +11,19 @@ import UIKit
class ImageHeaderView: UITableViewHeaderFooterView { class ImageHeaderView: UITableViewHeaderFooterView {
static let rowHeight = CGFloat(integerLiteral: 88) static let rowHeight = CGFloat(integerLiteral: 88)
var imageView = UIImageView() var imageView = UIImageView()
override init(reuseIdentifier: String?) { override init(reuseIdentifier: String?) {
super.init(reuseIdentifier: reuseIdentifier) super.init(reuseIdentifier: reuseIdentifier)
commonInit() commonInit()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
func commonInit() { func commonInit() {
imageView.tintColor = UIColor.label imageView.tintColor = UIColor.label
imageView.contentMode = .scaleAspectFit imageView.contentMode = .scaleAspectFit

View File

@ -59,10 +59,9 @@ class InteractiveLabel: UILabel, UIEditMenuInteractionDelegate {
func editMenuInteraction(_ interaction: UIEditMenuInteraction, menuFor configuration: UIEditMenuConfiguration, suggestedActions: [UIMenuElement]) -> UIMenu? { func editMenuInteraction(_ interaction: UIEditMenuInteraction, menuFor configuration: UIEditMenuConfiguration, suggestedActions: [UIMenuElement]) -> UIMenu? {
let copyAction = UIAction(title: "Copy", image: nil) { [weak self] action in let copyAction = UIAction(title: "Copy", image: nil) { [weak self] _ in
self?.copy(nil) self?.copy(nil)
} }
return UIMenu(title: "", children: [copyAction]) return UIMenu(title: "", children: [copyAction])
} }
} }

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
class InteractiveNavigationController: UINavigationController { class InteractiveNavigationController: UINavigationController {
private let poppableDelegate = PoppableGestureRecognizerDelegate() private let poppableDelegate = PoppableGestureRecognizerDelegate()
static func template() -> UINavigationController { static func template() -> UINavigationController {
@ -17,13 +17,13 @@ class InteractiveNavigationController: UINavigationController {
navController.configure() navController.configure()
return navController return navController
} }
static func template(rootViewController: UIViewController) -> UINavigationController { static func template(rootViewController: UIViewController) -> UINavigationController {
let navController = InteractiveNavigationController(rootViewController: rootViewController) let navController = InteractiveNavigationController(rootViewController: rootViewController)
navController.configure() navController.configure()
return navController return navController
} }
override func viewDidLoad() { override func viewDidLoad() {
super.viewDidLoad() super.viewDidLoad()
poppableDelegate.navigationController = self poppableDelegate.navigationController = self
@ -40,21 +40,21 @@ class InteractiveNavigationController: UINavigationController {
// MARK: Private // MARK: Private
private extension InteractiveNavigationController { private extension InteractiveNavigationController {
func configure() { func configure() {
isToolbarHidden = false isToolbarHidden = false
let navigationStandardAppearance = UINavigationBarAppearance() let navigationStandardAppearance = UINavigationBarAppearance()
navigationStandardAppearance.titleTextAttributes = [.foregroundColor: UIColor.label] navigationStandardAppearance.titleTextAttributes = [.foregroundColor: UIColor.label]
navigationStandardAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.label] navigationStandardAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.label]
navigationBar.standardAppearance = navigationStandardAppearance navigationBar.standardAppearance = navigationStandardAppearance
let scrollEdgeStandardAppearance = UINavigationBarAppearance() let scrollEdgeStandardAppearance = UINavigationBarAppearance()
scrollEdgeStandardAppearance.backgroundColor = .systemBackground scrollEdgeStandardAppearance.backgroundColor = .systemBackground
navigationBar.scrollEdgeAppearance = scrollEdgeStandardAppearance navigationBar.scrollEdgeAppearance = scrollEdgeStandardAppearance
navigationBar.tintColor = AppAssets.primaryAccentColor navigationBar.tintColor = AppAssets.primaryAccentColor
let toolbarAppearance = UIToolbarAppearance() let toolbarAppearance = UIToolbarAppearance()
toolbar.standardAppearance = toolbarAppearance toolbar.standardAppearance = toolbarAppearance
toolbar.compactAppearance = toolbarAppearance toolbar.compactAppearance = toolbarAppearance

View File

@ -12,10 +12,10 @@ class ModalNavigationController: UINavigationController {
override func viewDidLayoutSubviews() { override func viewDidLayoutSubviews() {
super.viewDidLayoutSubviews() super.viewDidLayoutSubviews()
// This hack is to resolve https://github.com/brentsimmons/NetNewsWire/issues/1301 // This hack is to resolve https://github.com/brentsimmons/NetNewsWire/issues/1301
let frame = navigationBar.frame let frame = navigationBar.frame
navigationBar.frame = CGRect(x: frame.minX, y: frame.minY, width: frame.size.width, height: 64.0) navigationBar.frame = CGRect(x: frame.minX, y: frame.minY, width: frame.size.width, height: 64.0)
} }
} }

View File

@ -14,5 +14,5 @@ class NonIntrinsicLabel: UILabel {
override var intrinsicContentSize: CGSize { override var intrinsicContentSize: CGSize {
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric) return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
} }
} }

View File

@ -10,7 +10,7 @@
import UIKit import UIKit
final class PoppableGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate { final class PoppableGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate {
weak var navigationController: UINavigationController? weak var navigationController: UINavigationController?
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool { func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
@ -20,12 +20,12 @@ final class PoppableGestureRecognizerDelegate: NSObject, UIGestureRecognizerDele
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
return true return true
} }
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool { func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
if otherGestureRecognizer is UIPanGestureRecognizer { if otherGestureRecognizer is UIPanGestureRecognizer {
return true return true
} }
return false return false
} }
} }

View File

@ -9,17 +9,17 @@
import UIKit import UIKit
extension String { extension String {
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat { func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude) let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil) let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.height) return ceil(boundingBox.height)
} }
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat { func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height) let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil) let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil)
return ceil(boundingBox.width) return ceil(boundingBox.width)
} }
} }

View File

@ -12,7 +12,7 @@ class TickMarkSlider: UISlider {
private var enableFeedback = false private var enableFeedback = false
private let feedbackGenerator = UISelectionFeedbackGenerator() private let feedbackGenerator = UISelectionFeedbackGenerator()
private var roundedValue: Float? private var roundedValue: Float?
override var value: Float { override var value: Float {
didSet { didSet {
@ -23,17 +23,17 @@ class TickMarkSlider: UISlider {
} }
} }
} }
func addTickMarks() { func addTickMarks() {
enableFeedback = true enableFeedback = true
let numberOfGaps = Int(maximumValue) - Int(minimumValue) let numberOfGaps = Int(maximumValue) - Int(minimumValue)
var gapLayoutGuides = [UILayoutGuide]() var gapLayoutGuides = [UILayoutGuide]()
for i in 0...numberOfGaps { for i in 0...numberOfGaps {
let tick = UIView() let tick = UIView()
tick.translatesAutoresizingMaskIntoConstraints = false tick.translatesAutoresizingMaskIntoConstraints = false
tick.backgroundColor = AppAssets.tickMarkColor tick.backgroundColor = AppAssets.tickMarkColor
@ -46,11 +46,11 @@ class TickMarkSlider: UISlider {
if i == 0 { if i == 0 {
tick.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true tick.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
} }
if let lastGapLayoutGuild = gapLayoutGuides.last { if let lastGapLayoutGuild = gapLayoutGuides.last {
lastGapLayoutGuild.trailingAnchor.constraint(equalTo: tick.leadingAnchor).isActive = true lastGapLayoutGuild.trailingAnchor.constraint(equalTo: tick.leadingAnchor).isActive = true
} }
if i != numberOfGaps { if i != numberOfGaps {
let gapLayoutGuild = UILayoutGuide() let gapLayoutGuild = UILayoutGuide()
gapLayoutGuides.append(gapLayoutGuild) gapLayoutGuides.append(gapLayoutGuild)
@ -59,17 +59,17 @@ class TickMarkSlider: UISlider {
} else { } else {
tick.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true tick.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
} }
} }
if let firstGapLayoutGuild = gapLayoutGuides.first { if let firstGapLayoutGuild = gapLayoutGuides.first {
for i in 1..<gapLayoutGuides.count { for i in 1..<gapLayoutGuides.count {
gapLayoutGuides[i].widthAnchor.constraint(equalTo: firstGapLayoutGuild.widthAnchor).isActive = true gapLayoutGuides[i].widthAnchor.constraint(equalTo: firstGapLayoutGuild.widthAnchor).isActive = true
} }
} }
} }
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool { override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
let result = super.continueTracking(touch, with: event) let result = super.continueTracking(touch, with: event)
value = value.rounded() value = value.rounded()
@ -79,5 +79,5 @@ class TickMarkSlider: UISlider {
override func endTracking(_ touch: UITouch?, with event: UIEvent?) { override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
value = value.rounded() value = value.rounded()
} }
} }

View File

@ -12,7 +12,7 @@ extension UIActivityViewController {
convenience init(url: URL, title: String?, applicationActivities: [UIActivity]?) { convenience init(url: URL, title: String?, applicationActivities: [UIActivity]?) {
let itemSource = ArticleActivityItemSource(url: url, subject: title) let itemSource = ArticleActivityItemSource(url: url, subject: title)
let titleSource = TitleActivityItemSource(title: title) let titleSource = TitleActivityItemSource(title: title)
self.init(activityItems: [titleSource, itemSource], applicationActivities: applicationActivities) self.init(activityItems: [titleSource, itemSource], applicationActivities: applicationActivities)
} }
} }

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
public extension UIBarButtonItem { public extension UIBarButtonItem {
@IBInspectable var accEnabled: Bool { @IBInspectable var accEnabled: Bool {
get { get {
return isAccessibilityElement return isAccessibilityElement
@ -18,7 +18,7 @@ public extension UIBarButtonItem {
isAccessibilityElement = newValue isAccessibilityElement = newValue
} }
} }
@IBInspectable var accLabelText: String? { @IBInspectable var accLabelText: String? {
get { get {
return accessibilityLabel return accessibilityLabel
@ -27,5 +27,5 @@ public extension UIBarButtonItem {
accessibilityLabel = newValue accessibilityLabel = newValue
} }
} }
} }

View File

@ -9,21 +9,21 @@
import UIKit import UIKit
extension UIFont { extension UIFont {
func withTraits(traits:UIFontDescriptor.SymbolicTraits) -> UIFont { func withTraits(traits: UIFontDescriptor.SymbolicTraits) -> UIFont {
if let descriptor = fontDescriptor.withSymbolicTraits(traits) { if let descriptor = fontDescriptor.withSymbolicTraits(traits) {
return UIFont(descriptor: descriptor, size: 0) //size 0 means keep the size as it is return UIFont(descriptor: descriptor, size: 0) // size 0 means keep the size as it is
} else { } else {
return self return self
} }
} }
func bold() -> UIFont { func bold() -> UIFont {
return withTraits(traits: .traitBold) return withTraits(traits: .traitBold)
} }
func italic() -> UIFont { func italic() -> UIFont {
return withTraits(traits: .traitItalic) return withTraits(traits: .traitItalic)
} }
} }

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
extension UIPageViewController { extension UIPageViewController {
var scrollViewInsidePageControl: UIScrollView? { var scrollViewInsidePageControl: UIScrollView? {
for view in view.subviews { for view in view.subviews {
if let scrollView = view as? UIScrollView { if let scrollView = view as? UIScrollView {
@ -18,5 +18,5 @@ extension UIPageViewController {
} }
return nil return nil
} }
} }

View File

@ -9,39 +9,39 @@
import UIKit import UIKit
extension UIStoryboard { extension UIStoryboard {
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0) static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0)
static var main: UIStoryboard { static var main: UIStoryboard {
return UIStoryboard(name: "Main", bundle: nil) return UIStoryboard(name: "Main", bundle: nil)
} }
static var add: UIStoryboard { static var add: UIStoryboard {
return UIStoryboard(name: "Add", bundle: nil) return UIStoryboard(name: "Add", bundle: nil)
} }
static var settings: UIStoryboard { static var settings: UIStoryboard {
return UIStoryboard(name: "Settings", bundle: nil) return UIStoryboard(name: "Settings", bundle: nil)
} }
static var inspector: UIStoryboard { static var inspector: UIStoryboard {
return UIStoryboard(name: "Inspector", bundle: nil) return UIStoryboard(name: "Inspector", bundle: nil)
} }
static var account: UIStoryboard { static var account: UIStoryboard {
return UIStoryboard(name: "Account", bundle: nil) return UIStoryboard(name: "Account", bundle: nil)
} }
func instantiateController<T>(ofType type: T.Type = T.self) -> T where T: UIViewController { func instantiateController<T>(ofType type: T.Type = T.self) -> T where T: UIViewController {
let storyboardId = String(describing: type) let storyboardId = String(describing: type)
guard let viewController = instantiateViewController(withIdentifier: storyboardId) as? T else { guard let viewController = instantiateViewController(withIdentifier: storyboardId) as? T else {
print("Unable to load view with Scene Identifier: \(storyboardId)") print("Unable to load view with Scene Identifier: \(storyboardId)")
fatalError() fatalError()
} }
return viewController return viewController
} }
} }

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
extension UITableView { extension UITableView {
/** /**
Selects a row and scrolls it to the middle if it is not visible Selects a row and scrolls it to the middle if it is not visible
*/ */
@ -20,7 +20,7 @@ extension UITableView {
indexPath.row < dataSource.tableView(self, numberOfRowsInSection: indexPath.section) else { indexPath.row < dataSource.tableView(self, numberOfRowsInSection: indexPath.section) else {
return return
} }
selectRow(at: indexPath, animated: animations.contains(.select), scrollPosition: .none) selectRow(at: indexPath, animated: animations.contains(.select), scrollPosition: .none)
if let visibleIndexPaths = indexPathsForRows(in: safeAreaLayoutGuide.layoutFrame) { if let visibleIndexPaths = indexPathsForRows(in: safeAreaLayoutGuide.layoutFrame) {
@ -29,12 +29,12 @@ extension UITableView {
} }
} }
} }
func cellCompletelyVisible(_ indexPath: IndexPath) -> Bool { func cellCompletelyVisible(_ indexPath: IndexPath) -> Bool {
let rect = rectForRow(at: indexPath) let rect = rectForRow(at: indexPath)
return safeAreaLayoutGuide.layoutFrame.contains(rect) return safeAreaLayoutGuide.layoutFrame.contains(rect)
} }
public func middleVisibleRow() -> IndexPath? { public func middleVisibleRow() -> IndexPath? {
if let visibleIndexPaths = indexPathsForRows(in: safeAreaLayoutGuide.layoutFrame), visibleIndexPaths.count > 2 { if let visibleIndexPaths = indexPathsForRows(in: safeAreaLayoutGuide.layoutFrame), visibleIndexPaths.count > 2 {
return visibleIndexPaths[visibleIndexPaths.count / 2] return visibleIndexPaths[visibleIndexPaths.count / 2]

View File

@ -11,7 +11,7 @@ import RSCore
import Account import Account
extension UIViewController { extension UIViewController {
func presentError(_ error: Error, dismiss: (() -> Void)? = nil) { func presentError(_ error: Error, dismiss: (() -> Void)? = nil) {
if let accountError = error as? AccountError, accountError.isCredentialsError { if let accountError = error as? AccountError, accountError.isCredentialsError {
presentAccountError(accountError, dismiss: dismiss) presentAccountError(accountError, dismiss: dismiss)
@ -41,7 +41,7 @@ extension UIViewController {
let localizedError = NSLocalizedString("This theme cannot be used because of data corruption in the Info.plist. %@.", comment: "Decoding key missing") let localizedError = NSLocalizedString("This theme cannot be used because of data corruption in the Info.plist. %@.", comment: "Decoding key missing")
informativeText = NSString.localizedStringWithFormat(localizedError as NSString, debugDescription) as String informativeText = NSString.localizedStringWithFormat(localizedError as NSString, debugDescription) as String
presentError(title: errorTitle, message: informativeText, dismiss: dismiss) presentError(title: errorTitle, message: informativeText, dismiss: dismiss)
default: default:
informativeText = error.localizedDescription informativeText = error.localizedDescription
presentError(title: errorTitle, message: informativeText, dismiss: dismiss) presentError(title: errorTitle, message: informativeText, dismiss: dismiss)
@ -55,35 +55,35 @@ extension UIViewController {
} }
private extension UIViewController { private extension UIViewController {
func presentAccountError(_ error: AccountError, dismiss: (() -> Void)? = nil) { func presentAccountError(_ error: AccountError, dismiss: (() -> Void)? = nil) {
let title = NSLocalizedString("Account Error", comment: "Account Error") let title = NSLocalizedString("Account Error", comment: "Account Error")
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert) let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)
if error.account?.type == .feedbin { if error.account?.type == .feedbin {
let credentialsTitle = NSLocalizedString("Update Credentials", comment: "Update Credentials") let credentialsTitle = NSLocalizedString("Update Credentials", comment: "Update Credentials")
let credentialsAction = UIAlertAction(title: credentialsTitle, style: .default) { [weak self] _ in let credentialsAction = UIAlertAction(title: credentialsTitle, style: .default) { [weak self] _ in
dismiss?() dismiss?()
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController
navController.modalPresentationStyle = .formSheet navController.modalPresentationStyle = .formSheet
let addViewController = navController.topViewController as! FeedbinAccountViewController let addViewController = navController.topViewController as! FeedbinAccountViewController
addViewController.account = error.account addViewController.account = error.account
self?.present(navController, animated: true) self?.present(navController, animated: true)
} }
alertController.addAction(credentialsAction) alertController.addAction(credentialsAction)
alertController.preferredAction = credentialsAction alertController.preferredAction = credentialsAction
} }
let dismissTitle = NSLocalizedString("OK", comment: "OK") let dismissTitle = NSLocalizedString("OK", comment: "OK")
let dismissAction = UIAlertAction(title: dismissTitle, style: .default) { _ in let dismissAction = UIAlertAction(title: dismissTitle, style: .default) { _ in
dismiss?() dismiss?()
} }
alertController.addAction(dismissAction) alertController.addAction(dismissAction)
self.present(alertController, animated: true, completion: nil) self.present(alertController, animated: true, completion: nil)
} }

View File

@ -9,7 +9,7 @@
import UIKit import UIKit
class VibrantButton: UIButton { class VibrantButton: UIButton {
@IBInspectable var backgroundHighlightColor: UIColor = AppAssets.secondaryAccentColor @IBInspectable var backgroundHighlightColor: UIColor = AppAssets.secondaryAccentColor
override init(frame: CGRect) { override init(frame: CGRect) {
@ -20,7 +20,7 @@ class VibrantButton: UIButton {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
private func commonInit() { private func commonInit() {
setTitleColor(AppAssets.vibrantTextColor, for: .highlighted) setTitleColor(AppAssets.vibrantTextColor, for: .highlighted)
let disabledColor = AppAssets.secondaryAccentColor.withAlphaComponent(0.5) let disabledColor = AppAssets.secondaryAccentColor.withAlphaComponent(0.5)
@ -47,5 +47,5 @@ class VibrantButton: UIButton {
isHighlighted = false isHighlighted = false
super.touchesCancelled(touches, with: event) super.touchesCancelled(touches, with: event)
} }
} }

View File

@ -9,17 +9,17 @@
import UIKit import UIKit
class VibrantLabel: UILabel { class VibrantLabel: UILabel {
override init(frame: CGRect) { override init(frame: CGRect) {
super.init(frame: frame) super.init(frame: frame)
commonInit() commonInit()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
private func commonInit() { private func commonInit() {
highlightedTextColor = AppAssets.vibrantTextColor highlightedTextColor = AppAssets.vibrantTextColor
} }

View File

@ -9,27 +9,27 @@
import UIKit import UIKit
class VibrantTableViewCell: UITableViewCell { class VibrantTableViewCell: UITableViewCell {
static let duration: TimeInterval = 0.6 static let duration: TimeInterval = 0.6
var labelColor: UIColor { var labelColor: UIColor {
return isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label return isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label
} }
var secondaryLabelColor: UIColor { var secondaryLabelColor: UIColor {
return isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.secondaryLabel return isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.secondaryLabel
} }
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) { override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
super.init(style: style, reuseIdentifier: reuseIdentifier) super.init(style: style, reuseIdentifier: reuseIdentifier)
commonInit() commonInit()
} }
required init?(coder: NSCoder) { required init?(coder: NSCoder) {
super.init(coder: coder) super.init(coder: coder)
commonInit() commonInit()
} }
private func commonInit() { private func commonInit() {
applyThemeProperties() applyThemeProperties()
} }
@ -43,7 +43,7 @@ class VibrantTableViewCell: UITableViewCell {
super.setSelected(selected, animated: animated) super.setSelected(selected, animated: animated)
updateVibrancy(animated: animated) updateVibrancy(animated: animated)
} }
/// Subclass overrides should call super /// Subclass overrides should call super
func applyThemeProperties() { func applyThemeProperties() {
let selectedBackgroundView = UIView(frame: .zero) let selectedBackgroundView = UIView(frame: .zero)
@ -56,7 +56,7 @@ class VibrantTableViewCell: UITableViewCell {
updateLabelVibrancy(textLabel, color: labelColor, animated: animated) updateLabelVibrancy(textLabel, color: labelColor, animated: animated)
updateLabelVibrancy(detailTextLabel, color: labelColor, animated: animated) updateLabelVibrancy(detailTextLabel, color: labelColor, animated: animated)
} }
func updateLabelVibrancy(_ label: UILabel?, color: UIColor, animated: Bool) { func updateLabelVibrancy(_ label: UILabel?, color: UIColor, animated: Bool) {
guard let label = label else { return } guard let label = label else { return }
if animated { if animated {
@ -67,33 +67,33 @@ class VibrantTableViewCell: UITableViewCell {
label.textColor = color label.textColor = color
} }
} }
} }
class VibrantBasicTableViewCell: VibrantTableViewCell { class VibrantBasicTableViewCell: VibrantTableViewCell {
@IBOutlet private var label: UILabel! @IBOutlet private var label: UILabel!
@IBOutlet private var detail: UILabel! @IBOutlet private var detail: UILabel!
@IBOutlet private var icon: UIImageView! @IBOutlet private var icon: UIImageView!
@IBInspectable var imageNormal: UIImage? @IBInspectable var imageNormal: UIImage?
@IBInspectable var imageSelected: UIImage? @IBInspectable var imageSelected: UIImage?
var iconTint: UIColor { var iconTint: UIColor {
return isHighlighted || isSelected ? labelColor : AppAssets.primaryAccentColor return isHighlighted || isSelected ? labelColor : AppAssets.primaryAccentColor
} }
var iconImage: UIImage? { var iconImage: UIImage? {
return isHighlighted || isSelected ? imageSelected : imageNormal return isHighlighted || isSelected ? imageSelected : imageNormal
} }
override func updateVibrancy(animated: Bool) { override func updateVibrancy(animated: Bool) {
super.updateVibrancy(animated: animated) super.updateVibrancy(animated: animated)
updateIconVibrancy(icon, color: iconTint, image: iconImage, animated: animated) updateIconVibrancy(icon, color: iconTint, image: iconImage, animated: animated)
updateLabelVibrancy(label, color: labelColor, animated: animated) updateLabelVibrancy(label, color: labelColor, animated: animated)
updateLabelVibrancy(detail, color: secondaryLabelColor, animated: animated) updateLabelVibrancy(detail, color: secondaryLabelColor, animated: animated)
} }
private func updateIconVibrancy(_ icon: UIImageView?, color: UIColor, image: UIImage?, animated: Bool) { private func updateIconVibrancy(_ icon: UIImageView?, color: UIColor, image: UIImage?, animated: Bool) {
guard let icon = icon else { return } guard let icon = icon else { return }
if animated { if animated {
@ -106,5 +106,5 @@ class VibrantBasicTableViewCell: VibrantTableViewCell {
icon.image = image icon.image = image
} }
} }
} }