Fix lint issues.
This commit is contained in:
parent
40ada2ba5a
commit
bbef99f2d3
@ -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
|
||||||
|
@ -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\nDon’t 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\nDon’t 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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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\nDon’t 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\nDon’t 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)
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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)
|
||||||
|
@ -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 {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()")
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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 ?? ""
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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]
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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. It’s easiest.
|
// Use this call. It’s easiest.
|
||||||
@ -60,5 +60,5 @@ final class SingleLineUILabelSizer {
|
|||||||
static func emptyCache() {
|
static func emptyCache() {
|
||||||
sizers = [UIFont: SingleLineUILabelSizer]()
|
sizers = [UIFont: SingleLineUILabelSizer]()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
@ -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 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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!
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -37,5 +37,5 @@ class TitleActivityItemSource: NSObject, UIActivityItemSource {
|
|||||||
return NSNull()
|
return NSNull()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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
|
||||||
|
@ -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])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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]
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user