Fix lint issues.
This commit is contained in:
parent
40ada2ba5a
commit
bbef99f2d3
@ -12,7 +12,7 @@ import Account
|
||||
|
||||
enum CloudKitAccountViewControllerError: LocalizedError {
|
||||
case iCloudDriveMissing
|
||||
|
||||
|
||||
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.")
|
||||
}
|
||||
@ -22,14 +22,14 @@ class CloudKitAccountViewController: UITableViewController {
|
||||
|
||||
weak var delegate: AddAccountDismissDelegate?
|
||||
@IBOutlet weak var footerLabel: UILabel!
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupFooter()
|
||||
|
||||
|
||||
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
}
|
||||
|
||||
|
||||
private func setupFooter() {
|
||||
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)
|
||||
delegate?.dismiss()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func add(_ sender: Any) {
|
||||
guard FileManager.default.ubiquityIdentityToken != nil else {
|
||||
presentError(CloudKitAccountViewControllerError.iCloudDriveMissing)
|
||||
return
|
||||
}
|
||||
|
||||
let _ = AccountManager.shared.createAccount(type: .cloudKit)
|
||||
|
||||
_ = AccountManager.shared.createAccount(type: .cloudKit)
|
||||
dismiss(animated: true, completion: nil)
|
||||
delegate?.dismiss()
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
if section == 0 {
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
|
||||
|
@ -32,7 +32,7 @@ class FeedbinAccountViewController: UITableViewController {
|
||||
activityIndicator.isHidden = true
|
||||
emailTextField.delegate = self
|
||||
passwordTextField.delegate = self
|
||||
|
||||
|
||||
if let account = account, let credentials = try? account.retrieveCredentials(type: .basic) {
|
||||
actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal)
|
||||
actionButton.isEnabled = true
|
||||
@ -47,7 +47,7 @@ class FeedbinAccountViewController: UITableViewController {
|
||||
|
||||
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
}
|
||||
|
||||
|
||||
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")
|
||||
}
|
||||
@ -55,7 +55,7 @@ class FeedbinAccountViewController: UITableViewController {
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
if section == 0 {
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
|
||||
@ -69,7 +69,7 @@ class FeedbinAccountViewController: UITableViewController {
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func showHidePassword(_ sender: Any) {
|
||||
if passwordTextField.isSecureTextEntry {
|
||||
passwordTextField.isSecureTextEntry = false
|
||||
@ -79,21 +79,21 @@ class FeedbinAccountViewController: UITableViewController {
|
||||
showHideButton.setTitle("Show", for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func action(_ sender: Any) {
|
||||
guard let email = emailTextField.text, let password = passwordTextField.text else {
|
||||
showError(NSLocalizedString("Username & password required.", comment: "Credentials Error"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// When you fill in the email address via auto-complete it adds extra whitespace
|
||||
let trimmedEmail = email.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
|
||||
guard account != nil || !AccountManager.shared.duplicateServiceAccount(type: .feedbin, username: trimmedEmail) else {
|
||||
showError(NSLocalizedString("There is already a Feedbin account with that username created.", comment: "Duplicate Error"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
resignFirstResponder()
|
||||
toggleActivityIndicatorAnimation(visible: true)
|
||||
setNavigationEnabled(to: false)
|
||||
@ -102,22 +102,22 @@ class FeedbinAccountViewController: UITableViewController {
|
||||
Account.validateCredentials(type: .feedbin, credentials: credentials) { result in
|
||||
self.toggleActivityIndicatorAnimation(visible: false)
|
||||
self.setNavigationEnabled(to: true)
|
||||
|
||||
|
||||
switch result {
|
||||
case .success(let credentials):
|
||||
if let credentials = credentials {
|
||||
if self.account == nil {
|
||||
self.account = AccountManager.shared.createAccount(type: .feedbin)
|
||||
}
|
||||
|
||||
|
||||
do {
|
||||
|
||||
|
||||
do {
|
||||
try self.account?.removeCredentials(type: .basic)
|
||||
} catch {}
|
||||
try self.account?.storeCredentials(credentials)
|
||||
|
||||
self.account?.refreshAll() { result in
|
||||
|
||||
self.account?.refreshAll { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
@ -125,7 +125,7 @@ class FeedbinAccountViewController: UITableViewController {
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
self.delegate?.dismiss()
|
||||
} catch {
|
||||
@ -137,32 +137,31 @@ class FeedbinAccountViewController: UITableViewController {
|
||||
case .failure:
|
||||
self.showError(NSLocalizedString("Network error. Try again later.", comment: "Credentials Error"))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func signUpWithProvider(_ sender: Any) {
|
||||
let url = URL(string: "https://feedbin.com/signup")!
|
||||
let safari = SFSafariViewController(url: url)
|
||||
safari.modalPresentationStyle = .currentContext
|
||||
self.present(safari, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
||||
|
||||
@objc func textDidChange(_ note: Notification) {
|
||||
actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false)
|
||||
actionButton.isEnabled = !(emailTextField.text?.isEmpty ?? false) && !(passwordTextField.text?.isEmpty ?? false)
|
||||
}
|
||||
|
||||
|
||||
private func showError(_ message: String) {
|
||||
presentError(title: NSLocalizedString("Error", comment: "Credentials Error"), message: message)
|
||||
}
|
||||
|
||||
private func setNavigationEnabled(to value:Bool){
|
||||
|
||||
private func setNavigationEnabled(to value: Bool) {
|
||||
cancelBarButtonItem.isEnabled = value
|
||||
actionButton.isEnabled = value
|
||||
}
|
||||
|
||||
private func toggleActivityIndicatorAnimation(visible value: Bool){
|
||||
|
||||
private func toggleActivityIndicatorAnimation(visible value: Bool) {
|
||||
activityIndicator.isHidden = !value
|
||||
if value {
|
||||
activityIndicator.startAnimating()
|
||||
@ -170,11 +169,11 @@ class FeedbinAccountViewController: UITableViewController {
|
||||
activityIndicator.stopAnimating()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension FeedbinAccountViewController: UITextFieldDelegate {
|
||||
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
if textField == emailTextField {
|
||||
passwordTextField.becomeFirstResponder()
|
||||
@ -184,5 +183,5 @@ extension FeedbinAccountViewController: UITextFieldDelegate {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ class LocalAccountViewController: UITableViewController {
|
||||
|
||||
@IBOutlet weak var nameTextField: UITextField!
|
||||
@IBOutlet weak var footerLabel: UILabel!
|
||||
|
||||
|
||||
weak var delegate: AddAccountDismissDelegate?
|
||||
|
||||
override func viewDidLoad() {
|
||||
@ -23,7 +23,7 @@ class LocalAccountViewController: UITableViewController {
|
||||
nameTextField.delegate = self
|
||||
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
}
|
||||
|
||||
|
||||
private func setupFooter() {
|
||||
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) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func add(_ sender: Any) {
|
||||
let account = AccountManager.shared.createAccount(type: .onMyMac)
|
||||
account.name = nameTextField.text
|
||||
dismiss(animated: true, completion: nil)
|
||||
delegate?.dismiss()
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
if section == 0 {
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
|
||||
@ -52,14 +52,14 @@ class LocalAccountViewController: UITableViewController {
|
||||
return super.tableView(tableView, viewForHeaderInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension LocalAccountViewController: UITextFieldDelegate {
|
||||
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -46,7 +46,7 @@ class NewsBlurAccountViewController: UITableViewController {
|
||||
|
||||
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
}
|
||||
|
||||
|
||||
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")
|
||||
}
|
||||
@ -93,7 +93,7 @@ class NewsBlurAccountViewController: UITableViewController {
|
||||
showError(NSLocalizedString("There is already a NewsBlur account with that username created.", comment: "Duplicate Error"))
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let password = passwordTextField.text ?? ""
|
||||
|
||||
startAnimatingActivityIndicator()
|
||||
@ -122,7 +122,7 @@ class NewsBlurAccountViewController: UITableViewController {
|
||||
try self.account?.storeCredentials(basicCredentials)
|
||||
try self.account?.storeCredentials(sessionCredentials)
|
||||
|
||||
self.account?.refreshAll() { result in
|
||||
self.account?.refreshAll { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
@ -145,7 +145,7 @@ class NewsBlurAccountViewController: UITableViewController {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func signUpWithProvider(_ sender: Any) {
|
||||
let url = URL(string: "https://newsblur.com")!
|
||||
let safari = SFSafariViewController(url: url)
|
||||
|
@ -27,7 +27,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
weak var account: Account?
|
||||
var accountType: AccountType?
|
||||
weak var delegate: AddAccountDismissDelegate?
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
setupFooter()
|
||||
@ -35,7 +35,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
activityIndicator.isHidden = true
|
||||
usernameTextField.delegate = self
|
||||
passwordTextField.delegate = self
|
||||
|
||||
|
||||
if let unwrappedAccount = account,
|
||||
let credentials = try? retrieveCredentialsForAccount(for: unwrappedAccount) {
|
||||
actionButton.setTitle(NSLocalizedString("Update Credentials", comment: "Update Credentials"), for: .normal)
|
||||
@ -45,7 +45,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
} else {
|
||||
actionButton.setTitle(NSLocalizedString("Add Account", comment: "Add Account"), for: .normal)
|
||||
}
|
||||
|
||||
|
||||
if let unwrappedAccountType = accountType {
|
||||
switch unwrappedAccountType {
|
||||
case .freshRSS:
|
||||
@ -61,14 +61,14 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
title = ""
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: usernameTextField)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: passwordTextField)
|
||||
|
||||
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
private func setupFooter() {
|
||||
switch accountType {
|
||||
case .bazQux:
|
||||
@ -87,11 +87,11 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
if section == 0 {
|
||||
let headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as! ImageHeaderView
|
||||
@ -101,7 +101,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
return super.tableView(tableView, viewForHeaderInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
switch section {
|
||||
case 0:
|
||||
@ -115,8 +115,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
dismiss(animated: true, completion: nil)
|
||||
}
|
||||
@ -130,19 +129,19 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
showHideButton.setTitle("Show", for: .normal)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func action(_ sender: Any) {
|
||||
guard validateDataEntry(), let type = accountType else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let username = usernameTextField.text!
|
||||
let password = passwordTextField.text!
|
||||
let url = apiURL()!
|
||||
|
||||
|
||||
// When you fill in the email address via auto-complete it adds extra whitespace
|
||||
let trimmedUsername = username.trimmingCharacters(in: .whitespaces)
|
||||
|
||||
|
||||
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"))
|
||||
return
|
||||
@ -167,15 +166,15 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
|
||||
do {
|
||||
self.account?.endpointURL = url
|
||||
|
||||
|
||||
try? self.account?.removeCredentials(type: .readerBasic)
|
||||
try? self.account?.removeCredentials(type: .readerAPIKey)
|
||||
try self.account?.storeCredentials(credentials)
|
||||
try self.account?.storeCredentials(validatedCredentials)
|
||||
|
||||
self.dismiss(animated: true, completion: nil)
|
||||
|
||||
self.account?.refreshAll() { result in
|
||||
|
||||
self.account?.refreshAll { result in
|
||||
switch result {
|
||||
case .success:
|
||||
break
|
||||
@ -183,7 +182,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
self.showError(NSLocalizedString(error.localizedDescription, comment: "Account Refresh Error"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
self.delegate?.dismiss()
|
||||
} catch {
|
||||
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? {
|
||||
switch accountType {
|
||||
case .bazQux, .inoreader, .theOldReader, .freshRSS:
|
||||
@ -206,7 +205,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func headerViewImage() -> UIImage? {
|
||||
if let accountType = accountType {
|
||||
switch accountType {
|
||||
@ -224,7 +223,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
private func validateDataEntry() -> Bool {
|
||||
switch accountType {
|
||||
case .freshRSS:
|
||||
@ -244,7 +243,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@IBAction func signUpWithProvider(_ sender: Any) {
|
||||
var url: URL!
|
||||
switch accountType {
|
||||
@ -263,7 +262,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
safari.modalPresentationStyle = .currentContext
|
||||
self.present(safari, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
||||
private func apiURL() -> URL? {
|
||||
switch accountType {
|
||||
case .freshRSS:
|
||||
@ -278,9 +277,7 @@ class ReaderAPIAccountViewController: UITableViewController {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@objc func textDidChange(_ note: Notification) {
|
||||
actionButton.isEnabled = !(usernameTextField.text?.isEmpty ?? false)
|
||||
}
|
||||
|
@ -12,10 +12,10 @@ class AddComboTableViewCell: VibrantTableViewCell {
|
||||
|
||||
@IBOutlet weak var icon: UIImageView!
|
||||
@IBOutlet weak var label: UILabel!
|
||||
|
||||
|
||||
override func updateVibrancy(animated: Bool) {
|
||||
super.updateVibrancy(animated: animated)
|
||||
|
||||
|
||||
let iconTintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : AppAssets.secondaryAccentColor
|
||||
if animated {
|
||||
UIView.animate(withDuration: Self.duration) {
|
||||
@ -26,5 +26,5 @@ class AddComboTableViewCell: VibrantTableViewCell {
|
||||
}
|
||||
updateLabelVibrancy(label, color: labelColor, animated: animated)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -15,15 +15,15 @@ protocol AddFeedFolderViewControllerDelegate {
|
||||
}
|
||||
|
||||
class AddFeedFolderViewController: UITableViewController {
|
||||
|
||||
|
||||
var delegate: AddFeedFolderViewControllerDelegate?
|
||||
var initialContainer: Container?
|
||||
|
||||
|
||||
var containers = [Container]()
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
let sortedActiveAccounts = AccountManager.shared.sortedActiveAccounts
|
||||
|
||||
for account in sortedActiveAccounts {
|
||||
@ -53,15 +53,15 @@ class AddFeedFolderViewController: UITableViewController {
|
||||
return tableView.dequeueReusableCell(withIdentifier: "FolderCell", for: indexPath) as! AddComboTableViewCell
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
if let smallIconProvider = container as? SmallIconProvider {
|
||||
cell.icon?.image = smallIconProvider.smallIcon?.image
|
||||
}
|
||||
|
||||
|
||||
if let displayNameProvider = container as? DisplayNameProvider {
|
||||
cell.label?.text = displayNameProvider.nameForDisplay
|
||||
}
|
||||
|
||||
|
||||
if let compContainer = initialContainer, container === compContainer {
|
||||
cell.accessoryType = .checkmark
|
||||
} else {
|
||||
@ -73,7 +73,7 @@ class AddFeedFolderViewController: UITableViewController {
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
let container = containers[indexPath.row]
|
||||
|
||||
|
||||
if let account = container as? Account, account.behaviors.contains(.disallowFeedInRootFolder) {
|
||||
tableView.selectRow(at: nil, animated: false, scrollPosition: .none)
|
||||
} else {
|
||||
@ -83,19 +83,19 @@ class AddFeedFolderViewController: UITableViewController {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
dismiss()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private extension AddFeedFolderViewController {
|
||||
|
||||
|
||||
func dismiss() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,14 +9,14 @@
|
||||
import UIKit
|
||||
|
||||
class AddFeedSelectFolderTableViewCell: VibrantTableViewCell {
|
||||
|
||||
|
||||
@IBOutlet weak var folderLabel: UILabel!
|
||||
@IBOutlet weak var detailLabel: UILabel!
|
||||
|
||||
|
||||
override func updateVibrancy(animated: Bool) {
|
||||
super.updateVibrancy(animated: animated)
|
||||
updateLabelVibrancy(folderLabel, 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 urlTextFieldToSuperViewConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var nameTextField: UITextField!
|
||||
|
||||
|
||||
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0)
|
||||
|
||||
|
||||
private var folderLabel = ""
|
||||
private var userCancelled = false
|
||||
|
||||
@ -29,88 +29,88 @@ class AddFeedViewController: UITableViewController {
|
||||
var initialFeedName: String?
|
||||
|
||||
var container: Container?
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
activityIndicator.isHidden = true
|
||||
activityIndicator.color = .label
|
||||
|
||||
|
||||
if initialFeed == nil, let urlString = UIPasteboard.general.string {
|
||||
if urlString.mayBeURL {
|
||||
initialFeed = urlString.normalizedURL
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
urlTextField.autocorrectionType = .no
|
||||
urlTextField.autocapitalizationType = .none
|
||||
urlTextField.text = initialFeed
|
||||
urlTextField.delegate = self
|
||||
|
||||
|
||||
if initialFeed != nil {
|
||||
addButton.isEnabled = true
|
||||
}
|
||||
|
||||
|
||||
nameTextField.text = initialFeedName
|
||||
nameTextField.delegate = self
|
||||
|
||||
|
||||
if let defaultContainer = AddFeedDefaultContainer.defaultContainer {
|
||||
container = defaultContainer
|
||||
} else {
|
||||
addButton.isEnabled = false
|
||||
}
|
||||
|
||||
|
||||
updateFolderLabel()
|
||||
|
||||
|
||||
tableView.register(UINib(nibName: "AddFeedSelectFolderTableViewCell", bundle: nil), forCellReuseIdentifier: "AddFeedSelectFolderTableViewCell")
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: urlTextField)
|
||||
|
||||
|
||||
if initialFeed == nil {
|
||||
urlTextField.becomeFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
userCancelled = true
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func add(_ sender: Any) {
|
||||
|
||||
let urlString = urlTextField.text ?? ""
|
||||
let normalizedURLString = urlString.normalizedURL
|
||||
|
||||
|
||||
guard !normalizedURLString.isEmpty, let url = URL(string: normalizedURLString) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let container = container else { return }
|
||||
|
||||
|
||||
var account: Account?
|
||||
if let containerAccount = container as? Account {
|
||||
account = containerAccount
|
||||
} else if let containerFolder = container as? Folder, let containerAccount = containerFolder.account {
|
||||
account = containerAccount
|
||||
}
|
||||
|
||||
|
||||
if account!.hasFeed(withURL: url.absoluteString) {
|
||||
presentError(AccountError.createErrorAlreadySubscribed)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
addButton.isEnabled = false
|
||||
activityIndicator.isHidden = false
|
||||
activityIndicator.startAnimating()
|
||||
|
||||
|
||||
let feedName = (nameTextField.text?.isEmpty ?? true) ? nil : nameTextField.text
|
||||
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
|
||||
|
||||
account!.createFeed(url: url.absoluteString, name: feedName, container: container, validateFeed: true) { result in
|
||||
|
||||
BatchUpdate.shared.end()
|
||||
|
||||
|
||||
switch result {
|
||||
case .success(let feed):
|
||||
self.dismiss(animated: true)
|
||||
@ -125,11 +125,11 @@ class AddFeedViewController: UITableViewController {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@objc func textDidChange(_ note: Notification) {
|
||||
updateUI()
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
if indexPath.row == 2 {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "AddFeedSelectFolderTableViewCell", for: indexPath) as? AddFeedSelectFolderTableViewCell
|
||||
@ -139,7 +139,7 @@ class AddFeedViewController: UITableViewController {
|
||||
return super.tableView(tableView, cellForRowAt: indexPath)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if indexPath.row == 2 {
|
||||
let navController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddFeedFolderNavViewController") as! UINavigationController
|
||||
@ -150,7 +150,7 @@ class AddFeedViewController: UITableViewController {
|
||||
present(navController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: AddFeedFolderViewControllerDelegate
|
||||
@ -166,22 +166,22 @@ extension AddFeedViewController: AddFeedFolderViewControllerDelegate {
|
||||
// MARK: UITextFieldDelegate
|
||||
|
||||
extension AddFeedViewController: UITextFieldDelegate {
|
||||
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension AddFeedViewController {
|
||||
|
||||
|
||||
func updateUI() {
|
||||
addButton.isEnabled = (urlTextField.text?.mayBeURL ?? false)
|
||||
}
|
||||
|
||||
|
||||
func updateFolderLabel() {
|
||||
if let containerName = (container as? DisplayNameProvider)?.nameForDisplay {
|
||||
if container is Folder {
|
||||
|
@ -16,13 +16,13 @@ class AddFolderViewController: UITableViewController {
|
||||
@IBOutlet private weak var nameTextField: UITextField!
|
||||
@IBOutlet private weak var accountLabel: UILabel!
|
||||
@IBOutlet private weak var accountPickerView: UIPickerView!
|
||||
|
||||
|
||||
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0)
|
||||
|
||||
|
||||
private var shouldDisplayPicker: Bool {
|
||||
return accounts.count > 1
|
||||
}
|
||||
|
||||
|
||||
private var accounts: [Account]! {
|
||||
didSet {
|
||||
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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
accounts = AccountManager.shared
|
||||
.sortedActiveAccounts
|
||||
.filter { !$0.behaviors.contains(.disallowFolderManagement) }
|
||||
|
||||
|
||||
nameTextField.delegate = self
|
||||
|
||||
|
||||
if shouldDisplayPicker {
|
||||
accountPickerView.dataSource = self
|
||||
accountPickerView.delegate = self
|
||||
|
||||
|
||||
if let index = accounts.firstIndex(of: selectedAccount) {
|
||||
accountPickerView.selectRow(index, inComponent: 0, animated: false)
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
accountPickerView.isHidden = true
|
||||
}
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: nameTextField)
|
||||
|
||||
|
||||
nameTextField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
|
||||
private func didSelect(_ account: Account) {
|
||||
AppDefaults.shared.addFolderAccountID = account.accountID
|
||||
selectedAccount = account
|
||||
@ -74,7 +74,7 @@ class AddFolderViewController: UITableViewController {
|
||||
@IBAction func cancel(_ sender: Any) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func add(_ sender: Any) {
|
||||
guard let folderName = nameTextField.text else {
|
||||
return
|
||||
@ -93,42 +93,42 @@ class AddFolderViewController: UITableViewController {
|
||||
@objc func textDidChange(_ note: Notification) {
|
||||
addButton.isEnabled = !(nameTextField.text?.isEmpty ?? false)
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: section)
|
||||
if section == 1 && !shouldDisplayPicker {
|
||||
return defaultNumberOfRows - 1
|
||||
}
|
||||
|
||||
return defaultNumberOfRows
|
||||
|
||||
return defaultNumberOfRows
|
||||
}
|
||||
}
|
||||
|
||||
extension AddFolderViewController: UIPickerViewDataSource, UIPickerViewDelegate {
|
||||
|
||||
func numberOfComponents(in pickerView: UIPickerView) ->Int {
|
||||
|
||||
func numberOfComponents(in pickerView: UIPickerView) -> Int {
|
||||
return 1
|
||||
}
|
||||
|
||||
|
||||
func pickerView(_ pickerView: UIPickerView, numberOfRowsInComponent component: Int) -> Int {
|
||||
return accounts.count
|
||||
}
|
||||
|
||||
|
||||
func pickerView(_ pickerView: UIPickerView, titleForRow row: Int, forComponent component: Int) -> String? {
|
||||
return (accounts[row] as DisplayNameProvider).nameForDisplay
|
||||
}
|
||||
|
||||
|
||||
func pickerView(_ pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int) {
|
||||
didSelect(accounts[row])
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension AddFolderViewController: UITextFieldDelegate {
|
||||
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -12,10 +12,10 @@ class SelectComboTableViewCell: VibrantTableViewCell {
|
||||
|
||||
@IBOutlet weak var icon: UIImageView!
|
||||
@IBOutlet weak var label: UILabel!
|
||||
|
||||
|
||||
override func updateVibrancy(animated: Bool) {
|
||||
super.updateVibrancy(animated: animated)
|
||||
|
||||
|
||||
let iconTintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label
|
||||
if animated {
|
||||
UIView.animate(withDuration: Self.duration) {
|
||||
@ -24,8 +24,8 @@ class SelectComboTableViewCell: VibrantTableViewCell {
|
||||
} else {
|
||||
self.icon.tintColor = iconTintColor
|
||||
}
|
||||
|
||||
|
||||
updateLabelVibrancy(label, color: labelColor, animated: animated)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import RSCore
|
||||
import Account
|
||||
|
||||
struct AppAssets {
|
||||
|
||||
|
||||
static var accountBazQuxImage: UIImage = {
|
||||
return UIImage(named: "accountBazQux")!
|
||||
}()
|
||||
@ -90,43 +90,43 @@ struct AppAssets {
|
||||
static var circleClosedImage: UIImage = {
|
||||
return UIImage(systemName: "largecircle.fill.circle")!
|
||||
}()
|
||||
|
||||
|
||||
static var circleOpenImage: UIImage = {
|
||||
return UIImage(systemName: "circle")!
|
||||
}()
|
||||
|
||||
|
||||
static var disclosureImage: UIImage = {
|
||||
return UIImage(named: "disclosure")!
|
||||
}()
|
||||
|
||||
|
||||
static var copyImage: UIImage = {
|
||||
return UIImage(systemName: "doc.on.doc")!
|
||||
}()
|
||||
|
||||
|
||||
static var deactivateImage: UIImage = {
|
||||
UIImage(systemName: "minus.circle")!
|
||||
}()
|
||||
|
||||
|
||||
static var editImage: UIImage = {
|
||||
UIImage(systemName: "square.and.pencil")!
|
||||
}()
|
||||
|
||||
|
||||
static var faviconTemplateImage: RSImage = {
|
||||
return RSImage(named: "faviconTemplateImage")!
|
||||
}()
|
||||
|
||||
|
||||
static var filterInactiveImage: UIImage = {
|
||||
UIImage(systemName: "line.horizontal.3.decrease.circle")!
|
||||
}()
|
||||
|
||||
|
||||
static var filterActiveImage: UIImage = {
|
||||
UIImage(systemName: "line.horizontal.3.decrease.circle.fill")!
|
||||
}()
|
||||
|
||||
|
||||
static var folderOutlinePlus: UIImage = {
|
||||
UIImage(systemName: "folder.badge.plus")!
|
||||
}()
|
||||
|
||||
|
||||
static var fullScreenBackgroundColor: UIColor = {
|
||||
return UIColor(named: "fullScreenBackgroundColor")!
|
||||
}()
|
||||
@ -134,19 +134,19 @@ struct AppAssets {
|
||||
static var infoImage: UIImage = {
|
||||
UIImage(systemName: "info.circle")!
|
||||
}()
|
||||
|
||||
|
||||
static var markAllAsReadImage: UIImage = {
|
||||
return UIImage(named: "markAllAsRead")!
|
||||
}()
|
||||
|
||||
|
||||
static var markBelowAsReadImage: UIImage = {
|
||||
return UIImage(systemName: "arrowtriangle.down.circle")!
|
||||
}()
|
||||
|
||||
|
||||
static var markAboveAsReadImage: UIImage = {
|
||||
return UIImage(systemName: "arrowtriangle.up.circle")!
|
||||
}()
|
||||
|
||||
|
||||
static var folderImage: IconImage = {
|
||||
return IconImage(UIImage(systemName: "folder.fill")!, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor)
|
||||
}()
|
||||
@ -154,67 +154,67 @@ struct AppAssets {
|
||||
static var moreImage: UIImage = {
|
||||
return UIImage(systemName: "ellipsis.circle")!
|
||||
}()
|
||||
|
||||
|
||||
static var nextArticleImage: UIImage = {
|
||||
return UIImage(systemName: "chevron.down")!
|
||||
}()
|
||||
|
||||
|
||||
static var nextUnreadArticleImage: UIImage = {
|
||||
return UIImage(systemName: "chevron.down.circle")!
|
||||
}()
|
||||
|
||||
|
||||
static var plus: UIImage = {
|
||||
UIImage(systemName: "plus")!
|
||||
}()
|
||||
|
||||
|
||||
static var prevArticleImage: UIImage = {
|
||||
return UIImage(systemName: "chevron.up")!
|
||||
}()
|
||||
|
||||
|
||||
static var openInSidebarImage: UIImage = {
|
||||
return UIImage(systemName: "arrow.turn.down.left")!
|
||||
}()
|
||||
|
||||
|
||||
static var primaryAccentColor: UIColor {
|
||||
return UIColor(named: "primaryAccentColor")!
|
||||
}
|
||||
|
||||
|
||||
static var safariImage: UIImage = {
|
||||
return UIImage(systemName: "safari")!
|
||||
}()
|
||||
|
||||
|
||||
static var searchFeedImage: IconImage = {
|
||||
return IconImage(UIImage(systemName: "magnifyingglass")!, isSymbol: true)
|
||||
}()
|
||||
|
||||
|
||||
static var secondaryAccentColor: UIColor {
|
||||
return UIColor(named: "secondaryAccentColor")!
|
||||
}
|
||||
|
||||
|
||||
static var sectionHeaderColor: UIColor = {
|
||||
return UIColor(named: "sectionHeaderColor")!
|
||||
}()
|
||||
|
||||
|
||||
static var shareImage: UIImage = {
|
||||
return UIImage(systemName: "square.and.arrow.up")!
|
||||
}()
|
||||
|
||||
|
||||
static var smartFeedImage: UIImage = {
|
||||
return UIImage(systemName: "gear")!
|
||||
}()
|
||||
|
||||
|
||||
static var starColor: UIColor = {
|
||||
return UIColor(named: "starColor")!
|
||||
}()
|
||||
|
||||
|
||||
static var starClosedImage: UIImage = {
|
||||
return UIImage(systemName: "star.fill")!
|
||||
}()
|
||||
|
||||
|
||||
static var starOpenImage: UIImage = {
|
||||
return UIImage(systemName: "star")!
|
||||
}()
|
||||
|
||||
|
||||
static var starredFeedImage: IconImage {
|
||||
let image = UIImage(systemName: "star.fill")!
|
||||
return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppAssets.starColor.cgColor)
|
||||
@ -223,12 +223,12 @@ struct AppAssets {
|
||||
static var tickMarkColor: UIColor = {
|
||||
return UIColor(named: "tickMarkColor")!
|
||||
}()
|
||||
|
||||
|
||||
static var timelineStarImage: UIImage = {
|
||||
let image = UIImage(systemName: "star.fill")!
|
||||
return image.withTintColor(AppAssets.starColor, renderingMode: .alwaysOriginal)
|
||||
}()
|
||||
|
||||
|
||||
static var todayFeedImage: IconImage {
|
||||
let image = UIImage(systemName: "sun.max.fill")!
|
||||
return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: UIColor.systemOrange.cgColor)
|
||||
@ -237,12 +237,12 @@ struct AppAssets {
|
||||
static var trashImage: UIImage = {
|
||||
return UIImage(systemName: "trash")!
|
||||
}()
|
||||
|
||||
|
||||
static var unreadFeedImage: IconImage {
|
||||
let image = UIImage(systemName: "largecircle.fill.circle")!
|
||||
return IconImage(image, isSymbol: true, isBackgroundSuppressed: true, preferredColor: AppAssets.secondaryAccentColor.cgColor)
|
||||
}
|
||||
|
||||
|
||||
static var vibrantTextColor: UIColor = {
|
||||
return UIColor(named: "vibrantTextColor")!
|
||||
}()
|
||||
@ -251,7 +251,6 @@ struct AppAssets {
|
||||
return UIColor(named: "controlBackgroundColor")!
|
||||
}()
|
||||
|
||||
|
||||
static func image(for accountType: AccountType) -> UIImage? {
|
||||
switch accountType {
|
||||
case .onMyMac:
|
||||
@ -278,5 +277,5 @@ struct AppAssets {
|
||||
return AppAssets.accountTheOldReaderImage
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -23,22 +23,22 @@ enum UserInterfaceColorPalette: Int, CustomStringConvertible, CaseIterable {
|
||||
return NSLocalizedString("Dark", comment: "Dark")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
final class AppDefaults {
|
||||
|
||||
static let defaultThemeName = "Default"
|
||||
|
||||
|
||||
static let shared = AppDefaults()
|
||||
private init() {}
|
||||
|
||||
|
||||
static var store: UserDefaults = {
|
||||
let appIdentifierPrefix = Bundle.main.object(forInfoDictionaryKey: "AppIdentifierPrefix") as! String
|
||||
let suiteName = "\(appIdentifierPrefix)group.\(Bundle.main.bundleIdentifier!)"
|
||||
return UserDefaults.init(suiteName: suiteName)!
|
||||
}()
|
||||
|
||||
|
||||
struct Key {
|
||||
static let userInterfaceColorPalette = "userInterfaceColorPalette"
|
||||
static let lastImageCacheFlushDate = "lastImageCacheFlushDate"
|
||||
@ -74,7 +74,7 @@ final class AppDefaults {
|
||||
firstRunDate = Date()
|
||||
return true
|
||||
}()
|
||||
|
||||
|
||||
static var userInterfaceColorPalette: UserInterfaceColorPalette {
|
||||
get {
|
||||
if let result = UserInterfaceColorPalette(rawValue: int(for: Key.userInterfaceColorPalette)) {
|
||||
@ -95,7 +95,7 @@ final class AppDefaults {
|
||||
AppDefaults.setString(for: Key.addFeedAccountID, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var addFeedFolderName: String? {
|
||||
get {
|
||||
return AppDefaults.string(for: Key.addFeedFolderName)
|
||||
@ -104,7 +104,7 @@ final class AppDefaults {
|
||||
AppDefaults.setString(for: Key.addFeedFolderName, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var addFolderAccountID: String? {
|
||||
get {
|
||||
return AppDefaults.string(for: Key.addFolderAccountID)
|
||||
@ -113,7 +113,7 @@ final class AppDefaults {
|
||||
AppDefaults.setString(for: Key.addFolderAccountID, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var useSystemBrowser: Bool {
|
||||
get {
|
||||
return UserDefaults.standard.bool(forKey: Key.useSystemBrowser)
|
||||
@ -122,7 +122,7 @@ final class AppDefaults {
|
||||
UserDefaults.standard.setValue(newValue, forKey: Key.useSystemBrowser)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var lastImageCacheFlushDate: Date? {
|
||||
get {
|
||||
return AppDefaults.date(for: Key.lastImageCacheFlushDate)
|
||||
@ -189,7 +189,7 @@ final class AppDefaults {
|
||||
AppDefaults.setBool(for: Key.confirmMarkAllAsRead, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var lastRefresh: Date? {
|
||||
get {
|
||||
return AppDefaults.date(for: Key.lastRefresh)
|
||||
@ -198,7 +198,7 @@ final class AppDefaults {
|
||||
AppDefaults.setDate(for: Key.lastRefresh, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var timelineNumberOfLines: Int {
|
||||
get {
|
||||
return AppDefaults.int(for: Key.timelineNumberOfLines)
|
||||
@ -207,7 +207,7 @@ final class AppDefaults {
|
||||
AppDefaults.setInt(for: Key.timelineNumberOfLines, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var timelineIconSize: IconSize {
|
||||
get {
|
||||
let rawValue = AppDefaults.store.integer(forKey: Key.timelineIconDimension)
|
||||
@ -217,7 +217,7 @@ final class AppDefaults {
|
||||
AppDefaults.store.set(newValue.rawValue, forKey: Key.timelineIconDimension)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var currentThemeName: String? {
|
||||
get {
|
||||
return AppDefaults.string(for: Key.currentThemeName)
|
||||
@ -237,7 +237,7 @@ final class AppDefaults {
|
||||
}
|
||||
|
||||
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.refreshClearsReadArticles: false,
|
||||
Key.timelineNumberOfLines: 2,
|
||||
@ -266,7 +266,7 @@ private extension AppDefaults {
|
||||
static func string(for key: String) -> String? {
|
||||
return UserDefaults.standard.string(forKey: key)
|
||||
}
|
||||
|
||||
|
||||
static func setString(for key: String, _ value: String?) {
|
||||
UserDefaults.standard.set(value, forKey: key)
|
||||
}
|
||||
@ -282,11 +282,11 @@ private extension AppDefaults {
|
||||
static func int(for key: String) -> Int {
|
||||
return AppDefaults.store.integer(forKey: key)
|
||||
}
|
||||
|
||||
|
||||
static func setInt(for key: String, _ x: Int) {
|
||||
AppDefaults.store.set(x, forKey: key)
|
||||
}
|
||||
|
||||
|
||||
static func date(for key: String) -> Date? {
|
||||
return AppDefaults.store.object(forKey: key) as? Date
|
||||
}
|
||||
@ -295,7 +295,7 @@ private extension AppDefaults {
|
||||
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)
|
||||
if rawInt == ComparisonResult.orderedAscending.rawValue {
|
||||
return .orderedAscending
|
||||
@ -306,10 +306,9 @@ private extension AppDefaults {
|
||||
static func setSortDirection(for key: String, _ value: ComparisonResult) {
|
||||
if value == .orderedAscending {
|
||||
setInt(for: key, ComparisonResult.orderedAscending.rawValue)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
setInt(for: key, ComparisonResult.orderedDescending.rawValue)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -19,14 +19,14 @@ var appDelegate: AppDelegate!
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate, UnreadCountProvider {
|
||||
|
||||
|
||||
private var bgTaskDispatchQueue = DispatchQueue.init(label: "BGTaskScheduler")
|
||||
|
||||
|
||||
private var waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
||||
private var syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
||||
|
||||
|
||||
var syncTimer: ArticleStatusSyncTimer?
|
||||
|
||||
|
||||
var shuttingDown = false {
|
||||
didSet {
|
||||
if shuttingDown {
|
||||
@ -35,7 +35,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private let logger = Logger(subsystem: Bundle.main.bundleIdentifier!, category: "Application")
|
||||
|
||||
var userNotificationManager: UserNotificationManager!
|
||||
@ -52,10 +52,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isSyncArticleStatusRunning = false
|
||||
var isWaitingForSyncTasks = false
|
||||
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
appDelegate = self
|
||||
@ -64,15 +64,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
let documentAccountsFolder = documentFolder.appendingPathComponent("Accounts").absoluteString
|
||||
let documentAccountsFolderPath = String(documentAccountsFolder.suffix(from: documentAccountsFolder.index(documentAccountsFolder.startIndex, offsetBy: 7)))
|
||||
AccountManager.shared = AccountManager(accountsFolder: documentAccountsFolderPath)
|
||||
|
||||
|
||||
let documentThemesFolder = documentFolder.appendingPathComponent("Themes").absoluteString
|
||||
let documentThemesFolderPath = String(documentThemesFolder.suffix(from: documentAccountsFolder.index(documentThemesFolder.startIndex, offsetBy: 7)))
|
||||
ArticleThemesManager.shared = ArticleThemesManager(folderPath: documentThemesFolderPath)
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountRefreshDidFinish(_:)), name: .AccountRefreshDidFinish, object: nil)
|
||||
}
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
AppDefaults.registerDefaults()
|
||||
|
||||
@ -80,22 +80,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
if isFirstRun {
|
||||
logger.info("Is first run.")
|
||||
}
|
||||
|
||||
|
||||
if isFirstRun && !AccountManager.shared.anyAccountHasAtLeastOneFeed() {
|
||||
let localAccount = AccountManager.shared.defaultAccount
|
||||
DefaultFeedsImporter.importDefaultFeeds(account: localAccount)
|
||||
}
|
||||
|
||||
|
||||
registerBackgroundTasks()
|
||||
CacheCleaner.purgeIfNecessary()
|
||||
initializeDownloaders()
|
||||
initializeHomeScreenQuickActions()
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
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 {
|
||||
DispatchQueue.main.async {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
@ -108,7 +108,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
|
||||
extensionContainersFile = ExtensionContainersFile()
|
||||
extensionFeedAddRequestFile = ExtensionFeedAddRequestFile()
|
||||
|
||||
|
||||
widgetDataEncoder = WidgetDataEncoder()
|
||||
|
||||
syncTimer = ArticleStatusSyncTimer()
|
||||
@ -116,12 +116,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
#if DEBUG
|
||||
syncTimer!.update()
|
||||
#endif
|
||||
|
||||
|
||||
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 {
|
||||
self.resumeDatabaseProcessingIfNecessary()
|
||||
AccountManager.shared.receiveRemoteNotification(userInfo: userInfo) {
|
||||
@ -130,7 +130,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
shuttingDown = true
|
||||
}
|
||||
@ -138,35 +138,35 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
func applicationDidEnterBackground(_ application: UIApplication) {
|
||||
IconImageCache.shared.emptyCache()
|
||||
}
|
||||
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
|
||||
@objc func unreadCountDidChange(_ note: Notification) {
|
||||
if note.object is AccountManager {
|
||||
unreadCount = AccountManager.shared.unreadCount
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func accountRefreshDidFinish(_ note: Notification) {
|
||||
AppDefaults.shared.lastRefresh = Date()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - API
|
||||
|
||||
func manualRefresh(errorHandler: @escaping (Error) -> ()) {
|
||||
UIApplication.shared.connectedScenes.compactMap( { $0.delegate as? SceneDelegate } ).forEach {
|
||||
|
||||
func manualRefresh(errorHandler: @escaping (Error) -> Void) {
|
||||
UIApplication.shared.connectedScenes.compactMap( { $0.delegate as? SceneDelegate }).forEach {
|
||||
$0.cleanUp(conditional: true)
|
||||
}
|
||||
AccountManager.shared.refreshAll(errorHandler: errorHandler)
|
||||
}
|
||||
|
||||
|
||||
func resumeDatabaseProcessingIfNecessary() {
|
||||
if AccountManager.shared.isSuspended {
|
||||
AccountManager.shared.resumeAll()
|
||||
logger.info("Application processing resumed.")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func prepareAccountsForBackground() {
|
||||
extensionFeedAddRequestFile.suspend()
|
||||
syncTimer?.invalidate()
|
||||
@ -190,16 +190,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
AccountManager.shared.refreshAll(errorHandler: ErrorHandler.log)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
completionHandler([.list, .banner, .badge, .sound])
|
||||
}
|
||||
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
defer { completionHandler() }
|
||||
|
||||
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
|
||||
|
||||
switch response.actionIdentifier {
|
||||
case "MARK_AS_READ":
|
||||
handleMarkAsRead(userInfo: userInfo)
|
||||
@ -213,15 +213,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: App Initialization
|
||||
|
||||
private extension AppDelegate {
|
||||
|
||||
|
||||
private func initializeDownloaders() {
|
||||
let tempDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first!
|
||||
let faviconsFolderURL = tempDir.appendingPathComponent("Favicons")
|
||||
@ -239,7 +239,7 @@ private extension AppDelegate {
|
||||
let unreadTitle = NSLocalizedString("First Unread", comment: "First Unread")
|
||||
let unreadIcon = UIApplicationShortcutIcon(systemImageName: "chevron.down.circle")
|
||||
let unreadItem = UIApplicationShortcutItem(type: "com.ranchero.NetNewsWire.FirstUnread", localizedTitle: unreadTitle, localizedSubtitle: nil, icon: unreadIcon, userInfo: nil)
|
||||
|
||||
|
||||
let searchTitle = NSLocalizedString("Search", comment: "Search")
|
||||
let searchIcon = UIApplicationShortcutIcon(systemImageName: "magnifyingglass")
|
||||
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]
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: Go To Background
|
||||
|
||||
private extension AppDelegate {
|
||||
|
||||
|
||||
func waitForSyncTasksToFinish() {
|
||||
guard !isWaitingForSyncTasks && UIApplication.shared.applicationState == .background else { return }
|
||||
|
||||
|
||||
isWaitingForSyncTasks = true
|
||||
|
||||
|
||||
self.waitBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask { [weak self] in
|
||||
guard let self = self else { return }
|
||||
self.completeProcessing(true)
|
||||
logger.info("Accounts wait for progress terminated for running too long.")
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
self?.waitToComplete() { [weak self] suspend in
|
||||
self?.waitToComplete { [weak self] suspend in
|
||||
self?.completeProcessing(suspend)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func waitToComplete(completion: @escaping (Bool) -> Void) {
|
||||
guard UIApplication.shared.applicationState == .background else {
|
||||
logger.info("App came back to foreground, no longer waiting.")
|
||||
completion(false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if AccountManager.shared.refreshInProgress || isSyncArticleStatusRunning || widgetDataEncoder.isRunning {
|
||||
logger.info("Waiting for sync to finish…")
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { [weak self] in
|
||||
@ -292,7 +292,7 @@ private extension AppDelegate {
|
||||
completion(true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func completeProcessing(_ suspend: Bool) {
|
||||
if suspend {
|
||||
suspendApplication()
|
||||
@ -301,33 +301,33 @@ private extension AppDelegate {
|
||||
self.waitBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
||||
isWaitingForSyncTasks = false
|
||||
}
|
||||
|
||||
|
||||
func syncArticleStatus() {
|
||||
guard !isSyncArticleStatusRunning else { return }
|
||||
|
||||
|
||||
isSyncArticleStatusRunning = true
|
||||
|
||||
|
||||
let completeProcessing = { [unowned self] in
|
||||
self.isSyncArticleStatusRunning = false
|
||||
UIApplication.shared.endBackgroundTask(self.syncBackgroundUpdateTask)
|
||||
self.syncBackgroundUpdateTask = UIBackgroundTaskIdentifier.invalid
|
||||
}
|
||||
|
||||
|
||||
self.syncBackgroundUpdateTask = UIApplication.shared.beginBackgroundTask {
|
||||
completeProcessing()
|
||||
self.logger.info("Accounts sync processing terminated for running too long.")
|
||||
}
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
AccountManager.shared.syncArticleStatusAll() {
|
||||
AccountManager.shared.syncArticleStatusAll {
|
||||
completeProcessing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func suspendApplication() {
|
||||
guard UIApplication.shared.applicationState == .background else { return }
|
||||
|
||||
|
||||
AccountManager.shared.suspendNetworkAll()
|
||||
AccountManager.shared.suspendDatabaseAll()
|
||||
ArticleThemeDownloader.shared.cleanUp()
|
||||
@ -338,10 +338,10 @@ private extension AppDelegate {
|
||||
sceneDelegate.suspend()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
logger.info("Application processing suspended.")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: Background Tasks
|
||||
@ -355,7 +355,7 @@ private extension AppDelegate {
|
||||
self.performBackgroundFeedRefresh(with: task as! BGAppRefreshTask)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Schedules a background app refresh based on `AppDefaults.refreshInterval`.
|
||||
func scheduleBackgroundFeedRefresh() {
|
||||
let request = BGAppRefreshTaskRequest(identifier: "com.ranchero.NetNewsWire.FeedRefresh")
|
||||
@ -371,7 +371,7 @@ private extension AppDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Performs background feed refresh.
|
||||
/// - Parameter task: `BGAppRefreshTask`
|
||||
/// - 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
|
||||
|
||||
private extension AppDelegate {
|
||||
|
||||
|
||||
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 articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
||||
return
|
||||
@ -435,9 +435,9 @@ private extension AppDelegate {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
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 articleID = articlePathUserInfo[ArticlePathKey.articleID] as? String else {
|
||||
return
|
||||
|
@ -16,9 +16,9 @@ enum ArticleExtractorButtonState {
|
||||
}
|
||||
|
||||
class ArticleExtractorButton: UIButton {
|
||||
|
||||
|
||||
private var animatedLayer: CALayer?
|
||||
|
||||
|
||||
var buttonState: ArticleExtractorButtonState = .off {
|
||||
didSet {
|
||||
if buttonState != oldValue {
|
||||
@ -39,7 +39,7 @@ class ArticleExtractorButton: UIButton {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override var accessibilityLabel: String? {
|
||||
get {
|
||||
switch buttonState {
|
||||
@ -57,7 +57,7 @@ class ArticleExtractorButton: UIButton {
|
||||
super.accessibilityLabel = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
guard case .animated = buttonState else {
|
||||
@ -66,31 +66,31 @@ class ArticleExtractorButton: UIButton {
|
||||
stripAnimatedSublayer()
|
||||
addAnimatedSublayer(to: layer)
|
||||
}
|
||||
|
||||
|
||||
private func stripAnimatedSublayer() {
|
||||
animatedLayer?.removeFromSuperlayer()
|
||||
}
|
||||
|
||||
|
||||
private func addAnimatedSublayer(to hostedLayer: CALayer) {
|
||||
let image1 = AppAssets.articleExtractorOffTinted.cgImage!
|
||||
let image2 = AppAssets.articleExtractorOnTinted.cgImage!
|
||||
let images = [image1, image2, image1]
|
||||
|
||||
|
||||
animatedLayer = CALayer()
|
||||
let imageSize = AppAssets.articleExtractorOff.size
|
||||
animatedLayer!.bounds = CGRect(x: 0, y: 0, width: imageSize.width, height: imageSize.height)
|
||||
animatedLayer!.position = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
|
||||
|
||||
hostedLayer.addSublayer(animatedLayer!)
|
||||
|
||||
|
||||
let animation = CAKeyframeAnimation(keyPath: "contents")
|
||||
animation.calculationMode = CAAnimationCalculationMode.linear
|
||||
animation.keyTimes = [0, 0.5, 1]
|
||||
animation.duration = 2
|
||||
animation.values = images as [Any]
|
||||
animation.repeatCount = HUGE
|
||||
|
||||
|
||||
animatedLayer!.add(animation, forKey: "contents")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -15,15 +15,14 @@ import UIKit
|
||||
@objc optional func searchBar(_ searchBar: ArticleSearchBar, textDidChange: String)
|
||||
}
|
||||
|
||||
|
||||
@IBDesignable final class ArticleSearchBar: UIStackView {
|
||||
var searchField: UISearchTextField!
|
||||
var nextButton: UIButton!
|
||||
var prevButton: UIButton!
|
||||
var background: UIView!
|
||||
|
||||
|
||||
weak private var resultsLabel: UILabel!
|
||||
|
||||
|
||||
var resultsCount: UInt = 0 {
|
||||
didSet {
|
||||
updateUI()
|
||||
@ -34,30 +33,30 @@ import UIKit
|
||||
updateUI()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
weak var delegate: SearchBarDelegate?
|
||||
|
||||
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
return [UIKeyCommand(title: "Exit Find", action: #selector(donePressed(_:)), input: UIKeyCommand.inputEscape)]
|
||||
}
|
||||
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
required init(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
override func didMoveToSuperview() {
|
||||
super.didMoveToSuperview()
|
||||
layer.backgroundColor = UIColor(named: "barBackgroundColor")?.cgColor ?? UIColor.white.cgColor
|
||||
isOpaque = true
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(textDidChange(_:)), name: UITextField.textDidChangeNotification, object: searchField)
|
||||
}
|
||||
|
||||
|
||||
private func updateUI() {
|
||||
if resultsCount > 0 {
|
||||
let format = NSLocalizedString("%d of %d", comment: "Results selection and count")
|
||||
@ -65,23 +64,23 @@ import UIKit
|
||||
} else {
|
||||
resultsLabel.text = NSLocalizedString("No results", comment: "No results")
|
||||
}
|
||||
|
||||
|
||||
nextButton.isEnabled = selectedResult < resultsCount
|
||||
prevButton.isEnabled = resultsCount > 0 && selectedResult > 1
|
||||
}
|
||||
|
||||
|
||||
@discardableResult override func becomeFirstResponder() -> Bool {
|
||||
searchField.becomeFirstResponder()
|
||||
}
|
||||
|
||||
|
||||
@discardableResult override func resignFirstResponder() -> Bool {
|
||||
searchField.resignFirstResponder()
|
||||
}
|
||||
|
||||
|
||||
override var isFirstResponder: Bool {
|
||||
searchField.isFirstResponder
|
||||
}
|
||||
|
||||
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
@ -94,12 +93,12 @@ private extension ArticleSearchBar {
|
||||
spacing = 8
|
||||
layoutMargins.left = 8
|
||||
layoutMargins.right = 8
|
||||
|
||||
|
||||
background = UIView(frame: bounds)
|
||||
background.backgroundColor = .systemGray5
|
||||
background.autoresizingMask = [.flexibleWidth, .flexibleHeight]
|
||||
addSubview(background)
|
||||
|
||||
|
||||
let doneButton = UIButton()
|
||||
doneButton.setTitle(NSLocalizedString("Done", comment: "Done"), for: .normal)
|
||||
doneButton.setTitleColor(UIColor.label, for: .normal)
|
||||
@ -108,14 +107,14 @@ private extension ArticleSearchBar {
|
||||
doneButton.addTarget(self, action: #selector(donePressed), for: .touchUpInside)
|
||||
doneButton.isEnabled = true
|
||||
addArrangedSubview(doneButton)
|
||||
|
||||
|
||||
let resultsLabel = UILabel()
|
||||
searchField = UISearchTextField()
|
||||
searchField.autocapitalizationType = .none
|
||||
searchField.autocorrectionType = .no
|
||||
searchField.returnKeyType = .search
|
||||
searchField.delegate = self
|
||||
|
||||
|
||||
resultsLabel.font = .systemFont(ofSize: UIFont.smallSystemFontSize)
|
||||
resultsLabel.textColor = .secondaryLabel
|
||||
resultsLabel.text = ""
|
||||
@ -123,17 +122,17 @@ private extension ArticleSearchBar {
|
||||
resultsLabel.adjustsFontSizeToFitWidth = true
|
||||
searchField.rightView = resultsLabel
|
||||
searchField.rightViewMode = .always
|
||||
|
||||
|
||||
self.resultsLabel = resultsLabel
|
||||
addArrangedSubview(searchField)
|
||||
|
||||
|
||||
prevButton = UIButton(type: .system)
|
||||
prevButton.setImage(UIImage(systemName: "chevron.up"), for: .normal)
|
||||
prevButton.accessibilityLabel = "Previous Result"
|
||||
prevButton.isAccessibilityElement = true
|
||||
prevButton.addTarget(self, action: #selector(previousPressed), for: .touchUpInside)
|
||||
addArrangedSubview(prevButton)
|
||||
|
||||
|
||||
nextButton = UIButton(type: .system)
|
||||
nextButton.setImage(UIImage(systemName: "chevron.down"), for: .normal)
|
||||
nextButton.accessibilityLabel = "Next Result"
|
||||
@ -144,25 +143,25 @@ private extension ArticleSearchBar {
|
||||
}
|
||||
|
||||
private extension ArticleSearchBar {
|
||||
|
||||
|
||||
@objc func textDidChange(_ notification: Notification) {
|
||||
delegate?.searchBar?(self, textDidChange: searchField.text ?? "")
|
||||
|
||||
|
||||
if searchField.text?.isEmpty ?? true {
|
||||
searchField.rightViewMode = .never
|
||||
} else {
|
||||
searchField.rightViewMode = .always
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func nextPressed() {
|
||||
delegate?.nextWasPressed?(self)
|
||||
}
|
||||
|
||||
|
||||
@objc func previousPressed() {
|
||||
delegate?.previousWasPressed?(self)
|
||||
}
|
||||
|
||||
|
||||
@objc func donePressed(_ _: Any? = nil) {
|
||||
delegate?.doneWasPressed?(self)
|
||||
}
|
||||
|
@ -16,21 +16,21 @@ final class ContextMenuPreviewViewController: UIViewController {
|
||||
@IBOutlet weak var blogAuthorLabel: UILabel!
|
||||
@IBOutlet weak var articleTitleLabel: UILabel!
|
||||
@IBOutlet weak var dateTimeLabel: UILabel!
|
||||
|
||||
|
||||
var article: Article?
|
||||
|
||||
init(article: Article?) {
|
||||
self.article = article
|
||||
super.init(nibName: "ContextMenuPreviewViewController", bundle: nil)
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
blogNameLabel.text = article?.feed?.nameForDisplay ?? ""
|
||||
blogAuthorLabel.text = article?.byline()
|
||||
articleTitleLabel.text = article?.title ?? ""
|
||||
@ -39,14 +39,14 @@ final class ContextMenuPreviewViewController: UIViewController {
|
||||
icon.iconImage = article?.iconImage()
|
||||
icon.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(icon)
|
||||
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
icon.widthAnchor.constraint(equalToConstant: 48),
|
||||
icon.heightAnchor.constraint(equalToConstant: 48),
|
||||
icon.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 8),
|
||||
icon.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -20)
|
||||
])
|
||||
|
||||
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateStyle = .long
|
||||
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
|
||||
// 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.
|
||||
|
||||
|
||||
let width: CGFloat
|
||||
let heightPadding: CGFloat
|
||||
if view.bounds.width > view.bounds.height {
|
||||
@ -68,7 +68,7 @@ final class ContextMenuPreviewViewController: UIViewController {
|
||||
width = view.bounds.width
|
||||
heightPadding = 8
|
||||
}
|
||||
|
||||
|
||||
view.setNeedsLayout()
|
||||
view.layoutIfNeeded()
|
||||
preferredContentSize = CGSize(width: width, height: dateTimeLabel.frame.maxY + heightPadding)
|
||||
|
@ -12,27 +12,27 @@ class FindInArticleActivity: UIActivity {
|
||||
override var activityTitle: String? {
|
||||
NSLocalizedString("Find in Article", comment: "Find in Article")
|
||||
}
|
||||
|
||||
|
||||
override var activityType: UIActivity.ActivityType? {
|
||||
UIActivity.ActivityType(rawValue: "com.ranchero.NetNewsWire.find")
|
||||
}
|
||||
|
||||
|
||||
override var activityImage: UIImage? {
|
||||
UIImage(systemName: "magnifyingglass", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))
|
||||
}
|
||||
|
||||
|
||||
override class var activityCategory: UIActivity.Category {
|
||||
.action
|
||||
}
|
||||
|
||||
|
||||
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
|
||||
true
|
||||
}
|
||||
|
||||
|
||||
override func prepare(withActivityItems activityItems: [Any]) {
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
override func perform() {
|
||||
NotificationCenter.default.post(Notification(name: .FindInArticle))
|
||||
activityDidFinish(true)
|
||||
|
@ -14,63 +14,63 @@ import UIKit
|
||||
}
|
||||
|
||||
open class ImageScrollView: UIScrollView {
|
||||
|
||||
|
||||
@objc public enum ScaleMode: Int {
|
||||
case aspectFill
|
||||
case aspectFit
|
||||
case widthFill
|
||||
case heightFill
|
||||
}
|
||||
|
||||
|
||||
@objc public enum Offset: Int {
|
||||
case beginning
|
||||
case center
|
||||
}
|
||||
|
||||
|
||||
static let kZoomInFactorFromMinWhenDoubleTap: CGFloat = 2
|
||||
|
||||
|
||||
@objc open var imageContentMode: ScaleMode = .widthFill
|
||||
@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?
|
||||
|
||||
|
||||
var imageSize: CGSize = CGSize.zero
|
||||
private var pointToCenterAfterResize: CGPoint = CGPoint.zero
|
||||
private var scaleToRestoreAfterResize: CGFloat = 1.0
|
||||
var maxScaleFromMinScale: CGFloat = 3.0
|
||||
|
||||
|
||||
var zoomedFrame: CGRect {
|
||||
return zoomView?.frame ?? CGRect.zero
|
||||
}
|
||||
|
||||
|
||||
override open var frame: CGRect {
|
||||
willSet {
|
||||
if frame.equalTo(newValue) == false && newValue.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false {
|
||||
prepareToResize()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
didSet {
|
||||
if frame.equalTo(oldValue) == false && frame.equalTo(CGRect.zero) == false && imageSize.equalTo(CGSize.zero) == false {
|
||||
recoverFromResizing()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override public init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
|
||||
required public init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
|
||||
|
||||
initialize()
|
||||
}
|
||||
|
||||
|
||||
private func initialize() {
|
||||
showsVerticalScrollIndicator = false
|
||||
showsHorizontalScrollIndicator = false
|
||||
@ -78,135 +78,135 @@ open class ImageScrollView: UIScrollView {
|
||||
decelerationRate = UIScrollView.DecelerationRate.fast
|
||||
delegate = self
|
||||
}
|
||||
|
||||
|
||||
@objc public func adjustFrameToCenter() {
|
||||
|
||||
|
||||
guard let unwrappedZoomView = zoomView else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
var frameToCenter = unwrappedZoomView.frame
|
||||
|
||||
|
||||
// center horizontally
|
||||
if frameToCenter.size.width < bounds.width {
|
||||
frameToCenter.origin.x = (bounds.width - frameToCenter.size.width) / 2
|
||||
} else {
|
||||
frameToCenter.origin.x = 0
|
||||
}
|
||||
|
||||
|
||||
// center vertically
|
||||
if frameToCenter.size.height < bounds.height {
|
||||
frameToCenter.origin.y = (bounds.height - frameToCenter.size.height) / 2
|
||||
} else {
|
||||
frameToCenter.origin.y = 0
|
||||
}
|
||||
|
||||
|
||||
unwrappedZoomView.frame = frameToCenter
|
||||
}
|
||||
|
||||
|
||||
private func prepareToResize() {
|
||||
let boundsCenter = CGPoint(x: bounds.midX, y: bounds.midY)
|
||||
pointToCenterAfterResize = convert(boundsCenter, to: zoomView)
|
||||
|
||||
|
||||
scaleToRestoreAfterResize = zoomScale
|
||||
|
||||
|
||||
// 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.
|
||||
if scaleToRestoreAfterResize <= minimumZoomScale + CGFloat(Float.ulpOfOne) {
|
||||
scaleToRestoreAfterResize = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func recoverFromResizing() {
|
||||
setMaxMinZoomScalesForCurrentBounds()
|
||||
|
||||
|
||||
// restore zoom scale, first making sure it is within the allowable range.
|
||||
let maxZoomScale = max(minimumZoomScale, scaleToRestoreAfterResize)
|
||||
zoomScale = min(maximumZoomScale, maxZoomScale)
|
||||
|
||||
|
||||
// restore center point, first making sure it is within the allowable range.
|
||||
|
||||
|
||||
// convert our desired center point back to our own coordinate space
|
||||
let boundsCenter = convert(pointToCenterAfterResize, to: zoomView)
|
||||
|
||||
|
||||
// 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)
|
||||
|
||||
|
||||
// restore offset, adjusted to be within the allowable range
|
||||
let maxOffset = maximumContentOffset()
|
||||
let minOffset = minimumContentOffset()
|
||||
|
||||
|
||||
var realMaxOffset = min(maxOffset.x, offset.x)
|
||||
offset.x = max(minOffset.x, realMaxOffset)
|
||||
|
||||
|
||||
realMaxOffset = min(maxOffset.y, offset.y)
|
||||
offset.y = max(minOffset.y, realMaxOffset)
|
||||
|
||||
|
||||
contentOffset = offset
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
return CGPoint.zero
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Set up
|
||||
|
||||
|
||||
open func setup() {
|
||||
var topSupperView = superview
|
||||
|
||||
|
||||
while topSupperView?.superview != nil {
|
||||
topSupperView = topSupperView?.superview
|
||||
}
|
||||
|
||||
|
||||
// Make sure views have already layout with precise frame
|
||||
topSupperView?.layoutIfNeeded()
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Display image
|
||||
|
||||
|
||||
@objc open func display(image: UIImage) {
|
||||
|
||||
|
||||
if let zoomView = zoomView {
|
||||
zoomView.removeFromSuperview()
|
||||
}
|
||||
|
||||
|
||||
zoomView = UIImageView(image: image)
|
||||
zoomView!.isUserInteractionEnabled = true
|
||||
addSubview(zoomView!)
|
||||
|
||||
|
||||
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(doubleTapGestureRecognizer(_:)))
|
||||
tapGesture.numberOfTapsRequired = 2
|
||||
zoomView!.addGestureRecognizer(tapGesture)
|
||||
|
||||
|
||||
let downSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeUpGestureRecognizer(_:)))
|
||||
downSwipeGesture.direction = .down
|
||||
zoomView!.addGestureRecognizer(downSwipeGesture)
|
||||
|
||||
|
||||
let upSwipeGesture = UISwipeGestureRecognizer(target: self, action: #selector(swipeDownGestureRecognizer(_:)))
|
||||
upSwipeGesture.direction = .up
|
||||
zoomView!.addGestureRecognizer(upSwipeGesture)
|
||||
|
||||
|
||||
configureImageForSize(image.size)
|
||||
adjustFrameToCenter()
|
||||
}
|
||||
|
||||
|
||||
private func configureImageForSize(_ size: CGSize) {
|
||||
imageSize = size
|
||||
contentSize = imageSize
|
||||
setMaxMinZoomScalesForCurrentBounds()
|
||||
zoomScale = minimumZoomScale
|
||||
|
||||
|
||||
switch initialOffset {
|
||||
case .beginning:
|
||||
contentOffset = CGPoint.zero
|
||||
case .center:
|
||||
let xOffset = contentSize.width < bounds.width ? 0 : (contentSize.width - bounds.width)/2
|
||||
let yOffset = contentSize.height < bounds.height ? 0 : (contentSize.height - bounds.height)/2
|
||||
|
||||
|
||||
switch imageContentMode {
|
||||
case .aspectFit:
|
||||
contentOffset = CGPoint.zero
|
||||
@ -219,14 +219,14 @@ open class ImageScrollView: UIScrollView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func setMaxMinZoomScalesForCurrentBounds() {
|
||||
// calculate min/max zoomscale
|
||||
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
|
||||
|
||||
|
||||
var minScale: CGFloat = 1
|
||||
|
||||
|
||||
switch imageContentMode {
|
||||
case .aspectFill:
|
||||
minScale = max(xScale, yScale)
|
||||
@ -237,21 +237,20 @@ open class ImageScrollView: UIScrollView {
|
||||
case .heightFill:
|
||||
minScale = yScale
|
||||
}
|
||||
|
||||
|
||||
|
||||
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.)
|
||||
if minScale > maxScale {
|
||||
minScale = maxScale
|
||||
}
|
||||
|
||||
|
||||
maximumZoomScale = maxScale
|
||||
minimumZoomScale = minScale // * 0.999 // the multiply factor to prevent user cannot scroll page while they use this control in UIPageViewController
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Gesture
|
||||
|
||||
|
||||
@objc func doubleTapGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
|
||||
// zoom out if it bigger than middle scale point. Else, zoom in
|
||||
if zoomScale >= maximumZoomScale / 2.0 {
|
||||
@ -262,96 +261,96 @@ open class ImageScrollView: UIScrollView {
|
||||
zoom(to: zoomRect, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func swipeUpGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
|
||||
if gestureRecognizer.state == .ended {
|
||||
imageScrollViewDelegate?.imageScrollViewDidGestureSwipeUp(imageScrollView: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func swipeDownGestureRecognizer(_ gestureRecognizer: UIGestureRecognizer) {
|
||||
if gestureRecognizer.state == .ended {
|
||||
imageScrollViewDelegate?.imageScrollViewDidGestureSwipeDown(imageScrollView: self)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func zoomRectForScale(_ scale: CGFloat, center: CGPoint) -> CGRect {
|
||||
var zoomRect = CGRect.zero
|
||||
|
||||
|
||||
// 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.
|
||||
// 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.width = frame.size.width / scale
|
||||
|
||||
|
||||
// choose an origin so as to get the right center.
|
||||
zoomRect.origin.x = center.x - (zoomRect.size.width / 2.0)
|
||||
zoomRect.origin.y = center.y - (zoomRect.size.height / 2.0)
|
||||
|
||||
|
||||
return zoomRect
|
||||
}
|
||||
|
||||
|
||||
open func refresh() {
|
||||
if let image = zoomView?.image {
|
||||
display(image: image)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
open func resize() {
|
||||
self.configureImageForSize(self.imageSize)
|
||||
}
|
||||
}
|
||||
|
||||
extension ImageScrollView: UIScrollViewDelegate {
|
||||
|
||||
|
||||
public func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
imageScrollViewDelegate?.scrollViewDidScroll?(scrollView)
|
||||
}
|
||||
|
||||
|
||||
public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
imageScrollViewDelegate?.scrollViewWillBeginDragging?(scrollView)
|
||||
}
|
||||
|
||||
|
||||
public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
|
||||
imageScrollViewDelegate?.scrollViewWillEndDragging?(scrollView, withVelocity: velocity, targetContentOffset: targetContentOffset)
|
||||
}
|
||||
|
||||
|
||||
public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
|
||||
imageScrollViewDelegate?.scrollViewDidEndDragging?(scrollView, willDecelerate: decelerate)
|
||||
}
|
||||
|
||||
|
||||
public func scrollViewWillBeginDecelerating(_ scrollView: UIScrollView) {
|
||||
imageScrollViewDelegate?.scrollViewWillBeginDecelerating?(scrollView)
|
||||
}
|
||||
|
||||
|
||||
public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
|
||||
imageScrollViewDelegate?.scrollViewDidEndDecelerating?(scrollView)
|
||||
}
|
||||
|
||||
|
||||
public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
|
||||
imageScrollViewDelegate?.scrollViewDidEndScrollingAnimation?(scrollView)
|
||||
}
|
||||
|
||||
|
||||
public func scrollViewWillBeginZooming(_ scrollView: UIScrollView, with view: UIView?) {
|
||||
imageScrollViewDelegate?.scrollViewWillBeginZooming?(scrollView, with: view)
|
||||
}
|
||||
|
||||
|
||||
public func scrollViewDidEndZooming(_ scrollView: UIScrollView, with view: UIView?, atScale scale: CGFloat) {
|
||||
imageScrollViewDelegate?.scrollViewDidEndZooming?(scrollView, with: view, atScale: scale)
|
||||
}
|
||||
|
||||
|
||||
public func scrollViewShouldScrollToTop(_ scrollView: UIScrollView) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
public func scrollViewDidChangeAdjustedContentInset(_ scrollView: UIScrollView) {
|
||||
imageScrollViewDelegate?.scrollViewDidChangeAdjustedContentInset?(scrollView)
|
||||
}
|
||||
|
||||
|
||||
public func viewForZooming(in scrollView: UIScrollView) -> UIView? {
|
||||
return zoomView
|
||||
}
|
||||
|
||||
|
||||
public func scrollViewDidZoom(_ scrollView: UIScrollView) {
|
||||
adjustFrameToCenter()
|
||||
imageScrollViewDelegate?.scrollViewDidZoom?(scrollView)
|
||||
|
@ -16,15 +16,15 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
var originFrame: CGRect!
|
||||
var maskFrame: CGRect!
|
||||
var originImage: UIImage!
|
||||
|
||||
|
||||
init(controller: WebViewController) {
|
||||
self.webViewController = controller
|
||||
}
|
||||
|
||||
|
||||
func transitionDuration(using transitionContext: UIViewControllerContextTransitioning?) -> TimeInterval {
|
||||
return duration
|
||||
}
|
||||
|
||||
|
||||
func animateTransition(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
if presenting {
|
||||
animateTransitionPresenting(using: transitionContext)
|
||||
@ -32,23 +32,23 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
animateTransitionReturning(using: transitionContext)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func animateTransitionPresenting(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
|
||||
let imageView = UIImageView(image: originImage)
|
||||
imageView.frame = originFrame
|
||||
|
||||
|
||||
let fromView = transitionContext.view(forKey: .from)!
|
||||
fromView.removeFromSuperview()
|
||||
|
||||
transitionContext.containerView.backgroundColor = AppAssets.fullScreenBackgroundColor
|
||||
transitionContext.containerView.addSubview(imageView)
|
||||
|
||||
|
||||
webViewController?.hideClickedImage()
|
||||
|
||||
UIView.animate(
|
||||
withDuration: duration,
|
||||
delay:0.0,
|
||||
delay: 0.0,
|
||||
usingSpringWithDamping: 0.8,
|
||||
initialSpringVelocity: 0.2,
|
||||
animations: {
|
||||
@ -61,40 +61,40 @@ class ImageTransition: NSObject, UIViewControllerAnimatedTransitioning {
|
||||
transitionContext.completeTransition(true)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
private func animateTransitionReturning(using transitionContext: UIViewControllerContextTransitioning) {
|
||||
let imageController = transitionContext.viewController(forKey: .from) as! ImageViewController
|
||||
let imageView = UIImageView(image: originImage)
|
||||
imageView.frame = imageController.zoomedFrame
|
||||
|
||||
|
||||
let fromView = transitionContext.view(forKey: .from)!
|
||||
let windowFrame = fromView.window!.frame
|
||||
fromView.removeFromSuperview()
|
||||
|
||||
|
||||
let toView = transitionContext.view(forKey: .to)!
|
||||
transitionContext.containerView.addSubview(toView)
|
||||
|
||||
|
||||
let maskingView = UIView()
|
||||
|
||||
|
||||
let fullMaskFrame = CGRect(x: windowFrame.minX, y: maskFrame.minY, width: windowFrame.width, height: maskFrame.height)
|
||||
let path = UIBezierPath(rect: fullMaskFrame)
|
||||
let maskLayer = CAShapeLayer()
|
||||
maskLayer.path = path.cgPath
|
||||
maskingView.layer.mask = maskLayer
|
||||
|
||||
|
||||
maskingView.addSubview(imageView)
|
||||
transitionContext.containerView.addSubview(maskingView)
|
||||
|
||||
UIView.animate(
|
||||
withDuration: duration,
|
||||
delay:0.0,
|
||||
delay: 0.0,
|
||||
usingSpringWithDamping: 0.8,
|
||||
initialSpringVelocity: 0.2,
|
||||
animations: {
|
||||
imageView.frame = self.originFrame
|
||||
}, completion: { _ in
|
||||
if let controller = self.webViewController {
|
||||
controller.showClickedImage() {
|
||||
controller.showClickedImage {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||
imageView.removeFromSuperview()
|
||||
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 titleLeading: NSLayoutConstraint!
|
||||
@IBOutlet weak var titleTrailing: NSLayoutConstraint!
|
||||
|
||||
|
||||
var image: UIImage!
|
||||
var imageTitle: String?
|
||||
var zoomedFrame: CGRect {
|
||||
@ -27,27 +27,27 @@ final class ImageViewController: UIViewController {
|
||||
init() {
|
||||
super.init(nibName: "ImageViewController", bundle: nil)
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
}
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
closeButton.imageView?.contentMode = .scaleAspectFit
|
||||
closeButton.accessibilityLabel = NSLocalizedString("Close", comment: "Close")
|
||||
shareButton.accessibilityLabel = NSLocalizedString("Share", comment: "Share")
|
||||
|
||||
|
||||
imageScrollView.setup()
|
||||
imageScrollView.imageScrollViewDelegate = self
|
||||
imageScrollView.imageContentMode = .aspectFit
|
||||
imageScrollView.initialOffset = .center
|
||||
imageScrollView.display(image: image)
|
||||
|
||||
|
||||
titleLabel.text = imageTitle ?? ""
|
||||
layoutTitleLabel()
|
||||
|
||||
|
||||
guard imageTitle != "" else {
|
||||
titleBackground.removeFromSuperview()
|
||||
return
|
||||
@ -57,11 +57,11 @@ final class ImageViewController: UIViewController {
|
||||
|
||||
override func viewWillTransition(to size: CGSize, with coordinator: UIViewControllerTransitionCoordinator) {
|
||||
super.viewWillTransition(to: size, with: coordinator)
|
||||
coordinator.animate(alongsideTransition: { [weak self] context in
|
||||
coordinator.animate(alongsideTransition: { [weak self] _ in
|
||||
self?.imageScrollView.resize()
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@IBAction func share(_ sender: Any) {
|
||||
guard let image = image else { return }
|
||||
let activityViewController = UIActivityViewController(activityItems: [image], applicationActivities: nil)
|
||||
@ -69,12 +69,12 @@ final class ImageViewController: UIViewController {
|
||||
activityViewController.popoverPresentationController?.sourceRect = shareButton.bounds
|
||||
present(activityViewController, animated: true)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func done(_ sender: Any) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
private func layoutTitleLabel(){
|
||||
|
||||
private func layoutTitleLabel() {
|
||||
let width = view.frame.width
|
||||
let multiplier = UIDevice.current.userInterfaceIdiom == .pad ? CGFloat(0.1) : CGFloat(0.04)
|
||||
titleLeading.constant += width * multiplier
|
||||
@ -90,10 +90,9 @@ extension ImageViewController: ImageScrollViewDelegate {
|
||||
func imageScrollViewDidGestureSwipeUp(imageScrollView: ImageScrollView) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
|
||||
func imageScrollViewDidGestureSwipeDown(imageScrollView: ImageScrollView) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,17 +9,17 @@
|
||||
import UIKit
|
||||
|
||||
class OpenInBrowserActivity: UIActivity {
|
||||
|
||||
|
||||
private var activityItems: [Any]?
|
||||
|
||||
override var activityTitle: String? {
|
||||
return NSLocalizedString("Open in Browser", comment: "Open in Browser")
|
||||
}
|
||||
|
||||
|
||||
override var activityImage: UIImage? {
|
||||
return UIImage(systemName: "globe", withConfiguration: UIImage.SymbolConfiguration(pointSize: 20, weight: .regular))
|
||||
}
|
||||
|
||||
|
||||
override var activityType: UIActivity.ActivityType? {
|
||||
return UIActivity.ActivityType(rawValue: "com.rancharo.NetNewsWire-Evergreen.safari")
|
||||
}
|
||||
@ -27,23 +27,23 @@ class OpenInBrowserActivity: UIActivity {
|
||||
override class var activityCategory: UIActivity.Category {
|
||||
return .action
|
||||
}
|
||||
|
||||
|
||||
override func canPerform(withActivityItems activityItems: [Any]) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
override func prepare(withActivityItems activityItems: [Any]) {
|
||||
self.activityItems = activityItems
|
||||
}
|
||||
|
||||
|
||||
override func perform() {
|
||||
guard let url = activityItems?.first(where: { $0 is URL }) as? URL else {
|
||||
activityDidFinish(false)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
activityDidFinish(true)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -30,7 +30,7 @@ final class WebViewController: UIViewController {
|
||||
private var bottomShowBarsView: UIView!
|
||||
private var topShowBarsViewConstraint: NSLayoutConstraint!
|
||||
private var bottomShowBarsViewConstraint: NSLayoutConstraint!
|
||||
|
||||
|
||||
var webView: WKWebView? {
|
||||
return view.subviews[0] as? WKWebView
|
||||
}
|
||||
@ -43,7 +43,7 @@ final class WebViewController: UIViewController {
|
||||
private lazy var transition = ImageTransition(controller: self)
|
||||
private var clickedImageCompletion: (() -> Void)?
|
||||
|
||||
private var articleExtractor: ArticleExtractor? = nil
|
||||
private var articleExtractor: ArticleExtractor?
|
||||
var extractedArticle: ExtractedArticle? {
|
||||
didSet {
|
||||
windowScrollY = 0
|
||||
@ -56,12 +56,12 @@ final class WebViewController: UIViewController {
|
||||
delegate?.webViewController(self, articleExtractorButtonStateDidUpdate: articleExtractorButtonState)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
weak var coordinator: SceneCoordinator!
|
||||
weak var delegate: WebViewControllerDelegate?
|
||||
|
||||
|
||||
private(set) var article: Article?
|
||||
|
||||
|
||||
let scrollPositionQueue = CoalescingQueue(name: "Article Scroll Position", interval: 0.3, maxInterval: 0.3)
|
||||
var windowScrollY = 0
|
||||
private var restoreWindowScrollY: Int?
|
||||
@ -77,13 +77,13 @@ final class WebViewController: UIViewController {
|
||||
// Configure the tap zones
|
||||
configureTopShowBarsView()
|
||||
configureBottomShowBarsView()
|
||||
|
||||
|
||||
loadWebView()
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
|
||||
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
||||
reloadArticleImage()
|
||||
}
|
||||
@ -101,16 +101,16 @@ final class WebViewController: UIViewController {
|
||||
}
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
|
||||
@objc func showBars(_ sender: Any) {
|
||||
showBars()
|
||||
}
|
||||
|
||||
|
||||
// MARK: API
|
||||
|
||||
func setArticle(_ article: Article?, updateView: Bool = true) {
|
||||
stopArticleExtractor()
|
||||
|
||||
|
||||
if article != self.article {
|
||||
self.article = article
|
||||
if updateView {
|
||||
@ -121,9 +121,9 @@ final class WebViewController: UIViewController {
|
||||
loadWebView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
func setScrollPosition(isShowingExtractedArticle: Bool, articleWindowScrollY: Int) {
|
||||
if isShowingExtractedArticle {
|
||||
switch articleExtractor?.state {
|
||||
@ -144,7 +144,7 @@ final class WebViewController: UIViewController {
|
||||
loadWebView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func focus() {
|
||||
webView?.becomeFirstResponder()
|
||||
}
|
||||
@ -164,7 +164,7 @@ final class WebViewController: UIViewController {
|
||||
|
||||
let overlap = 2 * UIFont.systemFont(ofSize: UIFont.systemFontSize).lineHeight * UIScreen.main.scale
|
||||
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 final = finalScrollPosition(scrollingUp: scrollingUp)
|
||||
return (scrollingUp ? fullScroll > final : fullScroll < final) ? fullScroll : final
|
||||
@ -186,12 +186,12 @@ final class WebViewController: UIViewController {
|
||||
func hideClickedImage() {
|
||||
webView?.evaluateJavaScript("hideClickedImage();")
|
||||
}
|
||||
|
||||
|
||||
func showClickedImage(completion: @escaping () -> Void) {
|
||||
clickedImageCompletion = completion
|
||||
webView?.evaluateJavaScript("showClickedImage();")
|
||||
}
|
||||
|
||||
|
||||
func fullReload() {
|
||||
loadWebView(replaceExistingWebView: true)
|
||||
}
|
||||
@ -205,7 +205,7 @@ final class WebViewController: UIViewController {
|
||||
navigationController?.setToolbarHidden(false, animated: true)
|
||||
configureContextMenuInteraction()
|
||||
}
|
||||
|
||||
|
||||
func hideBars() {
|
||||
if isFullScreenAvailable {
|
||||
AppDefaults.shared.articleFullscreenEnabled = true
|
||||
@ -248,7 +248,7 @@ final class WebViewController: UIViewController {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func stopArticleExtractorIfProcessing() {
|
||||
if articleExtractor?.state == .processing {
|
||||
stopArticleExtractor()
|
||||
@ -261,7 +261,7 @@ final class WebViewController: UIViewController {
|
||||
cancelImageLoad(webView)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func showActivityDialog(popOverBarButtonItem: UIBarButtonItem? = nil) {
|
||||
guard let url = article?.preferredURL else { return }
|
||||
let activityViewController = UIActivityViewController(url: url, title: article?.title, applicationActivities: [FindInArticleActivity(), OpenInBrowserActivity()])
|
||||
@ -325,12 +325,12 @@ extension WebViewController: ArticleExtractorDelegate {
|
||||
|
||||
extension WebViewController: UIContextMenuInteractionDelegate {
|
||||
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 }
|
||||
|
||||
var menus = [UIMenu]()
|
||||
|
||||
|
||||
var navActions = [UIAction]()
|
||||
if let action = self.prevArticleAction() {
|
||||
navActions.append(action)
|
||||
@ -341,7 +341,7 @@ extension WebViewController: UIContextMenuInteractionDelegate {
|
||||
if !navActions.isEmpty {
|
||||
menus.append(UIMenu(title: "", options: .displayInline, children: navActions))
|
||||
}
|
||||
|
||||
|
||||
var toggleActions = [UIAction]()
|
||||
if let action = self.toggleReadAction() {
|
||||
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.shareAction()]))
|
||||
|
||||
|
||||
return UIMenu(title: "", children: menus)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func contextMenuInteraction(_ interaction: UIContextMenuInteraction, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) {
|
||||
coordinator.showBrowserForCurrentArticle()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: WKNavigationDelegate
|
||||
|
||||
extension WebViewController: WKNavigationDelegate {
|
||||
|
||||
|
||||
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
|
||||
|
||||
|
||||
if navigationAction.navigationType == .linkActivated {
|
||||
guard let url = navigationAction.request.url else {
|
||||
decisionHandler(.allow)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
if components?.scheme == "http" || components?.scheme == "https" {
|
||||
decisionHandler(.cancel)
|
||||
@ -391,16 +391,16 @@ extension WebViewController: WKNavigationDelegate {
|
||||
self.openURLInSafariViewController(url)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} else if components?.scheme == "mailto" {
|
||||
decisionHandler(.cancel)
|
||||
|
||||
|
||||
guard let emailAddress = url.percentEncodedEmailAddress else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if UIApplication.shared.canOpenURL(emailAddress) {
|
||||
UIApplication.shared.open(emailAddress, options: [.universalLinksOnly : false], completionHandler: nil)
|
||||
UIApplication.shared.open(emailAddress, options: [.universalLinksOnly: false], completionHandler: nil)
|
||||
} else {
|
||||
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))
|
||||
@ -408,11 +408,11 @@ extension WebViewController: WKNavigationDelegate {
|
||||
}
|
||||
} else if components?.scheme == "tel" {
|
||||
decisionHandler(.cancel)
|
||||
|
||||
|
||||
if UIApplication.shared.canOpenURL(url) {
|
||||
UIApplication.shared.open(url, options: [.universalLinksOnly : false], completionHandler: nil)
|
||||
UIApplication.shared.open(url, options: [.universalLinksOnly: false], completionHandler: nil)
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
decisionHandler(.allow)
|
||||
}
|
||||
@ -424,13 +424,13 @@ extension WebViewController: WKNavigationDelegate {
|
||||
func webViewWebContentProcessDidTerminate(_ webView: WKWebView) {
|
||||
fullReload()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: WKUIDelegate
|
||||
|
||||
extension WebViewController: WKUIDelegate {
|
||||
|
||||
|
||||
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
|
||||
// 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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
openURL(url)
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: WKScriptMessageHandler
|
||||
@ -467,7 +467,7 @@ extension WebViewController: WKScriptMessageHandler {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: UIViewControllerTransitioningDelegate
|
||||
@ -478,7 +478,7 @@ extension WebViewController: UIViewControllerTransitioningDelegate {
|
||||
transition.presenting = true
|
||||
return transition
|
||||
}
|
||||
|
||||
|
||||
func animationController(forDismissed dismissed: UIViewController) -> UIViewControllerAnimatedTransitioning? {
|
||||
transition.presenting = false
|
||||
return transition
|
||||
@ -488,11 +488,11 @@ extension WebViewController: UIViewControllerTransitioningDelegate {
|
||||
// MARK:
|
||||
|
||||
extension WebViewController: UIScrollViewDelegate {
|
||||
|
||||
|
||||
func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
|
||||
}
|
||||
|
||||
|
||||
@objc func scrollPositionDidChange() {
|
||||
webView?.evaluateJavaScript("window.scrollY") { (scrollY, error) in
|
||||
guard error == nil else { return }
|
||||
@ -502,11 +502,9 @@ extension WebViewController: UIScrollViewDelegate {
|
||||
self.windowScrollY = javascriptScrollY
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: JSON
|
||||
|
||||
private struct ImageClickMessage: Codable {
|
||||
@ -564,7 +562,7 @@ private extension WebViewController {
|
||||
|
||||
func renderPage(_ webView: WKWebView?) {
|
||||
guard let webView = webView else { return }
|
||||
|
||||
|
||||
let theme = ArticleThemesManager.shared.currentTheme
|
||||
let rendering: ArticleRenderer.Rendering
|
||||
|
||||
@ -583,7 +581,7 @@ private extension WebViewController {
|
||||
} else {
|
||||
rendering = ArticleRenderer.noSelectionHTML(theme: theme)
|
||||
}
|
||||
|
||||
|
||||
let substitutions = [
|
||||
"title": rendering.title,
|
||||
"baseURL": rendering.baseURL,
|
||||
@ -595,7 +593,7 @@ private extension WebViewController {
|
||||
let html = try! MacroProcessor.renderedText(withTemplate: ArticleRenderer.page.html, substitutions: substitutions)
|
||||
webView.loadHTMLString(html, baseURL: URL(string: rendering.baseURL))
|
||||
}
|
||||
|
||||
|
||||
func finalScrollPosition(scrollingUp: Bool) -> CGFloat {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func startArticleExtractor() {
|
||||
guard articleExtractor == nil else { return }
|
||||
if let link = article?.preferredLink, let extractor = ArticleExtractor(link) {
|
||||
@ -629,12 +627,12 @@ private extension WebViewController {
|
||||
var components = URLComponents()
|
||||
components.scheme = ArticleRenderer.imageIconScheme
|
||||
components.path = article.articleID
|
||||
|
||||
|
||||
if let imageSrc = components.string {
|
||||
webView?.evaluateJavaScript("reloadArticleImage(\"\(imageSrc)\")")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func imageWasClicked(body: String?) {
|
||||
guard let webView = webView,
|
||||
let body = body,
|
||||
@ -642,22 +640,22 @@ private extension WebViewController {
|
||||
let clickMessage = try? JSONDecoder().decode(ImageClickMessage.self, from: data),
|
||||
let range = clickMessage.imageURL.range(of: ";base64,")
|
||||
else { return }
|
||||
|
||||
|
||||
let base64Image = String(clickMessage.imageURL.suffix(from: range.upperBound))
|
||||
if let imageData = Data(base64Encoded: base64Image), let image = UIImage(data: imageData) {
|
||||
|
||||
|
||||
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))
|
||||
transition.originFrame = webView.convert(rect, to: nil)
|
||||
|
||||
|
||||
if navigationController?.navigationBar.isHidden ?? false {
|
||||
transition.maskFrame = webView.convert(webView.frame, to: nil)
|
||||
} else {
|
||||
transition.maskFrame = webView.convert(webView.safeAreaLayoutGuide.layoutFrame, to: nil)
|
||||
}
|
||||
|
||||
|
||||
transition.originImage = image
|
||||
|
||||
|
||||
coordinator.showFullScreenImage(image: image, imageTitle: clickMessage.imageTitle, transitioningDelegate: self)
|
||||
}
|
||||
}
|
||||
@ -675,13 +673,13 @@ private extension WebViewController {
|
||||
topShowBarsView.backgroundColor = .clear
|
||||
topShowBarsView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(topShowBarsView)
|
||||
|
||||
|
||||
if AppDefaults.shared.logicalArticleFullscreenEnabled {
|
||||
topShowBarsViewConstraint = view.topAnchor.constraint(equalTo: topShowBarsView.bottomAnchor, constant: -44.0)
|
||||
} else {
|
||||
topShowBarsViewConstraint = view.topAnchor.constraint(equalTo: topShowBarsView.bottomAnchor, constant: 0.0)
|
||||
}
|
||||
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
topShowBarsViewConstraint,
|
||||
view.leadingAnchor.constraint(equalTo: topShowBarsView.leadingAnchor),
|
||||
@ -690,7 +688,7 @@ private extension WebViewController {
|
||||
])
|
||||
topShowBarsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBars(_:))))
|
||||
}
|
||||
|
||||
|
||||
func configureBottomShowBarsView() {
|
||||
bottomShowBarsView = UIView()
|
||||
topShowBarsView.backgroundColor = .clear
|
||||
@ -709,7 +707,7 @@ private extension WebViewController {
|
||||
])
|
||||
bottomShowBarsView.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(showBars(_:))))
|
||||
}
|
||||
|
||||
|
||||
func configureContextMenuInteraction() {
|
||||
if isFullScreenAvailable {
|
||||
if navigationController?.isNavigationBarHidden ?? false {
|
||||
@ -719,33 +717,33 @@ private extension WebViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func contextMenuPreviewProvider() -> UIViewController {
|
||||
ContextMenuPreviewViewController(article: article)
|
||||
}
|
||||
|
||||
|
||||
func prevArticleAction() -> UIAction? {
|
||||
guard coordinator.isPrevArticleAvailable else { return nil }
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func nextArticleAction() -> UIAction? {
|
||||
guard coordinator.isNextArticleAvailable else { return nil }
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func toggleReadAction() -> UIAction? {
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
@ -754,7 +752,7 @@ private extension WebViewController {
|
||||
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 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()
|
||||
}
|
||||
}
|
||||
@ -762,23 +760,23 @@ private extension WebViewController {
|
||||
func nextUnreadArticleAction() -> UIAction? {
|
||||
guard coordinator.isAnyUnreadAvailable else { return nil }
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func toggleArticleExtractorAction() -> UIAction {
|
||||
let extracted = articleExtractorButtonState == .on
|
||||
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
|
||||
return UIAction(title: title, image: extractorImage) { [weak self] action in
|
||||
return UIAction(title: title, image: extractorImage) { [weak self] _ in
|
||||
self?.toggleArticleExtractor()
|
||||
}
|
||||
}
|
||||
|
||||
func shareAction() -> UIAction {
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -816,27 +814,27 @@ internal struct FindInArticleState: Codable {
|
||||
let width: Double
|
||||
let height: Double
|
||||
}
|
||||
|
||||
|
||||
struct FindInArticleResult: Codable {
|
||||
let rects: [WebViewClientRect]
|
||||
let bounds: WebViewClientRect
|
||||
let index: UInt
|
||||
let matchGroups: [String]
|
||||
}
|
||||
|
||||
|
||||
let index: UInt?
|
||||
let results: [FindInArticleResult]
|
||||
let count: UInt
|
||||
}
|
||||
|
||||
extension WebViewController {
|
||||
|
||||
|
||||
func searchText(_ searchText: String, completionHandler: @escaping (FindInArticleState) -> Void) {
|
||||
guard let json = try? JSONEncoder().encode(FindInArticleOptions(text: searchText)) else {
|
||||
return
|
||||
}
|
||||
let encoded = json.base64EncodedString()
|
||||
|
||||
|
||||
webView?.evaluateJavaScript("updateFind(\"\(encoded)\")") {
|
||||
(result, error) in
|
||||
guard error == nil,
|
||||
@ -845,21 +843,21 @@ extension WebViewController {
|
||||
let findState = try? JSONDecoder().decode(FindInArticleState.self, from: rawData) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
completionHandler(findState)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func endSearch() {
|
||||
webView?.evaluateJavaScript("endFind()")
|
||||
}
|
||||
|
||||
|
||||
func selectNextSearchResult() {
|
||||
webView?.evaluateJavaScript("selectNextResult()")
|
||||
}
|
||||
|
||||
|
||||
func selectPreviousSearchResult() {
|
||||
webView?.evaluateJavaScript("selectPreviousResult()")
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,16 +10,16 @@ import Foundation
|
||||
import WebKit
|
||||
|
||||
class WrapperScriptMessageHandler: NSObject, WKScriptMessageHandler {
|
||||
|
||||
|
||||
// We need to wrap a message handler to prevent a circlular reference
|
||||
private weak var handler: WKScriptMessageHandler?
|
||||
|
||||
|
||||
init(_ handler: WKScriptMessageHandler) {
|
||||
self.handler = handler
|
||||
}
|
||||
|
||||
|
||||
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
|
||||
handler?.userContentController(userContentController, didReceive: message)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,25 +9,25 @@
|
||||
import UIKit
|
||||
|
||||
class ArticleActivityItemSource: NSObject, UIActivityItemSource {
|
||||
|
||||
|
||||
private let url: URL
|
||||
private let subject: String?
|
||||
|
||||
|
||||
init(url: URL, subject: String?) {
|
||||
self.url = url
|
||||
self.subject = subject
|
||||
}
|
||||
|
||||
func activityViewControllerPlaceholderItem(_ : UIActivityViewController) -> Any {
|
||||
|
||||
func activityViewControllerPlaceholderItem(_: UIActivityViewController) -> Any {
|
||||
return url
|
||||
}
|
||||
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, itemForActivityType activityType: UIActivity.ActivityType?) -> Any? {
|
||||
return url
|
||||
}
|
||||
|
||||
|
||||
func activityViewController(_ activityViewController: UIActivityViewController, subjectForActivityType activityType: UIActivity.ActivityType?) -> String {
|
||||
return subject ?? ""
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ struct ErrorHandler {
|
||||
|
||||
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
|
||||
if UIApplication.shared.applicationState == .active {
|
||||
viewController?.presentError(error)
|
||||
@ -23,7 +23,7 @@ struct ErrorHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static func log(_ error: Error) {
|
||||
os_log(.error, log: self.log, "%@", error.localizedDescription)
|
||||
}
|
||||
|
@ -10,7 +10,7 @@ import UIKit
|
||||
|
||||
final class IconView: UIView {
|
||||
|
||||
var iconImage: IconImage? = nil {
|
||||
var iconImage: IconImage? {
|
||||
didSet {
|
||||
guard iconImage !== oldValue else {
|
||||
return
|
||||
@ -19,8 +19,7 @@ final class IconView: UIView {
|
||||
if traitCollection.userInterfaceStyle == .dark {
|
||||
let isDark = iconImage?.isDark ?? false
|
||||
isDiscernable = !isDark
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
let isBright = iconImage?.isBright ?? false
|
||||
isDiscernable = !isBright
|
||||
}
|
||||
@ -45,11 +44,11 @@ final class IconView: UIView {
|
||||
private var isSymbolImage: Bool {
|
||||
return iconImage?.isSymbol ?? false
|
||||
}
|
||||
|
||||
|
||||
private var isBackgroundSuppressed: Bool {
|
||||
return iconImage?.isBackgroundSuppressed ?? false
|
||||
}
|
||||
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
commonInit()
|
||||
@ -59,7 +58,7 @@ final class IconView: UIView {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
convenience init() {
|
||||
self.init(frame: .zero)
|
||||
}
|
||||
@ -96,8 +95,7 @@ private extension IconView {
|
||||
}
|
||||
let offset = floor((viewSize.height - imageSize.height) / 2.0)
|
||||
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 width = imageSize.width * factor
|
||||
let originX = floor((viewSize.width - width) / 2.0)
|
||||
|
@ -21,23 +21,23 @@ class AccountInspectorViewController: UITableViewController {
|
||||
|
||||
var isModal = false
|
||||
weak var account: Account?
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
guard let account = account else { return }
|
||||
|
||||
|
||||
nameTextField.placeholder = account.defaultName
|
||||
nameTextField.text = account.name
|
||||
nameTextField.delegate = self
|
||||
activeSwitch.isOn = account.isActive
|
||||
|
||||
|
||||
navigationItem.title = account.nameForDisplay
|
||||
|
||||
|
||||
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 {
|
||||
limitationsAndSolutionsButton.isHidden = true
|
||||
}
|
||||
@ -46,11 +46,11 @@ class AccountInspectorViewController: UITableViewController {
|
||||
let doneBarButtonItem = UIBarButtonItem(barButtonSystemItem: .done, target: self, action: #selector(done))
|
||||
navigationItem.leftBarButtonItem = doneBarButtonItem
|
||||
}
|
||||
|
||||
|
||||
tableView.register(ImageHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
|
||||
}
|
||||
|
||||
|
||||
override func viewWillDisappear(_ animated: Bool) {
|
||||
account?.name = nameTextField.text
|
||||
account?.isActive = activeSwitch.isOn
|
||||
@ -59,7 +59,7 @@ class AccountInspectorViewController: UITableViewController {
|
||||
@objc func done() {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func credentials(_ sender: Any) {
|
||||
guard let account = account else { return }
|
||||
switch account.type {
|
||||
@ -86,12 +86,12 @@ class AccountInspectorViewController: UITableViewController {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func deleteAccount(_ sender: Any) {
|
||||
guard let account = account else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let title = NSLocalizedString("Remove Account", comment: "Remove Account")
|
||||
let message: String = {
|
||||
switch account.type {
|
||||
@ -105,9 +105,9 @@ class AccountInspectorViewController: UITableViewController {
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel)
|
||||
alertController.addAction(cancelAction)
|
||||
|
||||
|
||||
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 }
|
||||
AccountManager.shared.deleteAccount(account)
|
||||
if self.isModal {
|
||||
@ -118,7 +118,7 @@ class AccountInspectorViewController: UITableViewController {
|
||||
}
|
||||
alertController.addAction(markAction)
|
||||
alertController.preferredAction = markAction
|
||||
|
||||
|
||||
present(alertController, animated: true)
|
||||
}
|
||||
|
||||
@ -132,7 +132,7 @@ class AccountInspectorViewController: UITableViewController {
|
||||
// MARK: Table View
|
||||
|
||||
extension AccountInspectorViewController {
|
||||
|
||||
|
||||
var hidesCredentialsSection: Bool {
|
||||
guard let account = account else {
|
||||
return true
|
||||
@ -147,7 +147,7 @@ extension AccountInspectorViewController {
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
guard let account = account else { return 0 }
|
||||
|
||||
|
||||
if account == AccountManager.shared.defaultAccount {
|
||||
return 1
|
||||
} else if hidesCredentialsSection {
|
||||
@ -156,11 +156,11 @@ extension AccountInspectorViewController {
|
||||
return super.numberOfSections(in: tableView)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: section)
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
guard let account = account else { return nil }
|
||||
|
||||
@ -172,16 +172,16 @@ extension AccountInspectorViewController {
|
||||
return super.tableView(tableView, viewForHeaderInSection: section)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell: UITableViewCell
|
||||
|
||||
|
||||
if indexPath.section == 1, hidesCredentialsSection {
|
||||
cell = super.tableView(tableView, cellForRowAt: IndexPath(row: 0, section: 2))
|
||||
} else {
|
||||
cell = super.tableView(tableView, cellForRowAt: indexPath)
|
||||
}
|
||||
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
@ -191,17 +191,17 @@ extension AccountInspectorViewController {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: UITextFieldDelegate
|
||||
|
||||
extension AccountInspectorViewController: UITextFieldDelegate {
|
||||
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
|
@ -12,52 +12,52 @@ import SafariServices
|
||||
import UserNotifications
|
||||
|
||||
class FeedInspectorViewController: UITableViewController {
|
||||
|
||||
|
||||
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 500.0)
|
||||
|
||||
|
||||
var feed: Feed!
|
||||
@IBOutlet weak var nameTextField: UITextField!
|
||||
@IBOutlet weak var notifyAboutNewArticlesSwitch: UISwitch!
|
||||
@IBOutlet weak var alwaysShowReaderViewSwitch: UISwitch!
|
||||
@IBOutlet weak var homePageLabel: InteractiveLabel!
|
||||
@IBOutlet weak var feedURLLabel: InteractiveLabel!
|
||||
|
||||
|
||||
private var headerView: InspectorIconHeaderView?
|
||||
private var iconImage: IconImage? {
|
||||
return IconImageCache.shared.imageForFeed(feed)
|
||||
}
|
||||
|
||||
|
||||
private let homePageIndexPath = IndexPath(row: 0, section: 1)
|
||||
|
||||
|
||||
private var shouldHideHomePageSection: Bool {
|
||||
return feed.homePageURL == nil
|
||||
}
|
||||
|
||||
|
||||
private var userNotificationSettings: UNNotificationSettings?
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
tableView.register(InspectorIconHeaderView.self, forHeaderFooterViewReuseIdentifier: "SectionHeader")
|
||||
|
||||
|
||||
navigationItem.title = feed.nameForDisplay
|
||||
nameTextField.text = feed.nameForDisplay
|
||||
|
||||
|
||||
notifyAboutNewArticlesSwitch.setOn(feed.isNotifyAboutNewArticles ?? false, animated: false)
|
||||
|
||||
|
||||
alwaysShowReaderViewSwitch.setOn(feed.isArticleExtractorAlwaysOn ?? false, animated: false)
|
||||
|
||||
homePageLabel.text = feed.homePageURL
|
||||
feedURLLabel.text = feed.url
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(feedIconDidBecomeAvailable(_:)), name: .feedIconDidBecomeAvailable, object: nil)
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(updateNotificationSettings), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
updateNotificationSettings()
|
||||
}
|
||||
|
||||
|
||||
override func viewDidDisappear(_ animated: Bool) {
|
||||
if nameTextField.text != feed.nameForDisplay {
|
||||
let nameText = nameTextField.text ?? ""
|
||||
@ -65,12 +65,12 @@ class FeedInspectorViewController: UITableViewController {
|
||||
feed.rename(to: newName) { _ in }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Notifications
|
||||
@objc func feedIconDidBecomeAvailable(_ notification: Notification) {
|
||||
headerView?.iconView.iconImage = iconImage
|
||||
}
|
||||
|
||||
|
||||
@IBAction func notifyAboutNewArticlesChanged(_ sender: Any) {
|
||||
guard let settings = userNotificationSettings else {
|
||||
notifyAboutNewArticlesSwitch.isOn = !notifyAboutNewArticlesSwitch.isOn
|
||||
@ -82,7 +82,7 @@ class FeedInspectorViewController: UITableViewController {
|
||||
} else if settings.authorizationStatus == .authorized {
|
||||
feed.isNotifyAboutNewArticles = notifyAboutNewArticlesSwitch.isOn
|
||||
} else {
|
||||
UNUserNotificationCenter.current().requestAuthorization(options:[.badge, .sound, .alert]) { (granted, error) in
|
||||
UNUserNotificationCenter.current().requestAuthorization(options: [.badge, .sound, .alert]) { (granted, _) in
|
||||
self.updateNotificationSettings()
|
||||
if granted {
|
||||
DispatchQueue.main.async {
|
||||
@ -97,22 +97,22 @@ class FeedInspectorViewController: UITableViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func alwaysShowReaderViewChanged(_ sender: Any) {
|
||||
feed.isArticleExtractorAlwaysOn = alwaysShowReaderViewSwitch.isOn
|
||||
}
|
||||
|
||||
|
||||
@IBAction func done(_ sender: Any) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
|
||||
/// Returns a new indexPath, taking into consideration any
|
||||
/// conditions that may require the tableView to be
|
||||
/// displayed differently than what is setup in the storyboard.
|
||||
private func shift(_ indexPath: IndexPath) -> IndexPath {
|
||||
return IndexPath(row: indexPath.row, section: shift(indexPath.section))
|
||||
}
|
||||
|
||||
|
||||
/// Returns a new section, taking into consideration any
|
||||
/// conditions that may require the tableView to be
|
||||
/// displayed differently than what is setup in the storyboard.
|
||||
@ -123,7 +123,6 @@ class FeedInspectorViewController: UITableViewController {
|
||||
return section
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: Table View
|
||||
@ -134,15 +133,15 @@ extension FeedInspectorViewController {
|
||||
let numberOfSections = super.numberOfSections(in: tableView)
|
||||
return shouldHideHomePageSection ? numberOfSections - 1 : numberOfSections
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return super.tableView(tableView, numberOfRowsInSection: shift(section))
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat {
|
||||
return section == 0 ? ImageHeaderView.rowHeight : super.tableView(tableView, heightForHeaderInSection: shift(section))
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = super.tableView(tableView, cellForRowAt: shift(indexPath))
|
||||
if indexPath.section == 0 && indexPath.row == 1 {
|
||||
@ -154,11 +153,11 @@ extension FeedInspectorViewController {
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
super.tableView(tableView, titleForHeaderInSection: shift(section))
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? {
|
||||
if shift(section) == 0 {
|
||||
headerView = tableView.dequeueReusableHeaderFooterView(withIdentifier: "SectionHeader") as? InspectorIconHeaderView
|
||||
@ -168,12 +167,12 @@ extension FeedInspectorViewController {
|
||||
return super.tableView(tableView, viewForHeaderInSection: shift(section))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
if shift(indexPath) == homePageIndexPath,
|
||||
let homePageUrlString = feed.homePageURL,
|
||||
let homePageUrl = URL(string: homePageUrlString) {
|
||||
|
||||
|
||||
let safari = SFSafariViewController(url: homePageUrl)
|
||||
safari.modalPresentationStyle = .pageSheet
|
||||
present(safari, animated: true) {
|
||||
@ -181,24 +180,24 @@ extension FeedInspectorViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: UITextFieldDelegate
|
||||
|
||||
extension FeedInspectorViewController: UITextFieldDelegate {
|
||||
|
||||
|
||||
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
|
||||
textField.resignFirstResponder()
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: UNUserNotificationCenter
|
||||
|
||||
extension FeedInspectorViewController {
|
||||
|
||||
|
||||
@objc
|
||||
func updateNotificationSettings() {
|
||||
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
|
||||
@ -210,12 +209,12 @@ extension FeedInspectorViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func notificationUpdateErrorAlert() -> UIAlertController {
|
||||
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)
|
||||
let openSettings = UIAlertAction(title: NSLocalizedString("Open Settings", comment: "Open Settings"), style: .default) { (action) in
|
||||
UIApplication.shared.open(URL(string: UIApplication.openSettingsURLString)!, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly : false], completionHandler: nil)
|
||||
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)
|
||||
}
|
||||
let dismiss = UIAlertAction(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil)
|
||||
alert.addAction(openSettings)
|
||||
@ -223,5 +222,5 @@ extension FeedInspectorViewController {
|
||||
alert.preferredAction = openSettings
|
||||
return alert
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -11,17 +11,17 @@ import UIKit
|
||||
class InspectorIconHeaderView: UITableViewHeaderFooterView {
|
||||
|
||||
var iconView = IconView()
|
||||
|
||||
|
||||
override init(reuseIdentifier: String?) {
|
||||
super.init(reuseIdentifier: reuseIdentifier)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
func commonInit() {
|
||||
addSubview(iconView)
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import Intents
|
||||
|
||||
class IntentHandler: INExtension {
|
||||
|
||||
|
||||
override func handler(for intent: INIntent) -> Any {
|
||||
switch intent {
|
||||
case is AddWebFeedIntent:
|
||||
@ -18,5 +18,5 @@ class IntentHandler: INExtension {
|
||||
fatalError("Unhandled intent type: \(intent)")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -16,16 +16,16 @@ enum KeyboardType: String {
|
||||
}
|
||||
|
||||
class KeyboardManager {
|
||||
|
||||
|
||||
private(set) var _keyCommands: [UIKeyCommand]
|
||||
var keyCommands: [UIKeyCommand] {
|
||||
guard !UIResponder.isFirstResponderTextField else { return [UIKeyCommand]() }
|
||||
return _keyCommands
|
||||
}
|
||||
|
||||
|
||||
init(type: KeyboardType) {
|
||||
_keyCommands = KeyboardManager.globalAuxilaryKeyCommands()
|
||||
|
||||
|
||||
switch type {
|
||||
case .sidebar:
|
||||
_keyCommands.append(contentsOf: KeyboardManager.hardcodeFeedKeyCommands())
|
||||
@ -34,7 +34,7 @@ class KeyboardManager {
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
|
||||
let globalFile = Bundle.main.path(forResource: KeyboardType.global.rawValue, ofType: "plist")!
|
||||
let globalEntries = NSArray(contentsOfFile: globalFile)! as! [[String: Any]]
|
||||
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 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 {
|
||||
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)
|
||||
@ -54,7 +54,7 @@ class KeyboardManager {
|
||||
}
|
||||
|
||||
private extension KeyboardManager {
|
||||
|
||||
|
||||
static func createKeyCommand(keyEntry: [String: Any]) -> UIKeyCommand? {
|
||||
guard let input = createKeyCommandInput(keyEntry: keyEntry) else { return nil }
|
||||
let modifiers = createKeyModifierFlags(keyEntry: keyEntry)
|
||||
@ -71,8 +71,8 @@ private extension KeyboardManager {
|
||||
|
||||
static func createKeyCommandInput(keyEntry: [String: Any]) -> String? {
|
||||
guard let key = keyEntry["key"] as? String else { return nil }
|
||||
|
||||
switch(key) {
|
||||
|
||||
switch key {
|
||||
case "[space]":
|
||||
return "\u{0020}"
|
||||
case "[uparrow]":
|
||||
@ -96,34 +96,34 @@ private extension KeyboardManager {
|
||||
default:
|
||||
return key
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
static func createKeyModifierFlags(keyEntry: [String: Any]) -> UIKeyModifierFlags {
|
||||
var flags = UIKeyModifierFlags()
|
||||
|
||||
|
||||
if keyEntry["shiftModifier"] as? Bool ?? false {
|
||||
flags.insert(.shift)
|
||||
}
|
||||
|
||||
|
||||
if keyEntry["optionModifier"] as? Bool ?? false {
|
||||
flags.insert(.alternate)
|
||||
}
|
||||
|
||||
|
||||
if keyEntry["commandModifier"] as? Bool ?? false {
|
||||
flags.insert(.command)
|
||||
}
|
||||
|
||||
|
||||
if keyEntry["controlModifier"] as? Bool ?? false {
|
||||
flags.insert(.control)
|
||||
}
|
||||
|
||||
return flags
|
||||
}
|
||||
|
||||
|
||||
static func globalAuxilaryKeyCommands() -> [UIKeyCommand] {
|
||||
var keys = [UIKeyCommand]()
|
||||
|
||||
|
||||
let addNewFeedTitle = NSLocalizedString("New Feed", comment: "New Feed")
|
||||
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")
|
||||
keys.append(KeyboardManager.createKeyCommand(title: gotoSettings, action: "goToSettings:", input: ",", modifiers: [.command]))
|
||||
|
||||
|
||||
let articleSearchTitle = NSLocalizedString("Article Search", comment: "Article Search")
|
||||
keys.append(KeyboardManager.createKeyCommand(title: articleSearchTitle, action: "articleSearch:", input: "f", modifiers: [.command, .alternate]))
|
||||
|
||||
@ -165,7 +165,7 @@ private extension KeyboardManager {
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
|
||||
static func hardcodeFeedKeyCommands() -> [UIKeyCommand] {
|
||||
var keys = [UIKeyCommand]()
|
||||
|
||||
@ -174,16 +174,16 @@ private extension KeyboardManager {
|
||||
|
||||
let nextDownTitle = NSLocalizedString("Select Next Down", comment: "Select Next Down")
|
||||
keys.append(KeyboardManager.createKeyCommand(title: nextDownTitle, action: "selectNextDown:", input: UIKeyCommand.inputDownArrow, modifiers: []))
|
||||
|
||||
|
||||
let getFeedInfo = NSLocalizedString("Get Feed Info", comment: "Get Feed Info")
|
||||
keys.append(KeyboardManager.createKeyCommand(title: getFeedInfo, action: "showFeedInspector:", input: "i", modifiers: .command))
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
|
||||
static func hardcodeArticleKeyCommands() -> [UIKeyCommand] {
|
||||
var keys = [UIKeyCommand]()
|
||||
|
||||
|
||||
let openInBrowserTitle = NSLocalizedString("Open In Browser", comment: "Open In Browser")
|
||||
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")
|
||||
keys.append(KeyboardManager.createKeyCommand(title: toggleStarredTitle, action: "toggleStarred:", input: "l", modifiers: [.command, .shift]))
|
||||
|
||||
|
||||
let findInArticleTitle = NSLocalizedString("Find in Article", comment: "Find in Article")
|
||||
keys.append(KeyboardManager.createKeyCommand(title: findInArticleTitle, action: "beginFind:", input: "f", modifiers: [.command]))
|
||||
|
||||
@ -213,5 +213,5 @@ private extension KeyboardManager {
|
||||
|
||||
return keys
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -11,13 +11,13 @@ import Foundation
|
||||
class MainFeedRowIdentifier: NSObject, NSCopying {
|
||||
|
||||
var indexPath: IndexPath
|
||||
|
||||
|
||||
init(indexPath: IndexPath) {
|
||||
self.indexPath = indexPath
|
||||
}
|
||||
|
||||
|
||||
func copy(with zone: NSZone? = nil) -> Any {
|
||||
return self
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ protocol MainFeedTableViewCellDelegate: AnyObject {
|
||||
func mainFeedTableViewCellDisclosureDidToggle(_ sender: MainFeedTableViewCell, expanding: Bool)
|
||||
}
|
||||
|
||||
class MainFeedTableViewCell : VibrantTableViewCell {
|
||||
class MainFeedTableViewCell: VibrantTableViewCell {
|
||||
|
||||
weak var delegate: MainFeedTableViewCellDelegate?
|
||||
|
||||
@ -44,7 +44,7 @@ class MainFeedTableViewCell : VibrantTableViewCell {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isSeparatorShown = true {
|
||||
didSet {
|
||||
if isSeparatorShown != oldValue {
|
||||
@ -56,7 +56,7 @@ class MainFeedTableViewCell : VibrantTableViewCell {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var unreadCount: Int {
|
||||
get {
|
||||
return unreadCountView.unreadCount
|
||||
@ -100,17 +100,17 @@ class MainFeedTableViewCell : VibrantTableViewCell {
|
||||
view.alpha = 0.5
|
||||
return view
|
||||
}()
|
||||
|
||||
|
||||
private var isDisclosureExpanded = false
|
||||
private var disclosureButton: UIButton?
|
||||
private var unreadCountView = MainFeedUnreadCountView(frame: CGRect.zero)
|
||||
private var isShowingEditControl = false
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
func setDisclosure(isExpanded: Bool, animated: Bool) {
|
||||
isDisclosureExpanded = isExpanded
|
||||
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?.imageView?.transform = CGAffineTransform(rotationAngle: 1.570796)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func applyThemeProperties() {
|
||||
super.applyThemeProperties()
|
||||
}
|
||||
|
||||
override func willTransition(to state: UITableViewCell.StateMask) {
|
||||
super.willTransition(to: state)
|
||||
isShowingEditControl = state.contains(.showingEditControl)
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
return CGSize(width: bounds.width, height: layout.height)
|
||||
}
|
||||
|
||||
|
||||
override func layoutSubviews() {
|
||||
super.layoutSubviews()
|
||||
let layout = MainFeedTableViewCellLayout(cellWidth: bounds.size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView, showingEditingControl: isShowingEditControl, indent: indentationLevel == 1, shouldShowDisclosure: isDisclosureAvailable)
|
||||
layoutWith(layout)
|
||||
}
|
||||
|
||||
|
||||
@objc func buttonPressed(_ sender: UIButton) {
|
||||
if isDisclosureAvailable {
|
||||
setDisclosure(isExpanded: !isDisclosureExpanded, animated: true)
|
||||
delegate?.mainFeedTableViewCellDisclosureDidToggle(self, expanding: isDisclosureExpanded)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func updateVibrancy(animated: Bool) {
|
||||
super.updateVibrancy(animated: animated)
|
||||
|
||||
|
||||
let iconTintColor: UIColor
|
||||
if isHighlighted || isSelected {
|
||||
iconTintColor = AppAssets.vibrantTextColor
|
||||
@ -166,7 +162,7 @@ class MainFeedTableViewCell : VibrantTableViewCell {
|
||||
iconTintColor = AppAssets.secondaryAccentColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: Self.duration) {
|
||||
self.iconView.tintColor = iconTintColor
|
||||
@ -174,10 +170,10 @@ class MainFeedTableViewCell : VibrantTableViewCell {
|
||||
} else {
|
||||
self.iconView.tintColor = iconTintColor
|
||||
}
|
||||
|
||||
|
||||
updateLabelVibrancy(titleView, color: labelColor, animated: animated)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private extension MainFeedTableViewCell {
|
||||
@ -200,7 +196,7 @@ private extension MainFeedTableViewCell {
|
||||
disclosureButton?.addInteraction(UIPointerInteraction())
|
||||
addSubviewAtInit(disclosureButton!)
|
||||
}
|
||||
|
||||
|
||||
func addSubviewAtInit(_ view: UIView) {
|
||||
addSubview(view)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
@ -220,11 +216,11 @@ private extension MainFeedTableViewCell {
|
||||
view.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func showView(_ view: UIView) {
|
||||
if view.isHidden {
|
||||
view.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ struct MainFeedTableViewCellLayout {
|
||||
private static let verticalPadding = CGFloat(integerLiteral: 11)
|
||||
|
||||
private static let minRowHeight = CGFloat(integerLiteral: 44)
|
||||
|
||||
|
||||
static let faviconCornerRadius = CGFloat(integerLiteral: 2)
|
||||
|
||||
let faviconRect: CGRect
|
||||
@ -29,9 +29,9 @@ struct MainFeedTableViewCellLayout {
|
||||
let unreadCountRect: CGRect
|
||||
let disclosureButtonRect: CGRect
|
||||
let separatorRect: CGRect
|
||||
|
||||
|
||||
let height: CGFloat
|
||||
|
||||
|
||||
init(cellWidth: CGFloat, insets: UIEdgeInsets, label: UILabel, unreadCountView: MainFeedUnreadCountView, showingEditingControl: Bool, indent: Bool, shouldShowDisclosure: Bool) {
|
||||
|
||||
var initialIndent = insets.left
|
||||
@ -39,7 +39,7 @@ struct MainFeedTableViewCellLayout {
|
||||
initialIndent += MainFeedTableViewCellLayout.indentWidth
|
||||
}
|
||||
let bounds = CGRect(x: initialIndent, y: 0.0, width: floor(cellWidth - initialIndent - insets.right), height: 0.0)
|
||||
|
||||
|
||||
// Disclosure Button
|
||||
var rDisclosure = CGRect.zero
|
||||
if shouldShowDisclosure {
|
||||
@ -66,7 +66,7 @@ struct MainFeedTableViewCellLayout {
|
||||
rUnread.size = unreadCountSize
|
||||
rUnread.origin.x = bounds.maxX - (MainFeedTableViewCellLayout.unreadCountMarginRight + unreadCountSize.width)
|
||||
}
|
||||
|
||||
|
||||
// Title
|
||||
var rLabelx = insets.left + MainFeedTableViewCellLayout.disclosureButtonSize.width
|
||||
if !shouldShowDisclosure {
|
||||
@ -80,7 +80,7 @@ struct MainFeedTableViewCellLayout {
|
||||
} else {
|
||||
labelWidth = cellWidth - (rLabelx + MainFeedTableViewCellLayout.labelMarginRight)
|
||||
}
|
||||
|
||||
|
||||
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.
|
||||
@ -99,7 +99,7 @@ struct MainFeedTableViewCellLayout {
|
||||
}
|
||||
|
||||
var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
|
||||
|
||||
|
||||
// Determine cell height
|
||||
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MainFeedTableViewCellLayout.verticalPadding)
|
||||
let maxGraphicsHeight = [rFavicon, rUnread, rDisclosure].maxY()
|
||||
@ -107,7 +107,7 @@ struct MainFeedTableViewCellLayout {
|
||||
if cellHeight < MainFeedTableViewCellLayout.minRowHeight {
|
||||
cellHeight = MainFeedTableViewCellLayout.minRowHeight
|
||||
}
|
||||
|
||||
|
||||
// Center in Cell
|
||||
let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight)
|
||||
if !unreadCountIsHidden {
|
||||
@ -126,16 +126,16 @@ struct MainFeedTableViewCellLayout {
|
||||
// Separator Insets
|
||||
let separatorInset = MainFeedTableViewCellLayout.disclosureButtonSize.width
|
||||
separatorRect = CGRect(x: separatorInset, y: cellHeight - 0.5, width: cellWidth - separatorInset, height: 0.5)
|
||||
|
||||
|
||||
// Assign the properties
|
||||
self.height = cellHeight
|
||||
self.faviconRect = rFavicon
|
||||
self.unreadCountRect = rUnread
|
||||
self.disclosureButtonRect = rDisclosure
|
||||
self.titleRect = rLabel
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Ideally this will be implemented in RSCore (see RSGeometry)
|
||||
static func centerVertically(_ originalRect: CGRect, _ containerRect: CGRect) -> CGRect {
|
||||
var result = originalRect
|
||||
@ -144,5 +144,5 @@ struct MainFeedTableViewCellLayout {
|
||||
result.size = originalRect.size
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -15,7 +15,7 @@ protocol MainFeedTableViewSectionHeaderDelegate {
|
||||
class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
|
||||
|
||||
var delegate: MainFeedTableViewSectionHeaderDelegate?
|
||||
|
||||
|
||||
override var accessibilityLabel: String? {
|
||||
set {}
|
||||
get {
|
||||
@ -37,7 +37,7 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
|
||||
return NSLocalizedString("Collapsed", comment: "Disclosure button collapsed state for accessibility")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var unreadCount: Int {
|
||||
get {
|
||||
return unreadCountView.unreadCount
|
||||
@ -50,7 +50,7 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var name: String {
|
||||
get {
|
||||
return titleView.text ?? ""
|
||||
@ -62,16 +62,16 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var disclosureExpanded = false {
|
||||
didSet {
|
||||
updateExpandedState(animate: true)
|
||||
updateUnreadCountView()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var isLastSection = false
|
||||
|
||||
|
||||
private let titleView: UILabel = {
|
||||
let label = NonIntrinsicLabel()
|
||||
label.numberOfLines = 0
|
||||
@ -80,7 +80,7 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
|
||||
label.font = .preferredFont(forTextStyle: .body)
|
||||
return label
|
||||
}()
|
||||
|
||||
|
||||
private let unreadCountView = MainFeedUnreadCountView(frame: CGRect.zero)
|
||||
|
||||
private lazy var disclosureButton: UIButton = {
|
||||
@ -98,27 +98,27 @@ class MainFeedTableViewSectionHeader: UITableViewHeaderFooterView {
|
||||
view.backgroundColor = UIColor.separator
|
||||
return view
|
||||
}()
|
||||
|
||||
|
||||
private let bottomSeparatorView: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = UIColor.separator
|
||||
return view
|
||||
}()
|
||||
|
||||
|
||||
override init(reuseIdentifier: String?) {
|
||||
super.init(reuseIdentifier: reuseIdentifier)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
let layout = MainFeedTableViewSectionHeaderLayout(cellWidth: size.width, insets: safeAreaInsets, label: titleView, unreadCountView: unreadCountView)
|
||||
return CGSize(width: bounds.width, height: layout.height)
|
||||
|
||||
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
@ -137,7 +137,7 @@ private extension MainFeedTableViewSectionHeader {
|
||||
@objc func toggleDisclosure() {
|
||||
delegate?.mainFeedTableViewSectionHeaderDisclosureDidToggle(self)
|
||||
}
|
||||
|
||||
|
||||
func commonInit() {
|
||||
addSubviewAtInit(unreadCountView)
|
||||
addSubviewAtInit(titleView)
|
||||
@ -147,14 +147,14 @@ private extension MainFeedTableViewSectionHeader {
|
||||
addSubviewAtInit(topSeparatorView)
|
||||
addSubviewAtInit(bottomSeparatorView)
|
||||
}
|
||||
|
||||
|
||||
func updateExpandedState(animate: Bool) {
|
||||
if !isLastSection && self.disclosureExpanded {
|
||||
self.bottomSeparatorView.isHidden = false
|
||||
}
|
||||
|
||||
|
||||
let duration = animate ? 0.3 : 0.0
|
||||
|
||||
|
||||
UIView.animate(
|
||||
withDuration: duration,
|
||||
animations: {
|
||||
@ -169,7 +169,7 @@ private extension MainFeedTableViewSectionHeader {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
func updateUnreadCountView() {
|
||||
if !disclosureExpanded && unreadCount > 0 {
|
||||
UIView.animate(withDuration: 0.3) {
|
||||
@ -186,7 +186,7 @@ private extension MainFeedTableViewSectionHeader {
|
||||
contentView.addSubview(view)
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
}
|
||||
|
||||
|
||||
func layoutWith(_ layout: MainFeedTableViewSectionHeaderLayout) {
|
||||
titleView.setFrameIfNotEqual(layout.titleRect)
|
||||
unreadCountView.setFrameIfNotEqual(layout.unreadCountRect)
|
||||
@ -198,14 +198,14 @@ private extension MainFeedTableViewSectionHeader {
|
||||
|
||||
let top = CGRect(x: x, y: 0, width: width, height: height)
|
||||
topSeparatorView.setFrameIfNotEqual(top)
|
||||
|
||||
|
||||
let bottom = CGRect(x: x, y: frame.height - height, width: width, height: height)
|
||||
bottomSeparatorView.setFrameIfNotEqual(bottom)
|
||||
}
|
||||
|
||||
|
||||
func addBackgroundView() {
|
||||
self.backgroundView = UIView(frame: self.bounds)
|
||||
self.backgroundView?.backgroundColor = AppAssets.sectionHeaderColor
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -17,17 +17,17 @@ struct MainFeedTableViewSectionHeaderLayout {
|
||||
private static let verticalPadding = CGFloat(integerLiteral: 11)
|
||||
|
||||
private static let minRowHeight = CGFloat(integerLiteral: 44)
|
||||
|
||||
|
||||
let titleRect: CGRect
|
||||
let unreadCountRect: CGRect
|
||||
let disclosureButtonRect: CGRect
|
||||
|
||||
|
||||
let height: CGFloat
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
// Disclosure Button
|
||||
var rDisclosure = CGRect.zero
|
||||
rDisclosure.size = MainFeedTableViewSectionHeaderLayout.disclosureButtonSize
|
||||
@ -42,7 +42,7 @@ struct MainFeedTableViewSectionHeaderLayout {
|
||||
rUnread.size = unreadCountSize
|
||||
rUnread.origin.x = bounds.maxX - (MainFeedTableViewSectionHeaderLayout.unreadCountMarginRight + unreadCountSize.width)
|
||||
}
|
||||
|
||||
|
||||
// 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.
|
||||
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)))
|
||||
var rLabel = CGRect(x: rLabelx, y: rLabely, width: labelWidth, height: labelSizeInfo.size.height)
|
||||
|
||||
|
||||
// Determine cell height
|
||||
let paddedLabelHeight = rLabel.maxY + UIFontMetrics.default.scaledValue(for: MainFeedTableViewSectionHeaderLayout.verticalPadding)
|
||||
let maxGraphicsHeight = [rUnread, rDisclosure].maxY()
|
||||
@ -66,7 +66,7 @@ struct MainFeedTableViewSectionHeaderLayout {
|
||||
if cellHeight < MainFeedTableViewSectionHeaderLayout.minRowHeight {
|
||||
cellHeight = MainFeedTableViewSectionHeaderLayout.minRowHeight
|
||||
}
|
||||
|
||||
|
||||
// Center in Cell
|
||||
let newBounds = CGRect(x: bounds.origin.x, y: bounds.origin.y, width: bounds.width, height: cellHeight)
|
||||
if !unreadCountIsHidden {
|
||||
@ -78,13 +78,13 @@ struct MainFeedTableViewSectionHeaderLayout {
|
||||
if cellHeight == MainFeedTableViewSectionHeaderLayout.minRowHeight {
|
||||
rLabel = MainFeedTableViewCellLayout.centerVertically(rLabel, newBounds)
|
||||
}
|
||||
|
||||
|
||||
// Assign the properties
|
||||
self.height = cellHeight
|
||||
self.unreadCountRect = rUnread
|
||||
self.disclosureButtonRect = rDisclosure
|
||||
self.titleRect = rLabel
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,18 +8,18 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
class MainFeedUnreadCountView : UIView {
|
||||
class MainFeedUnreadCountView: UIView {
|
||||
|
||||
var padding: UIEdgeInsets {
|
||||
return UIEdgeInsets(top: 1.0, left: 9.0, bottom: 1.0, right: 9.0)
|
||||
}
|
||||
|
||||
|
||||
let cornerRadius = 8.0
|
||||
let bgColor = AppAssets.controlBackgroundColor
|
||||
var textColor: UIColor {
|
||||
return UIColor.white
|
||||
}
|
||||
|
||||
|
||||
var textAttributes: [NSAttributedString.Key: AnyObject] {
|
||||
let textFont = UIFont.preferredFont(forTextStyle: .caption1).bold()
|
||||
return [NSAttributedString.Key.foregroundColor: textColor, NSAttributedString.Key.font: textFont, NSAttributedString.Key.kern: NSNull()]
|
||||
@ -33,7 +33,7 @@ class MainFeedUnreadCountView : UIView {
|
||||
setNeedsDisplay()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var unreadCountString: String {
|
||||
return unreadCount < 1 ? "" : "\(unreadCount)"
|
||||
}
|
||||
@ -45,18 +45,18 @@ class MainFeedUnreadCountView : UIView {
|
||||
super.init(frame: frame)
|
||||
self.isOpaque = false
|
||||
}
|
||||
|
||||
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
super.init(coder: aDecoder)
|
||||
self.isOpaque = false
|
||||
}
|
||||
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
textSizeCache = [Int: CGSize]()
|
||||
contentSizeIsValid = false
|
||||
setNeedsDisplay()
|
||||
}
|
||||
|
||||
|
||||
var contentSize: CGSize {
|
||||
if !contentSizeIsValid {
|
||||
var size = CGSize.zero
|
||||
@ -70,7 +70,7 @@ class MainFeedUnreadCountView : UIView {
|
||||
}
|
||||
return _contentSize
|
||||
}
|
||||
|
||||
|
||||
// Prevent autolayout from messing around with our frame settings
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
||||
@ -92,7 +92,7 @@ class MainFeedUnreadCountView : UIView {
|
||||
|
||||
textSizeCache[unreadCount] = size
|
||||
return size
|
||||
|
||||
|
||||
}
|
||||
|
||||
func textRect() -> CGRect {
|
||||
@ -103,7 +103,7 @@ class MainFeedUnreadCountView : UIView {
|
||||
r.origin.x = (bounds.maxX - padding.right) - r.size.width
|
||||
r.origin.y = padding.top
|
||||
return r
|
||||
|
||||
|
||||
}
|
||||
|
||||
override func draw(_ dirtyRect: CGRect) {
|
||||
@ -116,8 +116,7 @@ class MainFeedUnreadCountView : UIView {
|
||||
if unreadCount > 0 {
|
||||
unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -12,25 +12,25 @@ import Account
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
extension MainFeedViewController: UITableViewDragDelegate {
|
||||
|
||||
|
||||
func tableView(_ tableView: UITableView, itemsForBeginning session: UIDragSession, at indexPath: IndexPath) -> [UIDragItem] {
|
||||
guard let node = coordinator.nodeFor(indexPath), let feed = node.representedObject as? Feed else {
|
||||
return [UIDragItem]()
|
||||
}
|
||||
|
||||
|
||||
let data = feed.url.data(using: .utf8)
|
||||
let itemProvider = NSItemProvider()
|
||||
|
||||
|
||||
itemProvider.registerDataRepresentation(forTypeIdentifier: UTType.url.identifier, visibility: .ownProcess) { completion in
|
||||
Task { @MainActor in
|
||||
completion(data, nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let dragItem = UIDragItem(itemProvider: itemProvider)
|
||||
dragItem.localObject = node
|
||||
return [dragItem]
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -12,16 +12,16 @@ import Account
|
||||
import RSTree
|
||||
|
||||
extension MainFeedViewController: UITableViewDropDelegate {
|
||||
|
||||
|
||||
func tableView(_ tableView: UITableView, canHandle session: UIDropSession) -> Bool {
|
||||
return session.localDragSession != nil
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
guard let destFeed = coordinator.nodeFor(destIndexPath)?.representedObject as? SidebarItem,
|
||||
let destAccount = destFeed.account,
|
||||
let destCell = tableView.cellForRow(at: destIndexPath) else {
|
||||
@ -48,7 +48,7 @@ extension MainFeedViewController: UITableViewDropDelegate {
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func tableView(_ tableView: UITableView, performDropWith dropCoordinator: UITableViewDropCoordinator) {
|
||||
guard let dragItem = dropCoordinator.items.first?.dragItem,
|
||||
let dragNode = dragItem.localObject as? Node,
|
||||
@ -56,17 +56,17 @@ extension MainFeedViewController: UITableViewDropDelegate {
|
||||
let destIndexPath = dropCoordinator.destinationIndexPath else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let isFolderDrop: Bool = {
|
||||
if coordinator.nodeFor(destIndexPath)?.representedObject is Folder, let propCell = tableView.cellForRow(at: destIndexPath) {
|
||||
return dropCoordinator.session.location(in: propCell).y >= 0
|
||||
}
|
||||
return false
|
||||
}()
|
||||
|
||||
|
||||
// Based on the drop we have to determine a node to start looking for a parent container.
|
||||
let destNode: Node? = {
|
||||
|
||||
|
||||
if isFolderDrop {
|
||||
return coordinator.nodeFor(destIndexPath)
|
||||
} else {
|
||||
@ -78,7 +78,7 @@ extension MainFeedViewController: UITableViewDropDelegate {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}()
|
||||
|
||||
// Now we start looking for the parent container
|
||||
@ -90,9 +90,9 @@ extension MainFeedViewController: UITableViewDropDelegate {
|
||||
return coordinator.rootNode.childAtIndex(destIndexPath.section)?.representedObject as? Account
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
guard let destination = destinationContainer, let feed = dragNode.representedObject as? Feed else { return }
|
||||
|
||||
|
||||
if source.account == destination.account {
|
||||
moveFeedInAccount(feed: feed, sourceContainer: source, destinationContainer: destination)
|
||||
} else {
|
||||
@ -102,7 +102,7 @@ extension MainFeedViewController: UITableViewDropDelegate {
|
||||
|
||||
func moveFeedInAccount(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
|
||||
guard sourceContainer !== destinationContainer else { return }
|
||||
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
sourceContainer.account?.moveFeed(feed, from: sourceContainer, to: destinationContainer) { result in
|
||||
BatchUpdate.shared.end()
|
||||
@ -114,11 +114,11 @@ extension MainFeedViewController: UITableViewDropDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func moveFeedBetweenAccounts(feed: Feed, sourceContainer: Container, destinationContainer: Container) {
|
||||
|
||||
|
||||
if let existingFeed = destinationContainer.account?.existingFeed(withURL: feed.url) {
|
||||
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
destinationContainer.account?.addFeed(existingFeed, to: destinationContainer) { result in
|
||||
switch result {
|
||||
@ -137,9 +137,9 @@ extension MainFeedViewController: UITableViewDropDelegate {
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
|
||||
|
||||
BatchUpdate.shared.start()
|
||||
destinationContainer.account?.createFeed(url: feed.url, name: feed.editedName, container: destinationContainer, validateFeed: false) { result in
|
||||
switch result {
|
||||
@ -158,9 +158,8 @@ extension MainFeedViewController: UITableViewDropDelegate {
|
||||
self.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -125,7 +125,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
return
|
||||
}
|
||||
|
||||
var node: Node? = nil
|
||||
var node: Node?
|
||||
if let coordinator = unreadCountProvider as? SceneCoordinator, let feed = coordinator.timelineFeed {
|
||||
node = coordinator.rootNode.descendantNodeRepresentingObject(feed as AnyObject)
|
||||
} else {
|
||||
@ -249,7 +249,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
headerView.gestureRecognizers?.removeAll()
|
||||
let tap = UITapGestureRecognizer(target: self, action:#selector(self.toggleSectionHeader(_:)))
|
||||
let tap = UITapGestureRecognizer(target: self, action: #selector(self.toggleSectionHeader(_:)))
|
||||
headerView.addGestureRecognizer(tap)
|
||||
|
||||
// Without this the swipe gesture registers on the cell below
|
||||
@ -279,7 +279,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
// Set up the delete action
|
||||
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)
|
||||
completion(true)
|
||||
}
|
||||
@ -288,7 +288,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
// Set up the rename action
|
||||
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)
|
||||
completion(true)
|
||||
}
|
||||
@ -354,7 +354,7 @@ class MainFeedViewController: UITableViewController, UndoableCommandRunner {
|
||||
return makeFeedContextMenu(indexPath: indexPath, includeDeleteRename: true)
|
||||
} else if feed is Folder {
|
||||
return makeFolderContextMenu(indexPath: indexPath)
|
||||
} else if feed is PseudoFeed {
|
||||
} else if feed is PseudoFeed {
|
||||
return makePseudoFeedContextMenu(indexPath: indexPath)
|
||||
} else {
|
||||
return nil
|
||||
@ -686,7 +686,7 @@ extension MainFeedViewController: UIContextMenuInteractionDelegate {
|
||||
return nil
|
||||
}
|
||||
|
||||
return UIContextMenuConfiguration(identifier: sectionIndex as NSCopying, previewProvider: nil) { suggestedActions in
|
||||
return UIContextMenuConfiguration(identifier: sectionIndex as NSCopying, previewProvider: nil) { _ in
|
||||
|
||||
var menuElements = [UIMenuElement]()
|
||||
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 {
|
||||
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 }
|
||||
|
||||
@ -935,7 +935,7 @@ private extension MainFeedViewController {
|
||||
}
|
||||
|
||||
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 }
|
||||
|
||||
@ -962,7 +962,7 @@ private extension MainFeedViewController {
|
||||
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])
|
||||
})
|
||||
}
|
||||
@ -973,7 +973,7 @@ private extension MainFeedViewController {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return action
|
||||
@ -985,7 +985,7 @@ private extension MainFeedViewController {
|
||||
}
|
||||
|
||||
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)
|
||||
completion(true)
|
||||
}
|
||||
@ -999,7 +999,7 @@ private extension MainFeedViewController {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return action
|
||||
@ -1012,7 +1012,7 @@ private extension MainFeedViewController {
|
||||
}
|
||||
|
||||
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
|
||||
completion(true)
|
||||
}
|
||||
@ -1027,7 +1027,7 @@ private extension MainFeedViewController {
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
return action
|
||||
@ -1041,7 +1041,7 @@ private extension MainFeedViewController {
|
||||
}
|
||||
|
||||
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
|
||||
completion(true)
|
||||
}
|
||||
@ -1061,8 +1061,7 @@ private extension MainFeedViewController {
|
||||
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
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
completion(true)
|
||||
@ -1074,7 +1073,7 @@ private extension MainFeedViewController {
|
||||
func deleteAction(indexPath: IndexPath) -> UIAction {
|
||||
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)
|
||||
}
|
||||
return action
|
||||
@ -1082,7 +1081,7 @@ private extension MainFeedViewController {
|
||||
|
||||
func renameAction(indexPath: IndexPath) -> UIAction {
|
||||
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)
|
||||
}
|
||||
return action
|
||||
@ -1094,7 +1093,7 @@ private extension MainFeedViewController {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
return action
|
||||
@ -1102,7 +1101,7 @@ private extension MainFeedViewController {
|
||||
|
||||
func getAccountInfoAction(account: Account) -> UIAction {
|
||||
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)
|
||||
}
|
||||
return action
|
||||
@ -1110,7 +1109,7 @@ private extension MainFeedViewController {
|
||||
|
||||
func deactivateAccountAction(account: Account) -> UIAction {
|
||||
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
|
||||
}
|
||||
return action
|
||||
@ -1122,7 +1121,7 @@ private extension MainFeedViewController {
|
||||
}
|
||||
|
||||
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)
|
||||
completion(true)
|
||||
}
|
||||
@ -1138,7 +1137,7 @@ private extension MainFeedViewController {
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
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
|
||||
if let articles = try? feed.fetchUnreadArticles() {
|
||||
self?.coordinator.markAllAsRead(Array(articles))
|
||||
@ -1156,7 +1155,7 @@ private extension MainFeedViewController {
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
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
|
||||
// If you don't have this delay the screen flashes when it executes this code
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
@ -1170,7 +1169,6 @@ private extension MainFeedViewController {
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func rename(indexPath: IndexPath) {
|
||||
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))
|
||||
|
||||
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 {
|
||||
return
|
||||
@ -1214,7 +1212,7 @@ private extension MainFeedViewController {
|
||||
alertController.addAction(renameAction)
|
||||
alertController.preferredAction = renameAction
|
||||
|
||||
alertController.addTextField() { textField in
|
||||
alertController.addTextField { textField in
|
||||
textField.text = feed.nameForDisplay
|
||||
textField.placeholder = NSLocalizedString("Name", comment: "Name")
|
||||
}
|
||||
@ -1234,7 +1232,7 @@ private extension MainFeedViewController {
|
||||
title = NSLocalizedString("Delete Folder", comment: "Delete folder")
|
||||
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
|
||||
} else {
|
||||
} else {
|
||||
title = NSLocalizedString("Delete Feed", comment: "Delete feed")
|
||||
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
|
||||
@ -1246,7 +1244,7 @@ private extension MainFeedViewController {
|
||||
alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
|
||||
|
||||
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)
|
||||
}
|
||||
alertController.addAction(deleteAction)
|
||||
@ -1284,6 +1282,6 @@ extension MainFeedViewController: UIGestureRecognizerDelegate {
|
||||
return false
|
||||
}
|
||||
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
|
||||
|
||||
class RefreshProgressView: UIView {
|
||||
|
||||
|
||||
@IBOutlet weak var progressView: UIProgressView!
|
||||
@IBOutlet weak var label: UILabel!
|
||||
|
||||
|
||||
override func awakeFromNib() {
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(progressDidChange(_:)), name: .combinedRefreshProgressDidChange, object: nil)
|
||||
@ -24,7 +24,7 @@ class RefreshProgressView: UIView {
|
||||
isAccessibilityElement = true
|
||||
accessibilityTraits = [.updatesFrequently, .notEnabled]
|
||||
}
|
||||
|
||||
|
||||
func update() {
|
||||
if !AccountManager.shared.combinedRefreshProgress.isComplete {
|
||||
progressChanged(animated: false)
|
||||
@ -50,7 +50,7 @@ class RefreshProgressView: UIView {
|
||||
deinit {
|
||||
NotificationCenter.default.removeObserver(self)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
@ -68,7 +68,7 @@ private extension RefreshProgressView {
|
||||
if isInViewHierarchy {
|
||||
progressView.setProgress(1, animated: animated)
|
||||
}
|
||||
|
||||
|
||||
func completeLabel() {
|
||||
// Check that there are no pending downloads.
|
||||
if AccountManager.shared.combinedRefreshProgress.isComplete {
|
||||
@ -101,7 +101,7 @@ private extension RefreshProgressView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateRefreshLabel() {
|
||||
if let accountLastArticleFetchEndTime = AccountManager.shared.lastArticleFetchEndTime {
|
||||
|
||||
@ -131,5 +131,5 @@ private extension RefreshProgressView {
|
||||
self?.scheduleUpdateRefreshLabel()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -19,7 +19,7 @@ struct MainTimelineAccessibilityCellLayout: MainTimelineCellLayout {
|
||||
let summaryRect: CGRect
|
||||
let feedNameRect: CGRect
|
||||
let dateRect: CGRect
|
||||
|
||||
|
||||
init(width: CGFloat, insets: UIEdgeInsets, cellData: MainTimelineCellData) {
|
||||
|
||||
var currentPoint = CGPoint.zero
|
||||
@ -40,13 +40,13 @@ struct MainTimelineAccessibilityCellLayout: MainTimelineCellLayout {
|
||||
} else {
|
||||
self.iconImageRect = CGRect.zero
|
||||
}
|
||||
|
||||
|
||||
let textAreaWidth = width - (currentPoint.x + MainTimelineDefaultCellLayout.cellPadding.right + insets.right)
|
||||
|
||||
// Title Text Block
|
||||
let (titleRect, numberOfLinesForTitle) = MainTimelineAccessibilityCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth)
|
||||
self.titleRect = titleRect
|
||||
|
||||
|
||||
// Summary Text Block
|
||||
if self.titleRect != CGRect.zero {
|
||||
currentPoint.y = self.titleRect.maxY + MainTimelineDefaultCellLayout.titleBottomMargin
|
||||
@ -54,21 +54,21 @@ struct MainTimelineAccessibilityCellLayout: MainTimelineCellLayout {
|
||||
self.summaryRect = MainTimelineAccessibilityCellLayout.rectForSummary(cellData, currentPoint, textAreaWidth, numberOfLinesForTitle)
|
||||
|
||||
currentPoint.y = [self.titleRect, self.summaryRect].maxY()
|
||||
|
||||
|
||||
if cellData.showFeedName != .none {
|
||||
self.feedNameRect = MainTimelineAccessibilityCellLayout.rectForFeedName(cellData, currentPoint, textAreaWidth)
|
||||
currentPoint.y = self.feedNameRect.maxY
|
||||
} else {
|
||||
self.feedNameRect = CGRect.zero
|
||||
}
|
||||
|
||||
|
||||
// Feed Name and Pub Date
|
||||
self.dateRect = MainTimelineAccessibilityCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)
|
||||
|
||||
self.height = self.dateRect.maxY + MainTimelineDefaultCellLayout.cellPadding.bottom
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Calculate Rects
|
||||
@ -78,13 +78,13 @@ private extension MainTimelineAccessibilityCellLayout {
|
||||
static func rectForDate(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||
|
||||
var r = CGRect.zero
|
||||
|
||||
|
||||
let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MainTimelineDefaultCellLayout.dateFont)
|
||||
r.size = size
|
||||
r.origin = point
|
||||
|
||||
|
||||
return r
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import Articles
|
||||
struct MainTimelineCellData {
|
||||
|
||||
private static let noText = NSLocalizedString("(No Text)", comment: "No Text")
|
||||
|
||||
|
||||
let title: String
|
||||
let attributedTitle: NSAttributedString
|
||||
let summary: String
|
||||
@ -38,16 +38,15 @@ struct MainTimelineCellData {
|
||||
} else {
|
||||
self.summary = truncatedSummary
|
||||
}
|
||||
|
||||
|
||||
self.dateString = ArticleStringFormatter.dateString(article.logicalDatePublished)
|
||||
|
||||
if let feedName = feedName {
|
||||
self.feedName = ArticleStringFormatter.truncatedFeedName(feedName)
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
self.feedName = ""
|
||||
}
|
||||
|
||||
|
||||
if let byline = byline {
|
||||
self.byline = byline
|
||||
} else {
|
||||
@ -63,10 +62,10 @@ struct MainTimelineCellData {
|
||||
self.starred = article.status.starred
|
||||
self.numberOfLines = numberOfLines
|
||||
self.iconSize = iconSize
|
||||
|
||||
|
||||
}
|
||||
|
||||
init() { //Empty
|
||||
init() { // Empty
|
||||
self.title = ""
|
||||
self.attributedTitle = NSAttributedString()
|
||||
self.summary = ""
|
||||
@ -81,5 +80,5 @@ struct MainTimelineCellData {
|
||||
self.numberOfLines = 0
|
||||
self.iconSize = .medium
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ protocol MainTimelineCellLayout {
|
||||
var summaryRect: CGRect {get}
|
||||
var feedNameRect: CGRect {get}
|
||||
var dateRect: CGRect {get}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension MainTimelineCellLayout {
|
||||
@ -30,8 +30,7 @@ extension MainTimelineCellLayout {
|
||||
r.origin.y = point.y + 5
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
|
||||
static func rectForStar(_ point: CGPoint) -> CGRect {
|
||||
var r = CGRect.zero
|
||||
r.size.width = MainTimelineDefaultCellLayout.starDimension
|
||||
@ -40,7 +39,7 @@ extension MainTimelineCellLayout {
|
||||
r.origin.y = point.y + 3
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
static func rectForIconView(_ point: CGPoint, iconSize: IconSize) -> CGRect {
|
||||
var r = CGRect.zero
|
||||
r.size = iconSize.size
|
||||
@ -48,16 +47,16 @@ extension MainTimelineCellLayout {
|
||||
r.origin.y = point.y + 4
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
static func rectForTitle(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> (CGRect, Int) {
|
||||
|
||||
var r = CGRect.zero
|
||||
if cellData.title.isEmpty {
|
||||
return (r, 0)
|
||||
}
|
||||
|
||||
|
||||
r.origin = point
|
||||
|
||||
|
||||
let sizeInfo = MultilineUILabelSizer.size(for: cellData.title, font: MainTimelineDefaultCellLayout.titleFont, numberOfLines: cellData.numberOfLines, width: Int(textAreaWidth))
|
||||
|
||||
r.size.width = textAreaWidth
|
||||
@ -65,22 +64,22 @@ extension MainTimelineCellLayout {
|
||||
if sizeInfo.numberOfLinesUsed < 1 {
|
||||
r.size.height = 0
|
||||
}
|
||||
|
||||
|
||||
return (r, sizeInfo.numberOfLinesUsed)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
static func rectForSummary(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat, _ linesUsed: Int) -> CGRect {
|
||||
|
||||
let linesLeft = cellData.numberOfLines - linesUsed
|
||||
|
||||
|
||||
var r = CGRect.zero
|
||||
if cellData.summary.isEmpty || linesLeft < 1 {
|
||||
return r
|
||||
}
|
||||
|
||||
|
||||
r.origin = point
|
||||
|
||||
|
||||
let sizeInfo = MultilineUILabelSizer.size(for: cellData.summary, font: MainTimelineDefaultCellLayout.summaryFont, numberOfLines: linesLeft, width: Int(textAreaWidth))
|
||||
|
||||
r.size.width = textAreaWidth
|
||||
@ -88,26 +87,26 @@ extension MainTimelineCellLayout {
|
||||
if sizeInfo.numberOfLinesUsed < 1 {
|
||||
r.size.height = 0
|
||||
}
|
||||
|
||||
|
||||
return r
|
||||
|
||||
|
||||
}
|
||||
|
||||
static func rectForFeedName(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||
|
||||
var r = CGRect.zero
|
||||
r.origin = point
|
||||
|
||||
|
||||
let feedName = cellData.showFeedName == .feed ? cellData.feedName : cellData.byline
|
||||
let size = SingleLineUILabelSizer.size(for: feedName, font: MainTimelineDefaultCellLayout.feedNameFont)
|
||||
r.size = size
|
||||
|
||||
|
||||
if r.size.width > textAreaWidth {
|
||||
r.size.width = textAreaWidth
|
||||
}
|
||||
|
||||
|
||||
return r
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import RSCore
|
||||
struct MainTimelineDefaultCellLayout: MainTimelineCellLayout {
|
||||
|
||||
static let cellPadding = UIEdgeInsets(top: 12, left: 8, bottom: 12, right: 20)
|
||||
|
||||
|
||||
static let unreadCircleMarginLeft = CGFloat(integerLiteral: 0)
|
||||
static let unreadCircleDimension = CGFloat(integerLiteral: 12)
|
||||
static let unreadCircleSize = CGSize(width: MainTimelineDefaultCellLayout.unreadCircleDimension, height: MainTimelineDefaultCellLayout.unreadCircleDimension)
|
||||
@ -33,7 +33,7 @@ struct MainTimelineDefaultCellLayout: MainTimelineCellLayout {
|
||||
return UIFont.preferredFont(forTextStyle: .footnote)
|
||||
}
|
||||
static let feedRightMargin = CGFloat(integerLiteral: 8)
|
||||
|
||||
|
||||
static var dateFont: UIFont {
|
||||
return UIFont.preferredFont(forTextStyle: .footnote)
|
||||
}
|
||||
@ -72,13 +72,13 @@ struct MainTimelineDefaultCellLayout: MainTimelineCellLayout {
|
||||
} else {
|
||||
self.iconImageRect = CGRect.zero
|
||||
}
|
||||
|
||||
|
||||
let textAreaWidth = width - (currentPoint.x + MainTimelineDefaultCellLayout.cellPadding.right + insets.right)
|
||||
|
||||
// Title Text Block
|
||||
let (titleRect, numberOfLinesForTitle) = MainTimelineDefaultCellLayout.rectForTitle(cellData, currentPoint, textAreaWidth)
|
||||
self.titleRect = titleRect
|
||||
|
||||
|
||||
// Summary Text Block
|
||||
if self.titleRect != CGRect.zero {
|
||||
currentPoint.y = self.titleRect.maxY + MainTimelineDefaultCellLayout.titleBottomMargin
|
||||
@ -93,7 +93,7 @@ struct MainTimelineDefaultCellLayout: MainTimelineCellLayout {
|
||||
y -= tmp.height
|
||||
}
|
||||
currentPoint.y = y
|
||||
|
||||
|
||||
// Feed Name and Pub Date
|
||||
self.dateRect = MainTimelineDefaultCellLayout.rectForDate(cellData, currentPoint, textAreaWidth)
|
||||
|
||||
@ -103,7 +103,7 @@ struct MainTimelineDefaultCellLayout: MainTimelineCellLayout {
|
||||
self.height = [self.iconImageRect, self.feedNameRect].maxY() + MainTimelineDefaultCellLayout.cellPadding.bottom
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Calculate Rects
|
||||
@ -113,14 +113,14 @@ extension MainTimelineDefaultCellLayout {
|
||||
static func rectForDate(_ cellData: MainTimelineCellData, _ point: CGPoint, _ textAreaWidth: CGFloat) -> CGRect {
|
||||
|
||||
var r = CGRect.zero
|
||||
|
||||
|
||||
let size = SingleLineUILabelSizer.size(for: cellData.dateString, font: MainTimelineDefaultCellLayout.dateFont)
|
||||
r.size = size
|
||||
r.origin.x = (point.x + textAreaWidth) - size.width
|
||||
r.origin.y = point.y
|
||||
|
||||
return r
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -18,11 +18,11 @@ class MainTimelineTableViewCell: VibrantTableViewCell {
|
||||
private let feedNameView = MainTimelineTableViewCell.singleLineUILabel()
|
||||
|
||||
private lazy var iconView = IconView()
|
||||
|
||||
|
||||
private lazy var starView = {
|
||||
return NonIntrinsicImageView(image: AppAssets.timelineStarImage)
|
||||
}()
|
||||
|
||||
|
||||
private var unreadIndicatorPropertyAnimator: UIViewPropertyAnimator?
|
||||
private var starViewPropertyAnimator: UIViewPropertyAnimator?
|
||||
|
||||
@ -31,12 +31,12 @@ class MainTimelineTableViewCell: VibrantTableViewCell {
|
||||
updateSubviews()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
override func prepareForReuse() {
|
||||
unreadIndicatorPropertyAnimator?.stopAnimation(true)
|
||||
unreadIndicatorPropertyAnimator = nil
|
||||
@ -46,19 +46,19 @@ class MainTimelineTableViewCell: VibrantTableViewCell {
|
||||
starViewPropertyAnimator = nil
|
||||
starView.isHidden = true
|
||||
}
|
||||
|
||||
|
||||
override var frame: CGRect {
|
||||
didSet {
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func updateVibrancy(animated: Bool) {
|
||||
updateLabelVibrancy(titleView, color: labelColor, animated: animated)
|
||||
updateLabelVibrancy(summaryView, color: labelColor, animated: animated)
|
||||
updateLabelVibrancy(dateView, color: secondaryLabelColor, animated: animated)
|
||||
updateLabelVibrancy(feedNameView, color: secondaryLabelColor, animated: animated)
|
||||
|
||||
|
||||
if animated {
|
||||
UIView.animate(withDuration: Self.duration) {
|
||||
if self.isHighlighted || self.isSelected {
|
||||
@ -75,16 +75,16 @@ class MainTimelineTableViewCell: VibrantTableViewCell {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func sizeThatFits(_ size: CGSize) -> CGSize {
|
||||
let layout = updatedLayout(width: size.width)
|
||||
return CGSize(width: size.width, height: layout.height)
|
||||
}
|
||||
|
||||
override func layoutSubviews() {
|
||||
|
||||
|
||||
super.layoutSubviews()
|
||||
|
||||
|
||||
let layout = updatedLayout(width: bounds.width)
|
||||
|
||||
unreadIndicatorView.setFrameIfNotEqual(layout.unreadIndicatorRect)
|
||||
@ -97,11 +97,11 @@ class MainTimelineTableViewCell: VibrantTableViewCell {
|
||||
|
||||
separatorInset = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 0)
|
||||
}
|
||||
|
||||
|
||||
func setIconImage(_ image: IconImage) {
|
||||
iconView.iconImage = image
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@ -115,7 +115,7 @@ private extension MainTimelineTableViewCell {
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
return label
|
||||
}
|
||||
|
||||
|
||||
static func multiLineUILabel() -> UILabel {
|
||||
let label = NonIntrinsicLabel()
|
||||
label.numberOfLines = 0
|
||||
@ -124,16 +124,16 @@ private extension MainTimelineTableViewCell {
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
return label
|
||||
}
|
||||
|
||||
|
||||
func setFrame(for label: UILabel, rect: CGRect) {
|
||||
|
||||
|
||||
if Int(floor(rect.height)) == 0 || Int(floor(rect.width)) == 0 {
|
||||
hideView(label)
|
||||
} else {
|
||||
showView(label)
|
||||
label.setFrameIfNotEqual(rect)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
func addSubviewAtInit(_ view: UIView, hidden: Bool) {
|
||||
@ -141,9 +141,9 @@ private extension MainTimelineTableViewCell {
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.isHidden = hidden
|
||||
}
|
||||
|
||||
|
||||
func commonInit() {
|
||||
|
||||
|
||||
addSubviewAtInit(titleView, hidden: false)
|
||||
addSubviewAtInit(summaryView, hidden: true)
|
||||
addSubviewAtInit(unreadIndicatorView, hidden: true)
|
||||
@ -152,7 +152,7 @@ private extension MainTimelineTableViewCell {
|
||||
addSubviewAtInit(iconView, hidden: true)
|
||||
addSubviewAtInit(starView, hidden: true)
|
||||
}
|
||||
|
||||
|
||||
func updatedLayout(width: CGFloat) -> MainTimelineCellLayout {
|
||||
if UIApplication.shared.preferredContentSizeCategory.isAccessibilityCategory {
|
||||
return MainTimelineAccessibilityCellLayout(width: width, insets: safeAreaInsets, cellData: cellData)
|
||||
@ -160,25 +160,25 @@ private extension MainTimelineTableViewCell {
|
||||
return MainTimelineDefaultCellLayout(width: width, insets: safeAreaInsets, cellData: cellData)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateTitleView() {
|
||||
titleView.font = MainTimelineDefaultCellLayout.titleFont
|
||||
titleView.textColor = labelColor
|
||||
updateTextFieldAttributedText(titleView, cellData?.attributedTitle)
|
||||
}
|
||||
|
||||
|
||||
func updateSummaryView() {
|
||||
summaryView.font = MainTimelineDefaultCellLayout.summaryFont
|
||||
summaryView.textColor = labelColor
|
||||
updateTextFieldText(summaryView, cellData?.summary)
|
||||
}
|
||||
|
||||
|
||||
func updateDateView() {
|
||||
dateView.font = MainTimelineDefaultCellLayout.dateFont
|
||||
dateView.textColor = secondaryLabelColor
|
||||
updateTextFieldText(dateView, cellData.dateString)
|
||||
}
|
||||
|
||||
|
||||
func updateTextFieldText(_ label: UILabel, _ text: String?) {
|
||||
let s = text ?? ""
|
||||
if label.text != s {
|
||||
@ -199,7 +199,7 @@ private extension MainTimelineTableViewCell {
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateFeedNameView() {
|
||||
switch cellData.showFeedName {
|
||||
case .feed:
|
||||
@ -216,7 +216,7 @@ private extension MainTimelineTableViewCell {
|
||||
hideView(feedNameView)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateUnreadIndicator() {
|
||||
if !unreadIndicatorView.isHidden && cellData.read && !cellData.starred {
|
||||
unreadIndicatorPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in
|
||||
@ -233,7 +233,7 @@ private extension MainTimelineTableViewCell {
|
||||
showOrHideView(unreadIndicatorView, cellData.read || cellData.starred)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateStarView() {
|
||||
if !starView.isHidden && cellData.read && !cellData.starred {
|
||||
starViewPropertyAnimator = UIViewPropertyAnimator(duration: 0.66, curve: .easeInOut) { [weak self] in
|
||||
@ -250,7 +250,7 @@ private extension MainTimelineTableViewCell {
|
||||
showOrHideView(starView, !cellData.starred)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateIconImage() {
|
||||
guard let image = cellData.iconImage, cellData.showIcon else {
|
||||
makeIconEmpty()
|
||||
@ -258,20 +258,20 @@ private extension MainTimelineTableViewCell {
|
||||
}
|
||||
|
||||
showView(iconView)
|
||||
|
||||
|
||||
if iconView.iconImage !== cellData.iconImage {
|
||||
iconView.iconImage = image
|
||||
setNeedsLayout()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateAccessiblityLabel() {
|
||||
let starredStatus = cellData.starred ? "\(NSLocalizedString("Starred", comment: "Starred article for accessibility")), " : ""
|
||||
let unreadStatus = cellData.read ? "" : "\(NSLocalizedString("Unread", comment: "Unread")), "
|
||||
let label = starredStatus + unreadStatus + "\(cellData.feedName), \(cellData.title), \(cellData.summary), \(cellData.dateString)"
|
||||
accessibilityLabel = label
|
||||
}
|
||||
|
||||
|
||||
func makeIconEmpty() {
|
||||
if iconView.iconImage != nil {
|
||||
iconView.iconImage = nil
|
||||
@ -279,23 +279,23 @@ private extension MainTimelineTableViewCell {
|
||||
}
|
||||
hideView(iconView)
|
||||
}
|
||||
|
||||
|
||||
func hideView(_ view: UIView) {
|
||||
if !view.isHidden {
|
||||
view.isHidden = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func showView(_ view: UIView) {
|
||||
if view.isHidden {
|
||||
view.isHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func showOrHideView(_ view: UIView, _ shouldHide: Bool) {
|
||||
shouldHide ? hideView(view) : showView(view)
|
||||
}
|
||||
|
||||
|
||||
func updateSubviews() {
|
||||
updateTitleView()
|
||||
updateSummaryView()
|
||||
@ -306,5 +306,5 @@ private extension MainTimelineTableViewCell {
|
||||
updateIconImage()
|
||||
updateAccessiblityLabel()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -15,5 +15,5 @@ class MainUnreadIndicatorView: UIView {
|
||||
layer.cornerRadius = frame.size.width / 2.0
|
||||
clipsToBounds = true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -42,7 +42,7 @@ final class MultilineUILabelSizer {
|
||||
|
||||
self.singleLineHeightEstimate = MultilineUILabelSizer.calculateHeight("AqLjJ0/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 {
|
||||
@ -52,7 +52,7 @@ final class MultilineUILabelSizer {
|
||||
static func emptyCache() {
|
||||
sizers = [UILabelSizerSpecifier: MultilineUILabelSizer]()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: - Private
|
||||
@ -69,7 +69,7 @@ private extension MultilineUILabelSizer {
|
||||
let newSizer = MultilineUILabelSizer(numberOfLines: numberOfLines, font: font)
|
||||
sizers[specifier] = newSizer
|
||||
return newSizer
|
||||
|
||||
|
||||
}
|
||||
|
||||
func sizeInfo(for string: String, width: Int) -> TextFieldSizeInfo {
|
||||
@ -80,7 +80,7 @@ private extension MultilineUILabelSizer {
|
||||
let size = CGSize(width: width, height: textFieldHeight)
|
||||
let sizeInfo = TextFieldSizeInfo(size: size, numberOfLinesUsed: numberOfLinesUsed)
|
||||
return sizeInfo
|
||||
|
||||
|
||||
}
|
||||
|
||||
func height(for string: String, width: Int) -> Int {
|
||||
@ -98,14 +98,14 @@ private extension MultilineUILabelSizer {
|
||||
}
|
||||
|
||||
var height = MultilineUILabelSizer.calculateHeight(string, width, font)
|
||||
|
||||
|
||||
if numberOfLines != 0 {
|
||||
let maxHeight = singleLineHeightEstimate * numberOfLines
|
||||
if height > maxHeight {
|
||||
height = maxHeight
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
cache[string]![width] = height
|
||||
|
||||
return height
|
||||
@ -123,7 +123,7 @@ private extension MultilineUILabelSizer {
|
||||
let averageHeight = CGFloat(doubleLineHeightEstimate) / 2.0
|
||||
let lines = Int(round(CGFloat(height) / averageHeight))
|
||||
return lines
|
||||
|
||||
|
||||
}
|
||||
|
||||
func heightIsProbablySingleLineHeight(_ height: Int) -> Bool {
|
||||
@ -140,7 +140,7 @@ private extension MultilineUILabelSizer {
|
||||
let minimum = estimate - slop
|
||||
let maximum = estimate + slop
|
||||
return height >= minimum && height <= maximum
|
||||
|
||||
|
||||
}
|
||||
|
||||
func heightConsideringNeighbors(_ heightCache: WidthHeightCache, _ width: Int) -> Int? {
|
||||
@ -165,8 +165,7 @@ private extension MultilineUILabelSizer {
|
||||
|
||||
if oneWidth < width && (oneWidth > smallNeighbor.width || smallNeighbor.width == 0) {
|
||||
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)
|
||||
}
|
||||
|
||||
@ -176,7 +175,7 @@ private extension MultilineUILabelSizer {
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -30,10 +30,10 @@ final class SingleLineUILabelSizer {
|
||||
let height = text.height(withConstrainedWidth: .greatestFiniteMagnitude, font: font)
|
||||
let width = text.width(withConstrainedHeight: .greatestFiniteMagnitude, font: font)
|
||||
let calculatedSize = CGSize(width: ceil(width), height: ceil(height))
|
||||
|
||||
|
||||
cache[text] = calculatedSize
|
||||
return calculatedSize
|
||||
|
||||
|
||||
}
|
||||
|
||||
static private var sizers = [UIFont: SingleLineUILabelSizer]()
|
||||
@ -48,7 +48,7 @@ final class SingleLineUILabelSizer {
|
||||
sizers[font] = newSizer
|
||||
|
||||
return newSizer
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Use this call. It’s easiest.
|
||||
@ -60,5 +60,5 @@ final class SingleLineUILabelSizer {
|
||||
static func emptyCache() {
|
||||
sizers = [UIFont: SingleLineUILabelSizer]()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -8,11 +8,10 @@
|
||||
|
||||
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 {
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -24,8 +24,7 @@ class MainTimelineTitleView: UIView {
|
||||
if let name = label.text {
|
||||
let unreadLabel = NSLocalizedString("unread", comment: "Unread label for accessibility")
|
||||
return "\(name) \(unreadCountView.unreadCount) \(unreadLabel)"
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
@ -36,7 +35,7 @@ class MainTimelineTitleView: UIView {
|
||||
accessibilityTraits = .button
|
||||
addInteraction(pointerInteraction)
|
||||
}
|
||||
|
||||
|
||||
func debuttonize() {
|
||||
heightAnchor.constraint(equalToConstant: 40.0).isActive = true
|
||||
accessibilityTraits.remove(.button)
|
||||
@ -45,7 +44,7 @@ class MainTimelineTitleView: UIView {
|
||||
}
|
||||
|
||||
extension MainTimelineTitleView: UIPointerInteractionDelegate {
|
||||
|
||||
|
||||
func pointerInteraction(_ interaction: UIPointerInteraction, styleFor region: UIPointerRegion) -> UIPointerStyle? {
|
||||
var rect = self.frame
|
||||
rect.origin.x = rect.origin.x - 10
|
||||
|
@ -17,11 +17,11 @@ class MainTimelineUnreadCountView: MainFeedUnreadCountView {
|
||||
override var textColor: UIColor {
|
||||
return UIColor.systemBackground
|
||||
}
|
||||
|
||||
|
||||
override var intrinsicContentSize: CGSize {
|
||||
return contentSize
|
||||
}
|
||||
|
||||
|
||||
override func draw(_ dirtyRect: CGRect) {
|
||||
|
||||
let cornerRadii = CGSize(width: cornerRadius, height: cornerRadius)
|
||||
@ -33,7 +33,7 @@ class MainTimelineUnreadCountView: MainFeedUnreadCountView {
|
||||
if unreadCount > 0 {
|
||||
unreadCountString.draw(at: textRect().origin, withAttributes: textAttributes)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -14,21 +14,20 @@ extension CGRect: MarkAsReadAlertControllerSourceType {}
|
||||
extension UIView: MarkAsReadAlertControllerSourceType {}
|
||||
extension UIBarButtonItem: MarkAsReadAlertControllerSourceType {}
|
||||
|
||||
|
||||
struct MarkAsReadAlertController {
|
||||
|
||||
|
||||
static func confirm<T>(_ controller: UIViewController?,
|
||||
coordinator: SceneCoordinator?,
|
||||
confirmTitle: String,
|
||||
sourceType: T,
|
||||
cancelCompletion: (() -> Void)? = nil,
|
||||
completion: @escaping () -> Void) where T: MarkAsReadAlertControllerSourceType {
|
||||
|
||||
|
||||
guard let controller = controller, let coordinator = coordinator else {
|
||||
completion()
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if AppDefaults.shared.confirmMarkAllAsRead {
|
||||
let alertController = MarkAsReadAlertController.alert(coordinator: coordinator, confirmTitle: confirmTitle, cancelCompletion: cancelCompletion, sourceType: sourceType) { _ in
|
||||
completion()
|
||||
@ -38,20 +37,19 @@ struct MarkAsReadAlertController {
|
||||
completion()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static func alert<T>(coordinator: SceneCoordinator,
|
||||
confirmTitle: String,
|
||||
cancelCompletion: (() -> Void)?,
|
||||
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 message = NSLocalizedString("You can turn this confirmation off in Settings.",
|
||||
comment: "You can turn this confirmation off in Settings.")
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
let settingsTitle = NSLocalizedString("Open Settings", comment: "Open Settings")
|
||||
|
||||
|
||||
let alertController = UIAlertController(title: title, message: message, preferredStyle: .actionSheet)
|
||||
let cancelAction = UIAlertAction(title: cancelTitle, style: .cancel) { _ in
|
||||
cancelCompletion?()
|
||||
@ -60,24 +58,24 @@ struct MarkAsReadAlertController {
|
||||
coordinator.showSettings(scrollToArticlesSection: true)
|
||||
}
|
||||
let markAction = UIAlertAction(title: confirmTitle, style: .default, handler: completion)
|
||||
|
||||
|
||||
alertController.addAction(markAction)
|
||||
alertController.addAction(settingsAction)
|
||||
alertController.addAction(cancelAction)
|
||||
|
||||
|
||||
if let barButtonItem = sourceType as? UIBarButtonItem {
|
||||
alertController.popoverPresentationController?.barButtonItem = barButtonItem
|
||||
}
|
||||
|
||||
|
||||
if let rect = sourceType as? CGRect {
|
||||
alertController.popoverPresentationController?.sourceRect = rect
|
||||
}
|
||||
|
||||
|
||||
if let view = sourceType as? UIView {
|
||||
alertController.popoverPresentationController?.sourceView = view
|
||||
}
|
||||
|
||||
return alertController
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -17,8 +17,8 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
private var numberOfTextLines = 0
|
||||
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?
|
||||
|
||||
@IBOutlet weak var markAllAsReadButton: UIBarButtonItem!
|
||||
@ -28,28 +28,28 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
|
||||
private lazy var dataSource = makeDataSource()
|
||||
private let searchController = UISearchController(searchResultsController: nil)
|
||||
|
||||
|
||||
weak var coordinator: SceneCoordinator!
|
||||
var undoableCommands = [UndoableCommand]()
|
||||
let scrollPositionQueue = CoalescingQueue(name: "Timeline Scroll Position", interval: 0.3, maxInterval: 1.0)
|
||||
|
||||
private let keyboardManager = KeyboardManager(type: .timeline)
|
||||
override var keyCommands: [UIKeyCommand]? {
|
||||
|
||||
|
||||
// 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
|
||||
// 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 }
|
||||
|
||||
|
||||
return keyboardManager.keyCommands
|
||||
}
|
||||
|
||||
|
||||
override var canBecomeFirstResponder: Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
|
||||
super.viewDidLoad()
|
||||
|
||||
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(displayNameDidChange), name: .DisplayNameDidChange, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil)
|
||||
|
||||
|
||||
// Initialize Programmatic Buttons
|
||||
filterButton = UIBarButtonItem(image: AppAssets.filterInactiveImage, style: .plain, target: self, action: #selector(toggleFilter(_:)))
|
||||
firstUnreadButton = UIBarButtonItem(image: AppAssets.nextUnreadArticleImage, style: .plain, target: self, action: #selector(firstUnread(_:)))
|
||||
|
||||
|
||||
// Setup the Search Controller
|
||||
searchController.delegate = 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 {
|
||||
navigationItem.titleView = titleView
|
||||
}
|
||||
|
||||
|
||||
refreshControl = UIRefreshControl()
|
||||
refreshControl!.addTarget(self, action: #selector(refreshAccounts(_:)), for: .valueChanged)
|
||||
|
||||
|
||||
configureToolbar()
|
||||
resetUI(resetScroll: true)
|
||||
|
||||
|
||||
// Load the table and then scroll to the saved position if available
|
||||
applyChanges(animated: false) {
|
||||
if let restoreIndexPath = self.coordinator.timelineMiddleIndexPath {
|
||||
self.tableView.scrollToRow(at: restoreIndexPath, at: .middle, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Disable swipe back on iPad Mice
|
||||
guard let gesture = self.navigationController?.interactivePopGestureRecognizer as? UIPanGestureRecognizer else {
|
||||
return
|
||||
}
|
||||
gesture.allowedScrollTypesMask = []
|
||||
}
|
||||
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
self.navigationController?.isToolbarHidden = false
|
||||
|
||||
@ -125,10 +125,10 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
if navigationController?.navigationBar.isHidden ?? false {
|
||||
navigationController?.navigationBar.alpha = 0
|
||||
}
|
||||
|
||||
|
||||
super.viewWillAppear(animated)
|
||||
}
|
||||
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(true)
|
||||
coordinator.isTimelineViewControllerPending = false
|
||||
@ -139,9 +139,9 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
|
||||
@objc func openInBrowser(_ sender: Any?) {
|
||||
coordinator.showBrowserForCurrentArticle()
|
||||
}
|
||||
@ -149,35 +149,35 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
@objc func openInAppBrowser(_ sender: Any?) {
|
||||
coordinator.showInAppBrowser()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func toggleFilter(_ sender: Any) {
|
||||
coordinator.toggleReadArticlesFilter()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func markAllAsRead(_ sender: Any) {
|
||||
let title = NSLocalizedString("Mark All as Read", comment: "Mark All as Read")
|
||||
|
||||
|
||||
if let source = sender as? UIBarButtonItem {
|
||||
MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: source) { [weak self] in
|
||||
self?.coordinator.markAllAsReadInTimeline()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if let _ = sender as? UIKeyCommand {
|
||||
guard let indexPath = tableView.indexPathForSelectedRow, let contentView = tableView.cellForRow(at: indexPath)?.contentView else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
MarkAsReadAlertController.confirm(self, coordinator: coordinator, confirmTitle: title, sourceType: contentView) { [weak self] in
|
||||
self?.coordinator.markAllAsReadInTimeline()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func firstUnread(_ sender: Any) {
|
||||
coordinator.selectFirstUnread()
|
||||
}
|
||||
|
||||
|
||||
@objc func refreshAccounts(_ sender: Any) {
|
||||
refreshControl?.endRefreshing()
|
||||
|
||||
@ -187,9 +187,9 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// MARK: Keyboard shortcuts
|
||||
|
||||
|
||||
@objc func selectNextUp(_ sender: Any?) {
|
||||
coordinator.selectPrevArticle()
|
||||
}
|
||||
@ -201,17 +201,17 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
@objc func navigateToSidebar(_ sender: Any?) {
|
||||
coordinator.navigateToFeeds()
|
||||
}
|
||||
|
||||
|
||||
@objc func navigateToDetail(_ sender: Any?) {
|
||||
coordinator.navigateToDetail()
|
||||
}
|
||||
|
||||
|
||||
@objc func showFeedInspector(_ sender: Any?) {
|
||||
coordinator.showFeedInspector()
|
||||
}
|
||||
|
||||
// MARK: API
|
||||
|
||||
|
||||
func restoreSelectionIfNecessary(adjustScroll: Bool) {
|
||||
if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) {
|
||||
if adjustScroll {
|
||||
@ -225,11 +225,11 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
func reinitializeArticles(resetScroll: Bool) {
|
||||
resetUI(resetScroll: resetScroll)
|
||||
}
|
||||
|
||||
|
||||
func reloadArticles(animated: Bool) {
|
||||
applyChanges(animated: animated)
|
||||
}
|
||||
|
||||
|
||||
func updateArticleSelection(animations: Animations) {
|
||||
if let article = coordinator.currentArticle, let indexPath = dataSource.indexPath(for: article) {
|
||||
if tableView.indexPathForSelectedRow != indexPath {
|
||||
@ -238,7 +238,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
} else {
|
||||
tableView.selectRow(at: nil, animated: animations.contains(.select), scrollPosition: .none)
|
||||
}
|
||||
|
||||
|
||||
updateUI()
|
||||
}
|
||||
|
||||
@ -247,7 +247,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
updateTitleUnreadCount()
|
||||
updateToolbar()
|
||||
}
|
||||
|
||||
|
||||
func hideSearch() {
|
||||
navigationItem.searchController?.isActive = false
|
||||
}
|
||||
@ -257,7 +257,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
navigationItem.searchController?.searchBar.selectedScopeButtonIndex = 1
|
||||
navigationItem.searchController?.searchBar.becomeFirstResponder()
|
||||
}
|
||||
|
||||
|
||||
func focus() {
|
||||
becomeFirstResponder()
|
||||
}
|
||||
@ -265,7 +265,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
func setRefreshToolbarItemVisibility(visible: Bool) {
|
||||
refreshProgressView?.alpha = visible ? 1.0 : 0
|
||||
}
|
||||
|
||||
|
||||
// MARK: - Table view
|
||||
|
||||
override func tableView(_ tableView: UITableView, leadingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
@ -276,41 +276,41 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
let readTitle = article.status.read ?
|
||||
NSLocalizedString("Mark as Unread", comment: "Mark as Unread") :
|
||||
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)
|
||||
completion(true)
|
||||
}
|
||||
|
||||
|
||||
readAction.image = article.status.read ? AppAssets.circleClosedImage : AppAssets.circleOpenImage
|
||||
readAction.backgroundColor = AppAssets.primaryAccentColor
|
||||
|
||||
|
||||
return UISwipeActionsConfiguration(actions: [readAction])
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
|
||||
|
||||
guard let article = dataSource.itemIdentifier(for: indexPath) else { return nil }
|
||||
|
||||
|
||||
// Set up the star action
|
||||
let starTitle = article.status.starred ?
|
||||
NSLocalizedString("Unstar", comment: "Unstar") :
|
||||
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)
|
||||
completion(true)
|
||||
}
|
||||
|
||||
|
||||
starAction.image = article.status.starred ? AppAssets.starOpenImage : AppAssets.starClosedImage
|
||||
starAction.backgroundColor = AppAssets.starColor
|
||||
|
||||
|
||||
// Set up the read action
|
||||
let moreTitle = NSLocalizedString("More", comment: "More")
|
||||
let moreAction = UIContextualAction(style: .normal, title: moreTitle) { [weak self] (action, view, completion) in
|
||||
|
||||
|
||||
if let self = self {
|
||||
|
||||
|
||||
let alert = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
if let popoverController = alert.popoverPresentationController {
|
||||
popoverController.sourceView = view
|
||||
@ -324,11 +324,11 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
if let action = self.markBelowAsReadAlertAction(article, indexPath: indexPath, completion: completion) {
|
||||
alert.addAction(action)
|
||||
}
|
||||
|
||||
|
||||
if let action = self.discloseFeedAlertAction(article, completion: completion) {
|
||||
alert.addAction(action)
|
||||
}
|
||||
|
||||
|
||||
if let action = self.markAllInFeedAsReadAlertAction(article, indexPath: indexPath, completion: completion) {
|
||||
alert.addAction(action)
|
||||
}
|
||||
@ -347,28 +347,28 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
})
|
||||
|
||||
self.present(alert, animated: true)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
moreAction.image = AppAssets.moreImage
|
||||
moreAction.backgroundColor = UIColor.systemGray
|
||||
|
||||
return UISwipeActionsConfiguration(actions: [starAction, moreAction])
|
||||
|
||||
|
||||
}
|
||||
|
||||
override func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? {
|
||||
|
||||
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 }
|
||||
|
||||
|
||||
var menuElements = [UIMenuElement]()
|
||||
|
||||
|
||||
var markActions = [UIAction]()
|
||||
if let action = self.toggleArticleReadStatusAction(article) {
|
||||
markActions.append(action)
|
||||
@ -381,7 +381,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
markActions.append(action)
|
||||
}
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: markActions))
|
||||
|
||||
|
||||
var secondaryActions = [UIAction]()
|
||||
if let action = self.discloseFeedAction(article) {
|
||||
secondaryActions.append(action)
|
||||
@ -392,7 +392,7 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
if !secondaryActions.isEmpty {
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: secondaryActions))
|
||||
}
|
||||
|
||||
|
||||
var copyActions = [UIAction]()
|
||||
if let action = self.copyArticleURLAction(article) {
|
||||
copyActions.append(action)
|
||||
@ -403,19 +403,19 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
if !copyActions.isEmpty {
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: copyActions))
|
||||
}
|
||||
|
||||
|
||||
if let action = self.openInBrowserAction(article) {
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: [action]))
|
||||
}
|
||||
|
||||
|
||||
if let action = self.shareAction(article, indexPath: indexPath) {
|
||||
menuElements.append(UIMenu(title: "", options: .displayInline, children: [action]))
|
||||
}
|
||||
|
||||
|
||||
return UIMenu(title: "", children: menuElements)
|
||||
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return UITargetedPreview(view: cell, parameters: CroppingPreviewParameters(view: cell))
|
||||
}
|
||||
|
||||
@ -432,17 +432,17 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
let article = dataSource.itemIdentifier(for: indexPath)
|
||||
coordinator.selectArticle(article, animations: [.scroll, .select, .navigation])
|
||||
}
|
||||
|
||||
|
||||
override func scrollViewDidScroll(_ scrollView: UIScrollView) {
|
||||
scrollPositionQueue.add(self, #selector(scrollPositionDidChange))
|
||||
}
|
||||
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
@objc dynamic func unreadCountDidChange(_ notification: Notification) {
|
||||
updateUI()
|
||||
}
|
||||
|
||||
|
||||
@objc func statusesDidChange(_ note: Notification) {
|
||||
guard let articleIDs = note.userInfo?[Account.UserInfoKey.articleIDs] as? Set<String>, !articleIDs.isEmpty else {
|
||||
return
|
||||
@ -461,11 +461,11 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
}
|
||||
|
||||
@objc func feedIconDidBecomeAvailable(_ note: Notification) {
|
||||
|
||||
|
||||
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
|
||||
titleView.iconView.iconImage = coordinator.timelineIconImage
|
||||
}
|
||||
|
||||
|
||||
guard let feed = note.userInfo?[UserInfoKey.feed] as? Feed else {
|
||||
return
|
||||
}
|
||||
@ -519,27 +519,27 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
self.updateToolbar()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func contentSizeCategoryDidChange(_ note: Notification) {
|
||||
reloadAllVisibleCells()
|
||||
}
|
||||
|
||||
|
||||
@objc func displayNameDidChange(_ note: Notification) {
|
||||
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
|
||||
titleView.label.text = coordinator.timelineFeed?.nameForDisplay
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func willEnterForeground(_ note: Notification) {
|
||||
updateUI()
|
||||
}
|
||||
|
||||
|
||||
@objc func scrollPositionDidChange() {
|
||||
coordinator.timelineMiddleIndexPath = tableView.middleVisibleRow()
|
||||
}
|
||||
|
||||
|
||||
// MARK: Reloading
|
||||
|
||||
|
||||
func queueReloadAvailableCells() {
|
||||
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 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 {
|
||||
let layout = MainTimelineAccessibilityCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData)
|
||||
tableView.estimatedRowHeight = layout.height
|
||||
@ -574,9 +574,9 @@ class TimelineViewController: UITableViewController, UndoableCommandRunner {
|
||||
let layout = MainTimelineDefaultCellLayout(width: tableView.bounds.width, insets: tableView.safeAreaInsets, cellData: prototypeCellData)
|
||||
tableView.estimatedRowHeight = layout.height
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: Searching
|
||||
@ -619,7 +619,7 @@ private extension TimelineViewController {
|
||||
guard !(splitViewController?.isCollapsed ?? true) else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
guard let refreshProgressView = Bundle.main.loadNibNamed("RefreshProgressView", owner: self, options: nil)?[0] as? RefreshProgressView else {
|
||||
return
|
||||
}
|
||||
@ -630,7 +630,7 @@ private extension TimelineViewController {
|
||||
}
|
||||
|
||||
func resetUI(resetScroll: Bool) {
|
||||
|
||||
|
||||
title = coordinator.timelineFeed?.nameForDisplay ?? "Timeline"
|
||||
|
||||
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
|
||||
@ -641,7 +641,7 @@ private extension TimelineViewController {
|
||||
} else {
|
||||
titleView.iconView.tintColor = nil
|
||||
}
|
||||
|
||||
|
||||
titleView.label.text = coordinator.timelineFeed?.nameForDisplay
|
||||
updateTitleUnreadCount()
|
||||
|
||||
@ -652,7 +652,7 @@ private extension TimelineViewController {
|
||||
titleView.debuttonize()
|
||||
titleView.removeGestureRecognizer(feedTapGestureRecognizer)
|
||||
}
|
||||
|
||||
|
||||
navigationItem.titleView = titleView
|
||||
}
|
||||
|
||||
@ -662,7 +662,7 @@ private extension TimelineViewController {
|
||||
case .alwaysRead:
|
||||
navigationItem.rightBarButtonItem = nil
|
||||
}
|
||||
|
||||
|
||||
if coordinator.isReadArticlesFiltered {
|
||||
filterButton?.image = AppAssets.filterActiveImage
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
updateToolbar()
|
||||
}
|
||||
|
||||
|
||||
func updateToolbar() {
|
||||
guard firstUnreadButton != nil else { return }
|
||||
|
||||
|
||||
markAllAsReadButton.isEnabled = coordinator.isTimelineUnreadAvailable
|
||||
firstUnreadButton.isEnabled = coordinator.isTimelineUnreadAvailable
|
||||
|
||||
|
||||
if coordinator.isRootSplitCollapsed {
|
||||
if let toolbarItems = toolbarItems, toolbarItems.last != firstUnreadButton {
|
||||
var items = toolbarItems
|
||||
@ -702,20 +702,20 @@ private extension TimelineViewController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func updateTitleUnreadCount() {
|
||||
if let titleView = navigationItem.titleView as? MainTimelineTitleView {
|
||||
titleView.unreadCountView.unreadCount = coordinator.timelineUnreadCount
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func applyChanges(animated: Bool, completion: (() -> Void)? = nil) {
|
||||
if coordinator.articles.count == 0 {
|
||||
tableView.rowHeight = tableView.estimatedRowHeight
|
||||
} else {
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
}
|
||||
|
||||
|
||||
var snapshot = NSDiffableDataSourceSnapshot<Int, Article>()
|
||||
snapshot.appendSections([0])
|
||||
snapshot.appendItems(coordinator.articles, toSection: 0)
|
||||
@ -725,7 +725,7 @@ private extension TimelineViewController {
|
||||
completion?()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func makeDataSource() -> UITableViewDiffableDataSource<Int, Article> {
|
||||
let dataSource: UITableViewDiffableDataSource<Int, Article> =
|
||||
MainTimelineDataSource(tableView: tableView, cellProvider: { [weak self] tableView, indexPath, article in
|
||||
@ -736,7 +736,7 @@ private extension TimelineViewController {
|
||||
dataSource.defaultRowAnimation = .middle
|
||||
return dataSource
|
||||
}
|
||||
|
||||
|
||||
func configure(_ cell: MainTimelineTableViewCell, article: 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)
|
||||
|
||||
}
|
||||
|
||||
|
||||
func iconImageFor(_ article: Article) -> IconImage? {
|
||||
if !coordinator.showIcons {
|
||||
return nil
|
||||
}
|
||||
return article.iconImage()
|
||||
}
|
||||
|
||||
|
||||
func toggleArticleReadStatusAction(_ article: Article) -> UIAction? {
|
||||
guard !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 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)
|
||||
}
|
||||
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func toggleArticleStarStatusAction(_ article: Article) -> UIAction {
|
||||
|
||||
let title = article.status.starred ?
|
||||
@ -776,10 +776,10 @@ private extension TimelineViewController {
|
||||
NSLocalizedString("Mark as Starred", comment: "Mark as Starred")
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
return action
|
||||
}
|
||||
|
||||
@ -790,14 +790,14 @@ private extension TimelineViewController {
|
||||
|
||||
let title = NSLocalizedString("Mark Above as Read", comment: "Mark Above as Read")
|
||||
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
|
||||
self?.coordinator.markAboveAsRead(article)
|
||||
}
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func markBelowAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
|
||||
guard coordinator.canMarkBelowAsRead(for: article), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
||||
return nil
|
||||
@ -805,14 +805,14 @@ private extension TimelineViewController {
|
||||
|
||||
let title = NSLocalizedString("Mark Below as Read", comment: "Mark Below as Read")
|
||||
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
|
||||
self?.coordinator.markBelowAsRead(article)
|
||||
}
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
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 {
|
||||
return nil
|
||||
@ -823,7 +823,7 @@ private extension TimelineViewController {
|
||||
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
|
||||
self?.coordinator.markAboveAsRead(article)
|
||||
completion(true)
|
||||
@ -841,8 +841,8 @@ private extension TimelineViewController {
|
||||
let cancel = {
|
||||
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
|
||||
self?.coordinator.markBelowAsRead(article)
|
||||
completion(true)
|
||||
@ -854,26 +854,26 @@ private extension TimelineViewController {
|
||||
func discloseFeedAction(_ article: Article) -> UIAction? {
|
||||
guard let feed = article.feed,
|
||||
!coordinator.timelineFeedIsEqualTo(feed) else { return nil }
|
||||
|
||||
|
||||
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])
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func discloseFeedAlertAction(_ article: Article, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let feed = article.feed,
|
||||
!coordinator.timelineFeedIsEqualTo(feed) else { return nil }
|
||||
|
||||
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])
|
||||
completion(true)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func markAllInFeedAsReadAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
|
||||
guard let feed = article.feed else { return nil }
|
||||
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 {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
|
||||
let localizedMenuText = NSLocalizedString("Mark All as Read in “%@”", comment: "Command")
|
||||
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
|
||||
self?.coordinator.markAllAsRead(articles)
|
||||
}
|
||||
@ -902,19 +901,19 @@ private extension TimelineViewController {
|
||||
guard let fetchedArticles = try? feed.fetchArticles() else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
let articles = Array(fetchedArticles)
|
||||
guard articles.canMarkAllAsRead(), let contentView = self.tableView.cellForRow(at: indexPath)?.contentView else {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
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 cancel = {
|
||||
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
|
||||
self?.coordinator.markAllAsRead(articles)
|
||||
completion(true)
|
||||
@ -922,30 +921,29 @@ private extension TimelineViewController {
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func copyArticleURLAction(_ article: Article) -> UIAction? {
|
||||
guard let url = article.preferredURL else { return nil }
|
||||
let title = NSLocalizedString("Copy Article URL", comment: "Copy Article URL")
|
||||
let action = UIAction(title: title, image: AppAssets.copyImage) { action 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
|
||||
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) { _ in
|
||||
UIPasteboard.general.url = url
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
func openInBrowserAction(_ article: Article) -> UIAction? {
|
||||
guard let _ = article.preferredURL else { return nil }
|
||||
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)
|
||||
}
|
||||
return action
|
||||
@ -955,41 +953,41 @@ private extension TimelineViewController {
|
||||
guard let _ = article.preferredURL else { return nil }
|
||||
|
||||
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)
|
||||
completion(true)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func shareDialogForTableCell(indexPath: IndexPath, url: URL, title: String?) {
|
||||
let activityViewController = UIActivityViewController(url: url, title: title, applicationActivities: nil)
|
||||
|
||||
|
||||
guard let cell = tableView.cellForRow(at: indexPath) else { return }
|
||||
let popoverController = activityViewController.popoverPresentationController
|
||||
popoverController?.sourceView = cell
|
||||
popoverController?.sourceRect = CGRect(x: 0, y: 0, width: cell.frame.size.width, height: cell.frame.size.height)
|
||||
|
||||
|
||||
present(activityViewController, animated: true)
|
||||
}
|
||||
|
||||
|
||||
func shareAction(_ article: Article, indexPath: IndexPath) -> UIAction? {
|
||||
guard let url = article.preferredURL else { return nil }
|
||||
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)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
func shareAlertAction(_ article: Article, indexPath: IndexPath, completion: @escaping (Bool) -> Void) -> UIAlertAction? {
|
||||
guard let url = article.preferredURL else { return nil }
|
||||
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)
|
||||
self?.shareDialogForTableCell(indexPath: indexPath, url: url, title: article.title)
|
||||
}
|
||||
return action
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,28 +10,28 @@ import UIKit
|
||||
import Account
|
||||
|
||||
class RootSplitViewController: UISplitViewController {
|
||||
|
||||
|
||||
var coordinator: SceneCoordinator!
|
||||
|
||||
|
||||
override var prefersStatusBarHidden: Bool {
|
||||
return coordinator.prefersStatusBarHidden
|
||||
}
|
||||
|
||||
|
||||
override var preferredStatusBarUpdateAnimation: UIStatusBarAnimation {
|
||||
return .slide
|
||||
}
|
||||
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
coordinator.resetFocus()
|
||||
}
|
||||
|
||||
|
||||
override func show(_ column: UISplitViewController.Column) {
|
||||
guard !coordinator.isNavigationDisabled else { return }
|
||||
super.show(column)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Keyboard Shortcuts
|
||||
|
||||
|
||||
@objc func scrollOrGoToNextUnread(_ sender: Any?) {
|
||||
coordinator.scrollOrGoToNextUnread()
|
||||
}
|
||||
@ -39,26 +39,26 @@ class RootSplitViewController: UISplitViewController {
|
||||
@objc func scrollUp(_ sender: Any?) {
|
||||
coordinator.scrollUp()
|
||||
}
|
||||
|
||||
|
||||
@objc func goToPreviousUnread(_ sender: Any?) {
|
||||
coordinator.selectPrevUnread()
|
||||
}
|
||||
|
||||
|
||||
@objc func nextUnread(_ sender: Any?) {
|
||||
coordinator.selectNextUnread()
|
||||
}
|
||||
|
||||
|
||||
@objc func markRead(_ sender: Any?) {
|
||||
coordinator.markAsReadForCurrentArticle()
|
||||
}
|
||||
|
||||
|
||||
@objc func markUnreadAndGoToNextUnread(_ sender: Any?) {
|
||||
coordinator.markAsUnreadForCurrentArticle()
|
||||
coordinator.selectNextUnread()
|
||||
}
|
||||
|
||||
|
||||
@objc func markAllAsReadAndGoToNextUnread(_ sender: Any?) {
|
||||
coordinator.markAllAsReadInTimeline() {
|
||||
coordinator.markAllAsReadInTimeline {
|
||||
self.coordinator.selectNextUnread()
|
||||
}
|
||||
}
|
||||
@ -66,23 +66,23 @@ class RootSplitViewController: UISplitViewController {
|
||||
@objc func markAboveAsRead(_ sender: Any?) {
|
||||
coordinator.markAboveAsRead()
|
||||
}
|
||||
|
||||
|
||||
@objc func markBelowAsRead(_ sender: Any?) {
|
||||
coordinator.markBelowAsRead()
|
||||
}
|
||||
|
||||
|
||||
@objc func markUnread(_ sender: Any?) {
|
||||
coordinator.markAsUnreadForCurrentArticle()
|
||||
}
|
||||
|
||||
|
||||
@objc func goToPreviousSubscription(_ sender: Any?) {
|
||||
coordinator.selectPrevFeed()
|
||||
}
|
||||
|
||||
|
||||
@objc func goToNextSubscription(_ sender: Any?) {
|
||||
coordinator.selectNextFeed()
|
||||
}
|
||||
|
||||
|
||||
@objc func openInBrowser(_ sender: Any?) {
|
||||
coordinator.showBrowserForCurrentArticle()
|
||||
}
|
||||
@ -90,11 +90,11 @@ class RootSplitViewController: UISplitViewController {
|
||||
@objc func openInAppBrowser(_ sender: Any?) {
|
||||
coordinator.showInAppBrowser()
|
||||
}
|
||||
|
||||
|
||||
@objc func articleSearch(_ sender: Any?) {
|
||||
coordinator.showSearch()
|
||||
}
|
||||
|
||||
|
||||
@objc func addNewFeed(_ sender: Any?) {
|
||||
coordinator.showAddFeed()
|
||||
}
|
||||
@ -106,27 +106,27 @@ class RootSplitViewController: UISplitViewController {
|
||||
@objc func cleanUp(_ sender: Any?) {
|
||||
coordinator.cleanUp(conditional: false)
|
||||
}
|
||||
|
||||
|
||||
@objc func toggleReadFeedsFilter(_ sender: Any?) {
|
||||
coordinator.toggleReadFeedsFilter()
|
||||
}
|
||||
|
||||
|
||||
@objc func toggleReadArticlesFilter(_ sender: Any?) {
|
||||
coordinator.toggleReadArticlesFilter()
|
||||
}
|
||||
|
||||
|
||||
@objc func refresh(_ sender: Any?) {
|
||||
appDelegate.manualRefresh(errorHandler: ErrorHandler.present(self))
|
||||
}
|
||||
|
||||
|
||||
@objc func goToToday(_ sender: Any?) {
|
||||
coordinator.selectTodayFeed()
|
||||
}
|
||||
|
||||
|
||||
@objc func goToAllUnread(_ sender: Any?) {
|
||||
coordinator.selectAllUnreadFeed()
|
||||
}
|
||||
|
||||
|
||||
@objc func goToStarred(_ sender: Any?) {
|
||||
coordinator.selectStarredFeed()
|
||||
}
|
||||
@ -138,7 +138,7 @@ class RootSplitViewController: UISplitViewController {
|
||||
@objc func toggleRead(_ sender: Any?) {
|
||||
coordinator.toggleReadForCurrentArticle()
|
||||
}
|
||||
|
||||
|
||||
@objc func toggleStarred(_ sender: Any?) {
|
||||
coordinator.toggleStarredForCurrentArticle()
|
||||
}
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -11,18 +11,18 @@ import UserNotifications
|
||||
import Account
|
||||
|
||||
class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
|
||||
|
||||
var window: UIWindow?
|
||||
var coordinator: SceneCoordinator!
|
||||
|
||||
|
||||
// UIWindowScene delegate
|
||||
|
||||
|
||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||
|
||||
|
||||
window!.tintColor = AppAssets.primaryAccentColor
|
||||
updateUserInterfaceStyle()
|
||||
UINavigationBar.appearance().scrollEdgeAppearance = UINavigationBarAppearance()
|
||||
|
||||
|
||||
let rootViewController = window!.rootViewController as! RootSplitViewController
|
||||
rootViewController.presentsWithGesture = true
|
||||
rootViewController.showsSecondaryOnlyButton = true
|
||||
@ -38,43 +38,43 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
coordinator = SceneCoordinator(rootSplitViewController: rootViewController)
|
||||
rootViewController.coordinator = coordinator
|
||||
rootViewController.delegate = coordinator
|
||||
|
||||
|
||||
coordinator.restoreWindowState(session.stateRestorationActivity)
|
||||
|
||||
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let shortcutItem = connectionOptions.shortcutItem {
|
||||
handleShortcutItem(shortcutItem)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let notificationResponse = connectionOptions.notificationResponse {
|
||||
coordinator.handle(notificationResponse)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity {
|
||||
coordinator.handle(userActivity)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) {
|
||||
appDelegate.resumeDatabaseProcessingIfNecessary()
|
||||
handleShortcutItem(shortcutItem)
|
||||
completionHandler(true)
|
||||
}
|
||||
|
||||
|
||||
func scene(_ scene: UIScene, continue userActivity: NSUserActivity) {
|
||||
appDelegate.resumeDatabaseProcessingIfNecessary()
|
||||
coordinator.handle(userActivity)
|
||||
}
|
||||
|
||||
|
||||
func sceneDidEnterBackground(_ scene: UIScene) {
|
||||
ArticleStringFormatter.emptyCaches()
|
||||
appDelegate.prepareAccountsForBackground()
|
||||
@ -85,39 +85,39 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
appDelegate.prepareAccountsForForeground()
|
||||
coordinator.resetFocus()
|
||||
}
|
||||
|
||||
|
||||
func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? {
|
||||
return coordinator.stateRestorationActivity
|
||||
}
|
||||
|
||||
|
||||
// API
|
||||
|
||||
|
||||
func handle(_ response: UNNotificationResponse) {
|
||||
appDelegate.resumeDatabaseProcessingIfNecessary()
|
||||
coordinator.handle(response)
|
||||
}
|
||||
|
||||
|
||||
func suspend() {
|
||||
coordinator.suspend()
|
||||
}
|
||||
|
||||
|
||||
func cleanUp(conditional: Bool) {
|
||||
coordinator.cleanUp(conditional: conditional)
|
||||
}
|
||||
|
||||
|
||||
// Handle Opening of URLs
|
||||
|
||||
|
||||
func scene(_ scene: UIScene, openURLContexts urlContexts: Set<UIOpenURLContext>) {
|
||||
guard let context = urlContexts.first else { return }
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
|
||||
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||
self.coordinator.dismissIfLaunchingFromExternalAction()
|
||||
}
|
||||
|
||||
|
||||
let urlString = context.url.absoluteString
|
||||
|
||||
|
||||
// Handle the feed: and feeds: schemes
|
||||
if urlString.starts(with: "feed:") || urlString.starts(with: "feeds:") {
|
||||
let normalizedURLString = urlString.normalizedURL
|
||||
@ -125,7 +125,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
self.coordinator.showAddFeed(initialFeed: normalizedURLString, initialFeedName: nil)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show Unread View or Article
|
||||
if urlString.contains(WidgetDeepLink.unread.url.absoluteString) {
|
||||
guard let comps = URLComponents(string: urlString ) else { return }
|
||||
@ -134,14 +134,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
if AccountManager.shared.isSuspended {
|
||||
AccountManager.shared.resumeAll()
|
||||
}
|
||||
self.coordinator.selectAllUnreadFeed() {
|
||||
self.coordinator.selectAllUnreadFeed {
|
||||
self.coordinator.selectArticleInCurrentFeed(id!)
|
||||
}
|
||||
} else {
|
||||
self.coordinator.selectAllUnreadFeed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show Today View or Article
|
||||
if urlString.contains(WidgetDeepLink.today.url.absoluteString) {
|
||||
guard let comps = URLComponents(string: urlString ) else { return }
|
||||
@ -150,14 +150,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
if AccountManager.shared.isSuspended {
|
||||
AccountManager.shared.resumeAll()
|
||||
}
|
||||
self.coordinator.selectTodayFeed() {
|
||||
self.coordinator.selectTodayFeed {
|
||||
self.coordinator.selectArticleInCurrentFeed(id!)
|
||||
}
|
||||
} else {
|
||||
self.coordinator.selectTodayFeed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Show Starred View or Article
|
||||
if urlString.contains(WidgetDeepLink.starred.url.absoluteString) {
|
||||
guard let comps = URLComponents(string: urlString ) else { return }
|
||||
@ -166,38 +166,38 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
if AccountManager.shared.isSuspended {
|
||||
AccountManager.shared.resumeAll()
|
||||
}
|
||||
self.coordinator.selectStarredFeed() {
|
||||
self.coordinator.selectStarredFeed {
|
||||
self.coordinator.selectArticleInCurrentFeed(id!)
|
||||
}
|
||||
} else {
|
||||
self.coordinator.selectStarredFeed()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let filename = context.url.standardizedFileURL.path
|
||||
if filename.hasSuffix(ArticleTheme.nnwThemeSuffix) {
|
||||
self.coordinator.importTheme(filename: filename)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// Handle theme URLs: netnewswire://theme/add?url={url}
|
||||
guard let comps = URLComponents(url: context.url, resolvingAgainstBaseURL: false),
|
||||
"theme" == comps.host,
|
||||
let queryItems = comps.queryItems else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
if let providedThemeURL = queryItems.first(where: { $0.name == "url" })?.value {
|
||||
if let themeURL = URL(string: providedThemeURL) {
|
||||
let request = URLRequest(url: themeURL)
|
||||
|
||||
|
||||
DispatchQueue.main.async {
|
||||
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
|
||||
let location = location else { return }
|
||||
|
||||
|
||||
do {
|
||||
try ArticleThemeDownloader.shared.handleFile(at: location)
|
||||
} catch {
|
||||
@ -212,14 +212,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||
} else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension SceneDelegate {
|
||||
|
||||
|
||||
func handleShortcutItem(_ shortcutItem: UIApplicationShortcutItem) {
|
||||
switch shortcutItem.type {
|
||||
case "com.ranchero.NetNewsWire.FirstUnread":
|
||||
@ -232,11 +231,11 @@ private extension SceneDelegate {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@objc func userDefaultsDidChange() {
|
||||
updateUserInterfaceStyle()
|
||||
}
|
||||
|
||||
|
||||
func updateUserInterfaceStyle() {
|
||||
DispatchQueue.main.async {
|
||||
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 thanksTextView: UITextView!
|
||||
@IBOutlet weak var dedicationTextView: UITextView!
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
configureCell(file: "About", textView: aboutTextView)
|
||||
configureCell(file: "Credits", textView: creditsTextView)
|
||||
configureCell(file: "Thanks", textView: thanksTextView)
|
||||
@ -32,7 +32,7 @@ class AboutViewController: UITableViewController {
|
||||
buildLabel.numberOfLines = 0
|
||||
buildLabel.sizeToFit()
|
||||
buildLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
|
||||
let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0))
|
||||
wrapperView.translatesAutoresizingMaskIntoConstraints = false
|
||||
wrapperView.addSubview(buildLabel)
|
||||
@ -42,11 +42,11 @@ class AboutViewController: UITableViewController {
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return UITableView.automaticDimension
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private extension AboutViewController {
|
||||
|
||||
|
||||
func configureCell(file: String, textView: UITextView) {
|
||||
let url = Bundle.main.url(forResource: file, withExtension: "rtf")!
|
||||
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.font = .preferredFont(forTextStyle: .body)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
|
||||
case icloud
|
||||
case web
|
||||
case selfhosted
|
||||
|
||||
|
||||
var sectionHeader: String {
|
||||
switch self {
|
||||
case .local:
|
||||
@ -34,7 +34,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
|
||||
return NSLocalizedString("Self-hosted", comment: "Self hosted Account")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var sectionFooter: String {
|
||||
switch self {
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var sectionContent: [AccountType] {
|
||||
switch self {
|
||||
case .local:
|
||||
@ -65,35 +65,31 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
}
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return AddAccountSections.allCases.count
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
if section == AddAccountSections.local.rawValue {
|
||||
return AddAccountSections.local.sectionContent.count
|
||||
}
|
||||
|
||||
|
||||
if section == AddAccountSections.icloud.rawValue {
|
||||
return AddAccountSections.icloud.sectionContent.count
|
||||
}
|
||||
|
||||
|
||||
if section == AddAccountSections.web.rawValue {
|
||||
return AddAccountSections.web.sectionContent.count
|
||||
}
|
||||
|
||||
|
||||
if section == AddAccountSections.selfhosted.rawValue {
|
||||
return AddAccountSections.selfhosted.sectionContent.count
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
|
||||
switch section {
|
||||
case AddAccountSections.local.rawValue:
|
||||
@ -108,7 +104,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
|
||||
switch section {
|
||||
case AddAccountSections.local.rawValue:
|
||||
@ -123,10 +119,10 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "SettingsAccountTableViewCell", for: indexPath) as! SettingsComboTableViewCell
|
||||
|
||||
|
||||
switch indexPath.section {
|
||||
case AddAccountSections.local.rawValue:
|
||||
cell.comboNameLabel?.text = AddAccountSections.local.sectionContent[indexPath.row].localizedAccountName()
|
||||
@ -149,15 +145,15 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
|
||||
case AddAccountSections.selfhosted.rawValue:
|
||||
cell.comboNameLabel?.text = AddAccountSections.selfhosted.sectionContent[indexPath.row].localizedAccountName()
|
||||
cell.comboImage?.image = AppAssets.image(for: AddAccountSections.selfhosted.sectionContent[indexPath.row])
|
||||
|
||||
|
||||
default:
|
||||
return cell
|
||||
}
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
|
||||
|
||||
switch indexPath.section {
|
||||
case AddAccountSections.local.rawValue:
|
||||
let type = AddAccountSections.local.sectionContent[indexPath.row]
|
||||
@ -175,7 +171,7 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private func presentController(for accountType: AccountType) {
|
||||
switch accountType {
|
||||
case .onMyMac:
|
||||
@ -216,18 +212,18 @@ class AddAccountViewController: UITableViewController, AddAccountDismissDelegate
|
||||
present(navController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func dismiss() {
|
||||
navigationController?.popViewController(animated: false)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
extension AddAccountViewController: OAuthAccountAuthorizationOperationDelegate {
|
||||
|
||||
|
||||
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didCreate account: Account) {
|
||||
let rootViewController = view.window?.rootViewController
|
||||
|
||||
|
||||
account.refreshAll { result in
|
||||
switch result {
|
||||
case .success:
|
||||
@ -239,10 +235,10 @@ extension AddAccountViewController: OAuthAccountAuthorizationOperationDelegate {
|
||||
viewController.presentError(error)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
||||
|
||||
func oauthAccountAuthorizationOperation(_ operation: OAuthAccountAuthorizationOperation, didFailWith error: Error) {
|
||||
presentError(error)
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
|
||||
struct ArticleThemeImporter {
|
||||
|
||||
|
||||
static func importTheme(controller: UIViewController, url: URL) throws {
|
||||
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 alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
|
||||
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
||||
alertController.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
|
||||
|
||||
|
||||
if let websiteURL = URL(string: theme.creatorHomePage) {
|
||||
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)
|
||||
try? Self.importTheme(controller: controller, url: url)
|
||||
}
|
||||
@ -49,7 +49,7 @@ struct ArticleThemeImporter {
|
||||
}
|
||||
|
||||
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) {
|
||||
let title = NSLocalizedString("Duplicate Theme", comment: "Duplicate Theme")
|
||||
@ -61,7 +61,7 @@ struct ArticleThemeImporter {
|
||||
let cancelTitle = NSLocalizedString("Cancel", comment: "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()
|
||||
}
|
||||
alertController.addAction(overwriteAction)
|
||||
@ -71,32 +71,32 @@ struct ArticleThemeImporter {
|
||||
} else {
|
||||
importTheme()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
alertController.addAction(installThemeAction)
|
||||
alertController.preferredAction = installThemeAction
|
||||
|
||||
controller.present(alertController, animated: true)
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private extension ArticleThemeImporter {
|
||||
|
||||
|
||||
static func confirmImportSuccess(controller: UIViewController, themeName: String) {
|
||||
let title = NSLocalizedString("Theme 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 alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
|
||||
|
||||
let doneTitle = NSLocalizedString("Done", comment: "Done")
|
||||
alertController.addAction(UIAlertAction(title: doneTitle, style: .default))
|
||||
|
||||
|
||||
controller.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -17,15 +17,15 @@ extension UTType {
|
||||
class ArticleThemesTableViewController: UITableViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
let importBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(importTheme(_:)));
|
||||
importBarButtonItem.title = NSLocalizedString("Import Theme", comment: "Import Theme");
|
||||
let importBarButtonItem = UIBarButtonItem(barButtonSystemItem: .add, target: self, action: #selector(importTheme(_:)))
|
||||
importBarButtonItem.title = NSLocalizedString("Import Theme", comment: "Import Theme")
|
||||
navigationItem.rightBarButtonItem = importBarButtonItem
|
||||
|
||||
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil)
|
||||
}
|
||||
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
|
||||
@objc func articleThemeNamesDidChangeNotification(_ note: Notification) {
|
||||
tableView.reloadData()
|
||||
}
|
||||
@ -49,21 +49,21 @@ class ArticleThemesTableViewController: UITableViewController {
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
|
||||
|
||||
|
||||
let themeName: String
|
||||
if indexPath.row == 0 {
|
||||
themeName = ArticleTheme.defaultTheme.name
|
||||
} else {
|
||||
themeName = ArticleThemesManager.shared.themeNames[indexPath.row - 1]
|
||||
}
|
||||
|
||||
|
||||
cell.textLabel?.text = themeName
|
||||
if themeName == ArticleThemesManager.shared.currentTheme.name {
|
||||
cell.accessoryType = .checkmark
|
||||
} else {
|
||||
cell.accessoryType = .none
|
||||
}
|
||||
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
@ -80,33 +80,33 @@ class ArticleThemesTableViewController: UITableViewController {
|
||||
!theme.isAppTheme else { return nil }
|
||||
|
||||
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 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 alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
|
||||
|
||||
|
||||
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)
|
||||
}
|
||||
alertController.addAction(cancelAction)
|
||||
|
||||
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)
|
||||
completion(true)
|
||||
}
|
||||
alertController.addAction(deleteAction)
|
||||
|
||||
|
||||
self?.present(alertController, animated: true)
|
||||
}
|
||||
|
||||
|
||||
deleteAction.image = AppAssets.trashImage
|
||||
deleteAction.backgroundColor = UIColor.systemRed
|
||||
|
||||
|
||||
return UISwipeActionsConfiguration(actions: [deleteAction])
|
||||
}
|
||||
}
|
||||
@ -114,12 +114,12 @@ class ArticleThemesTableViewController: UITableViewController {
|
||||
// MARK: UIDocumentPickerDelegate
|
||||
|
||||
extension ArticleThemesTableViewController: UIDocumentPickerDelegate {
|
||||
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
guard let url = urls.first else { return }
|
||||
|
||||
if url.startAccessingSecurityScopedResource() {
|
||||
|
||||
|
||||
defer {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ class SettingsComboTableViewCell: VibrantTableViewCell {
|
||||
override func updateVibrancy(animated: Bool) {
|
||||
super.updateVibrancy(animated: animated)
|
||||
updateLabelVibrancy(comboNameLabel, color: labelColor, animated: animated)
|
||||
|
||||
|
||||
let tintColor = isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label
|
||||
if animated {
|
||||
UIView.animate(withDuration: Self.duration) {
|
||||
@ -26,5 +26,5 @@ class SettingsComboTableViewCell: VibrantTableViewCell {
|
||||
self.comboImage?.tintColor = tintColor
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -16,7 +16,7 @@ import UniformTypeIdentifiers
|
||||
class SettingsViewController: UITableViewController {
|
||||
|
||||
private weak var opmlAccount: Account?
|
||||
|
||||
|
||||
@IBOutlet weak var timelineSortOrderSwitch: UISwitch!
|
||||
@IBOutlet weak var groupByFeedSwitch: UISwitch!
|
||||
@IBOutlet weak var refreshClearsReadArticlesSwitch: UISwitch!
|
||||
@ -25,10 +25,10 @@ class SettingsViewController: UITableViewController {
|
||||
@IBOutlet weak var showFullscreenArticlesSwitch: UISwitch!
|
||||
@IBOutlet weak var colorPaletteDetailLabel: UILabel!
|
||||
@IBOutlet weak var openLinksInNetNewsWire: UISwitch!
|
||||
|
||||
|
||||
var scrollToArticlesSection = false
|
||||
weak var presentingParentController: UIViewController?
|
||||
|
||||
|
||||
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/
|
||||
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: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell")
|
||||
|
||||
|
||||
tableView.rowHeight = UITableView.automaticDimension
|
||||
tableView.estimatedRowHeight = 44
|
||||
}
|
||||
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
|
||||
|
||||
if AppDefaults.shared.timelineSortDirection == .orderedAscending {
|
||||
timelineSortOrderSwitch.isOn = true
|
||||
} else {
|
||||
@ -66,7 +66,6 @@ class SettingsViewController: UITableViewController {
|
||||
refreshClearsReadArticlesSwitch.isOn = false
|
||||
}
|
||||
|
||||
|
||||
articleThemeDetailLabel.text = ArticleThemesManager.shared.currentTheme.name
|
||||
|
||||
if AppDefaults.shared.confirmMarkAllAsRead {
|
||||
@ -80,11 +79,10 @@ class SettingsViewController: UITableViewController {
|
||||
} else {
|
||||
showFullscreenArticlesSwitch.isOn = false
|
||||
}
|
||||
|
||||
|
||||
colorPaletteDetailLabel.text = String(describing: AppDefaults.userInterfaceColorPalette)
|
||||
|
||||
|
||||
openLinksInNetNewsWire.isOn = !AppDefaults.shared.useSystemBrowser
|
||||
|
||||
|
||||
let buildLabel = NonIntrinsicLabel(frame: CGRect(x: 32.0, y: 0.0, width: 0.0, height: 0.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.sizeToFit()
|
||||
buildLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
|
||||
let wrapperView = UIView(frame: CGRect(x: 0, y: 0, width: buildLabel.frame.width, height: buildLabel.frame.height + 10.0))
|
||||
wrapperView.translatesAutoresizingMaskIntoConstraints = false
|
||||
wrapperView.addSubview(buildLabel)
|
||||
tableView.tableFooterView = wrapperView
|
||||
|
||||
}
|
||||
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
self.tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
|
||||
|
||||
if scrollToArticlesSection {
|
||||
tableView.scrollToRow(at: IndexPath(row: 0, section: 4), at: .top, animated: true)
|
||||
scrollToArticlesSection = false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// MARK: UITableView
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
|
||||
switch section {
|
||||
@ -237,7 +235,7 @@ class SettingsViewController: UITableViewController {
|
||||
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
|
||||
return false
|
||||
}
|
||||
@ -245,21 +243,21 @@ class SettingsViewController: UITableViewController {
|
||||
override func tableView(_ tableView: UITableView, editingStyleForRowAt indexPath: IndexPath) -> UITableViewCell.EditingStyle {
|
||||
return .none
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat {
|
||||
return UITableView.automaticDimension
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, indentationLevelForRowAt indexPath: IndexPath) -> Int {
|
||||
return super.tableView(tableView, indentationLevelForRowAt: IndexPath(row: 0, section: 1))
|
||||
}
|
||||
|
||||
|
||||
// MARK: Actions
|
||||
|
||||
|
||||
@IBAction func done(_ sender: Any) {
|
||||
dismiss(animated: true)
|
||||
}
|
||||
|
||||
|
||||
@IBAction func switchTimelineOrder(_ sender: Any) {
|
||||
if timelineSortOrderSwitch.isOn {
|
||||
AppDefaults.shared.timelineSortDirection = .orderedAscending
|
||||
@ -267,7 +265,7 @@ class SettingsViewController: UITableViewController {
|
||||
AppDefaults.shared.timelineSortDirection = .orderedDescending
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func switchGroupByFeed(_ sender: Any) {
|
||||
if groupByFeedSwitch.isOn {
|
||||
AppDefaults.shared.timelineGroupByFeed = true
|
||||
@ -275,7 +273,7 @@ class SettingsViewController: UITableViewController {
|
||||
AppDefaults.shared.timelineGroupByFeed = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func switchClearsReadArticles(_ sender: Any) {
|
||||
if refreshClearsReadArticlesSwitch.isOn {
|
||||
AppDefaults.shared.refreshClearsReadArticles = true
|
||||
@ -283,7 +281,7 @@ class SettingsViewController: UITableViewController {
|
||||
AppDefaults.shared.refreshClearsReadArticles = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func switchConfirmMarkAllAsRead(_ sender: Any) {
|
||||
if confirmMarkAllAsReadSwitch.isOn {
|
||||
AppDefaults.shared.confirmMarkAllAsRead = true
|
||||
@ -291,7 +289,7 @@ class SettingsViewController: UITableViewController {
|
||||
AppDefaults.shared.confirmMarkAllAsRead = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func switchFullscreenArticles(_ sender: Any) {
|
||||
if showFullscreenArticlesSwitch.isOn {
|
||||
AppDefaults.shared.articleFullscreenAvailable = true
|
||||
@ -299,7 +297,7 @@ class SettingsViewController: UITableViewController {
|
||||
AppDefaults.shared.articleFullscreenAvailable = false
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBAction func switchBrowserPreference(_ sender: Any) {
|
||||
if openLinksInNetNewsWire.isOn {
|
||||
AppDefaults.shared.useSystemBrowser = false
|
||||
@ -307,14 +305,13 @@ class SettingsViewController: UITableViewController {
|
||||
AppDefaults.shared.useSystemBrowser = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// MARK: Notifications
|
||||
|
||||
|
||||
@objc func contentSizeCategoryDidChange() {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
|
||||
@objc func accountsDidChange() {
|
||||
tableView.reloadData()
|
||||
}
|
||||
@ -322,17 +319,17 @@ class SettingsViewController: UITableViewController {
|
||||
@objc func displayNameDidChange() {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
|
||||
@objc func browserPreferenceDidChange() {
|
||||
tableView.reloadData()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: OPML Document Picker
|
||||
|
||||
extension SettingsViewController: UIDocumentPickerDelegate {
|
||||
|
||||
|
||||
func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) {
|
||||
for url in urls {
|
||||
opmlAccount?.importOPML(url) { result in
|
||||
@ -347,13 +344,13 @@ extension SettingsViewController: UIDocumentPickerDelegate {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension SettingsViewController {
|
||||
|
||||
|
||||
func addFeed() {
|
||||
self.dismiss(animated: true)
|
||||
|
||||
@ -363,10 +360,10 @@ private extension SettingsViewController {
|
||||
addViewController.initialFeedName = NSLocalizedString("NetNewsWire News", comment: "NetNewsWire News")
|
||||
addNavViewController.modalPresentationStyle = .formSheet
|
||||
addNavViewController.preferredContentSize = AddFeedViewController.preferredContentSizeForFormSheetDisplay
|
||||
|
||||
|
||||
presentingParentController?.present(addNavViewController, animated: true)
|
||||
}
|
||||
|
||||
|
||||
func importOPML(sourceView: UIView, sourceRect: CGRect) {
|
||||
switch AccountManager.shared.activeAccounts.count {
|
||||
case 0:
|
||||
@ -378,18 +375,18 @@ private extension SettingsViewController {
|
||||
importOPMLAccountPicker(sourceView: sourceView, sourceRect: sourceRect)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func importOPMLAccountPicker(sourceView: UIView, sourceRect: CGRect) {
|
||||
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)
|
||||
|
||||
|
||||
if let popoverController = alert.popoverPresentationController {
|
||||
popoverController.sourceView = view
|
||||
popoverController.sourceRect = sourceRect
|
||||
}
|
||||
|
||||
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?.importOPMLDocumentPicker()
|
||||
}
|
||||
@ -401,15 +398,15 @@ private extension SettingsViewController {
|
||||
|
||||
self.present(alert, animated: true)
|
||||
}
|
||||
|
||||
|
||||
func importOPMLDocumentPicker() {
|
||||
|
||||
|
||||
let documentPicker = UIDocumentPickerViewController(forOpeningContentTypes: [UTType.opml, UTType.xml], asCopy: true)
|
||||
documentPicker.delegate = self
|
||||
documentPicker.modalPresentationStyle = .formSheet
|
||||
self.present(documentPicker, animated: true)
|
||||
}
|
||||
|
||||
|
||||
func exportOPML(sourceView: UIView, sourceRect: CGRect) {
|
||||
if AccountManager.shared.accounts.count == 1 {
|
||||
opmlAccount = AccountManager.shared.accounts.first!
|
||||
@ -418,18 +415,18 @@ private extension SettingsViewController {
|
||||
exportOPMLAccountPicker(sourceView: sourceView, sourceRect: sourceRect)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func exportOPMLAccountPicker(sourceView: UIView, sourceRect: CGRect) {
|
||||
let title = NSLocalizedString("Choose an account with the subscriptions to export", comment: "Export Account")
|
||||
let alert = UIAlertController(title: title, message: nil, preferredStyle: .actionSheet)
|
||||
|
||||
|
||||
if let popoverController = alert.popoverPresentationController {
|
||||
popoverController.sourceView = view
|
||||
popoverController.sourceRect = sourceRect
|
||||
}
|
||||
|
||||
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?.exportOPMLDocumentPicker()
|
||||
}
|
||||
@ -441,10 +438,10 @@ private extension SettingsViewController {
|
||||
|
||||
self.present(alert, animated: true)
|
||||
}
|
||||
|
||||
|
||||
func exportOPMLDocumentPicker() {
|
||||
guard let account = opmlAccount else { return }
|
||||
|
||||
|
||||
let accountName = account.nameForDisplay.replacingOccurrences(of: " ", with: "").trimmingCharacters(in: .whitespaces)
|
||||
let filename = "Subscriptions-\(accountName).opml"
|
||||
let tempFile = FileManager.default.temporaryDirectory.appendingPathComponent(filename)
|
||||
@ -454,16 +451,16 @@ private extension SettingsViewController {
|
||||
} catch {
|
||||
self.presentError(title: "OPML Export Error", message: error.localizedDescription)
|
||||
}
|
||||
|
||||
|
||||
let documentPicker = UIDocumentPickerViewController(forExporting: [tempFile])
|
||||
documentPicker.modalPresentationStyle = .formSheet
|
||||
self.present(documentPicker, animated: true)
|
||||
}
|
||||
|
||||
|
||||
func openURL(_ urlString: String) {
|
||||
let vc = SFSafariViewController(url: URL(string: urlString)!)
|
||||
vc.modalPresentationStyle = .pageSheet
|
||||
present(vc, animated: true)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -14,15 +14,15 @@ class TimelineCustomizerViewController: UIViewController {
|
||||
@IBOutlet weak var iconSizeSlider: TickMarkSlider!
|
||||
@IBOutlet weak var numberOfLinesSliderContainerView: UIView!
|
||||
@IBOutlet weak var numberOfLinesSlider: TickMarkSlider!
|
||||
|
||||
|
||||
@IBOutlet weak var previewWidthConstraint: NSLayoutConstraint!
|
||||
@IBOutlet weak var previewHeightConstraint: NSLayoutConstraint!
|
||||
|
||||
|
||||
@IBOutlet weak var previewContainerView: UIView!
|
||||
var previewController: TimelinePreviewTableViewController {
|
||||
return children.first as! TimelinePreviewTableViewController
|
||||
}
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
@ -34,13 +34,13 @@ class TimelineCustomizerViewController: UIViewController {
|
||||
numberOfLinesSlider.value = Float(AppDefaults.shared.timelineNumberOfLines)
|
||||
numberOfLinesSlider.addTickMarks()
|
||||
}
|
||||
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
updatePreviewBorder()
|
||||
updatePreview()
|
||||
}
|
||||
|
||||
|
||||
override func traitCollectionDidChange(_ previousTraitCollection: UITraitCollection?) {
|
||||
updatePreviewBorder()
|
||||
updatePreview()
|
||||
@ -51,18 +51,18 @@ class TimelineCustomizerViewController: UIViewController {
|
||||
AppDefaults.shared.timelineIconSize = iconSize
|
||||
updatePreview()
|
||||
}
|
||||
|
||||
|
||||
@IBAction func numberOfLinesChanged(_ sender: Any) {
|
||||
AppDefaults.shared.timelineNumberOfLines = Int(numberOfLinesSlider.value.rounded())
|
||||
updatePreview()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// MARK: Private
|
||||
|
||||
private extension TimelineCustomizerViewController {
|
||||
|
||||
|
||||
func updatePreview() {
|
||||
let previewWidth: CGFloat = {
|
||||
if traitCollection.userInterfaceIdiom == .phone {
|
||||
@ -71,13 +71,13 @@ private extension TimelineCustomizerViewController {
|
||||
return view.bounds.width / 1.5
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
previewWidthConstraint.constant = previewWidth
|
||||
previewHeightConstraint.constant = previewController.heightFor(width: previewWidth)
|
||||
|
||||
|
||||
previewController.reload()
|
||||
}
|
||||
|
||||
|
||||
func updatePreviewBorder() {
|
||||
if traitCollection.userInterfaceStyle == .dark {
|
||||
previewContainerView.layer.borderColor = UIColor.black.cgColor
|
||||
@ -86,5 +86,5 @@ private extension TimelineCustomizerViewController {
|
||||
previewContainerView.layer.borderWidth = 0
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -12,13 +12,13 @@ import Articles
|
||||
class TimelinePreviewTableViewController: UIViewController, UITableViewDelegate, UITableViewDataSource {
|
||||
|
||||
@IBOutlet weak var tableView: UITableView!
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
|
||||
tableView.delegate = self
|
||||
tableView.dataSource = self
|
||||
|
||||
|
||||
}
|
||||
|
||||
func heightFor(width: CGFloat) -> CGFloat {
|
||||
@ -46,7 +46,7 @@ class TimelinePreviewTableViewController: UIViewController, UITableViewDelegate,
|
||||
cell.cellData = prototypeCellData
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
||||
}
|
||||
@ -64,14 +64,14 @@ private extension TimelinePreviewTableViewController {
|
||||
|
||||
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 prototypeID = "prototype"
|
||||
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 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)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
|
||||
class ShareFolderPickerCell: UITableViewCell {
|
||||
|
||||
|
||||
@IBOutlet weak var icon: UIImageView!
|
||||
@IBOutlet weak var label: UILabel!
|
||||
}
|
||||
|
@ -20,13 +20,13 @@ class ShareFolderPickerController: UITableViewController {
|
||||
var selectedContainerID: ContainerIdentifier?
|
||||
|
||||
weak var delegate: ShareFolderPickerControllerDelegate?
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
tableView.register(UINib(nibName: "ShareFolderPickerAccountCell", bundle: Bundle.main), forCellReuseIdentifier: "AccountCell")
|
||||
tableView.register(UINib(nibName: "ShareFolderPickerFolderCell", bundle: Bundle.main), forCellReuseIdentifier: "FolderCell")
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
override func numberOfSections(in tableView: UITableView) -> Int {
|
||||
return 1
|
||||
}
|
||||
@ -34,7 +34,7 @@ class ShareFolderPickerController: UITableViewController {
|
||||
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
|
||||
return containers?.count ?? 0
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
||||
let container = containers?[indexPath.row]
|
||||
let cell: ShareFolderPickerCell = {
|
||||
@ -44,7 +44,7 @@ class ShareFolderPickerController: UITableViewController {
|
||||
return tableView.dequeueReusableCell(withIdentifier: "FolderCell", for: indexPath) as! ShareFolderPickerCell
|
||||
}
|
||||
}()
|
||||
|
||||
|
||||
if let account = container as? ExtensionAccount {
|
||||
cell.icon.image = AppAssets.image(for: account.type)
|
||||
} else {
|
||||
@ -58,13 +58,13 @@ class ShareFolderPickerController: UITableViewController {
|
||||
} else {
|
||||
cell.accessoryType = .none
|
||||
}
|
||||
|
||||
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
||||
guard let container = containers?[indexPath.row] else { return }
|
||||
|
||||
|
||||
if let account = container as? ExtensionAccount, account.disallowFeedInRootFolder {
|
||||
tableView.selectRow(at: nil, animated: false, scrollPosition: .none)
|
||||
} else {
|
||||
@ -73,5 +73,5 @@ class ShareFolderPickerController: UITableViewController {
|
||||
delegate?.shareFolderPickerDidSelect(container)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -15,15 +15,15 @@ import RSTree
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
class ShareViewController: SLComposeServiceViewController, ShareFolderPickerControllerDelegate {
|
||||
|
||||
|
||||
private var url: URL?
|
||||
private var extensionContainers: ExtensionContainers?
|
||||
private var flattenedContainers: [ExtensionContainer]!
|
||||
private var selectedContainer: ExtensionContainer?
|
||||
private var folderItem: SLComposeSheetConfigurationItem!
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
|
||||
|
||||
extensionContainers = ExtensionContainersFile.read()
|
||||
flattenedContainers = extensionContainers?.flattened ?? [ExtensionContainer]()
|
||||
if let extensionContainers = extensionContainers {
|
||||
@ -42,7 +42,7 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
||||
tableView.rowHeight = 38
|
||||
}
|
||||
|
||||
var provider: NSItemProvider? = nil
|
||||
var provider: NSItemProvider?
|
||||
|
||||
// Try to get any HTML that is maybe passed in
|
||||
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
|
||||
if error != nil {
|
||||
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
|
||||
if error != nil {
|
||||
return
|
||||
@ -92,32 +92,32 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Reddit in particular doesn't pass the URL correctly and instead puts it in the contentText
|
||||
url = URL(string: contentText)
|
||||
}
|
||||
|
||||
|
||||
override func isContentValid() -> Bool {
|
||||
return url != nil && selectedContainer != nil
|
||||
}
|
||||
|
||||
|
||||
override func didSelectPost() {
|
||||
guard let url = url, let selectedContainer = selectedContainer, let containerID = selectedContainer.containerID else {
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
return
|
||||
}
|
||||
|
||||
var name: String? = nil
|
||||
var name: String?
|
||||
if !contentText.mayBeURL {
|
||||
name = contentText.isEmpty ? nil : contentText
|
||||
}
|
||||
|
||||
|
||||
let request = ExtensionFeedAddRequest(name: name, feedURL: url, destinationContainerID: containerID)
|
||||
ExtensionFeedAddRequestFile.save(request)
|
||||
|
||||
|
||||
self.extensionContext!.completeRequest(returningItems: [], completionHandler: nil)
|
||||
}
|
||||
|
||||
|
||||
func shareFolderPickerDidSelect(_ container: ExtensionContainer) {
|
||||
ShareDefaultContainer.saveDefaultContainer(container)
|
||||
self.selectedContainer = container
|
||||
@ -126,37 +126,37 @@ class ShareViewController: SLComposeServiceViewController, ShareFolderPickerCont
|
||||
}
|
||||
|
||||
override func configurationItems() -> [Any]! {
|
||||
|
||||
|
||||
// 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 }
|
||||
urlItem.title = "URL"
|
||||
urlItem.value = url?.absoluteString ?? ""
|
||||
|
||||
|
||||
folderItem = SLComposeSheetConfigurationItem()
|
||||
folderItem.title = "Folder"
|
||||
updateFolderItemValue()
|
||||
|
||||
|
||||
folderItem.tapHandler = {
|
||||
|
||||
|
||||
let folderPickerController = ShareFolderPickerController()
|
||||
|
||||
|
||||
folderPickerController.navigationController?.title = NSLocalizedString("Folder", comment: "Folder")
|
||||
folderPickerController.delegate = self
|
||||
folderPickerController.containers = self.flattenedContainers
|
||||
folderPickerController.selectedContainerID = self.selectedContainer?.containerID
|
||||
|
||||
|
||||
self.pushConfigurationViewController(folderPickerController)
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
return [folderItem!, urlItem]
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private extension ShareViewController {
|
||||
|
||||
|
||||
func updateFolderItemValue() {
|
||||
if let account = selectedContainer as? ExtensionAccount {
|
||||
self.folderItem.value = account.name
|
||||
@ -164,5 +164,5 @@ private extension ShareViewController {
|
||||
self.folderItem.value = "\(folder.accountName) / \(folder.name)"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -37,5 +37,5 @@ class TitleActivityItemSource: NSObject, UIActivityItemSource {
|
||||
return NSNull()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,16 +10,16 @@ import Foundation
|
||||
|
||||
/// Used to select which animations should be performed
|
||||
public struct Animations: OptionSet {
|
||||
|
||||
|
||||
/// Selections and deselections will be animated.
|
||||
public static let select = Animations(rawValue: 1)
|
||||
|
||||
|
||||
/// Scrolling will be animated
|
||||
public static let scroll = Animations(rawValue: 2)
|
||||
|
||||
|
||||
/// Pushing and popping navigation view controllers will be animated
|
||||
public static let navigation = Animations(rawValue: 4)
|
||||
|
||||
|
||||
public let rawValue: Int
|
||||
public init(rawValue: Int) {
|
||||
self.rawValue = rawValue
|
||||
|
@ -9,14 +9,14 @@
|
||||
import UIKit
|
||||
|
||||
extension Array where Element == CGRect {
|
||||
|
||||
|
||||
func maxY() -> CGFloat {
|
||||
|
||||
|
||||
var y: CGFloat = 0.0
|
||||
for oneRect in self {
|
||||
y = Swift.max(y, oneRect.maxY)
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,17 +9,17 @@
|
||||
import Foundation
|
||||
|
||||
extension Bundle {
|
||||
|
||||
|
||||
var appName: String {
|
||||
return infoDictionary?["CFBundleName"] as! String
|
||||
}
|
||||
|
||||
|
||||
var versionNumber: String {
|
||||
return infoDictionary?["CFBundleShortVersionString"] as! String
|
||||
}
|
||||
|
||||
|
||||
var buildNumber: String {
|
||||
return infoDictionary?["CFBundleVersion"] as! String
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,11 +9,11 @@
|
||||
import UIKit
|
||||
|
||||
class CroppingPreviewParameters: UIPreviewParameters {
|
||||
|
||||
|
||||
override init() {
|
||||
super.init()
|
||||
}
|
||||
|
||||
|
||||
init(view: UIView) {
|
||||
super.init()
|
||||
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 {
|
||||
|
||||
static let rowHeight = CGFloat(integerLiteral: 88)
|
||||
|
||||
|
||||
var imageView = UIImageView()
|
||||
|
||||
|
||||
override init(reuseIdentifier: String?) {
|
||||
super.init(reuseIdentifier: reuseIdentifier)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
func commonInit() {
|
||||
imageView.tintColor = UIColor.label
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
|
@ -59,10 +59,9 @@ class InteractiveLabel: UILabel, UIEditMenuInteractionDelegate {
|
||||
|
||||
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)
|
||||
}
|
||||
return UIMenu(title: "", children: [copyAction])
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
|
||||
class InteractiveNavigationController: UINavigationController {
|
||||
|
||||
|
||||
private let poppableDelegate = PoppableGestureRecognizerDelegate()
|
||||
|
||||
static func template() -> UINavigationController {
|
||||
@ -17,13 +17,13 @@ class InteractiveNavigationController: UINavigationController {
|
||||
navController.configure()
|
||||
return navController
|
||||
}
|
||||
|
||||
|
||||
static func template(rootViewController: UIViewController) -> UINavigationController {
|
||||
let navController = InteractiveNavigationController(rootViewController: rootViewController)
|
||||
navController.configure()
|
||||
return navController
|
||||
}
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
poppableDelegate.navigationController = self
|
||||
@ -40,21 +40,21 @@ class InteractiveNavigationController: UINavigationController {
|
||||
// MARK: Private
|
||||
|
||||
private extension InteractiveNavigationController {
|
||||
|
||||
|
||||
func configure() {
|
||||
isToolbarHidden = false
|
||||
|
||||
|
||||
let navigationStandardAppearance = UINavigationBarAppearance()
|
||||
navigationStandardAppearance.titleTextAttributes = [.foregroundColor: UIColor.label]
|
||||
navigationStandardAppearance.largeTitleTextAttributes = [.foregroundColor: UIColor.label]
|
||||
navigationBar.standardAppearance = navigationStandardAppearance
|
||||
|
||||
|
||||
let scrollEdgeStandardAppearance = UINavigationBarAppearance()
|
||||
scrollEdgeStandardAppearance.backgroundColor = .systemBackground
|
||||
navigationBar.scrollEdgeAppearance = scrollEdgeStandardAppearance
|
||||
|
||||
|
||||
navigationBar.tintColor = AppAssets.primaryAccentColor
|
||||
|
||||
|
||||
let toolbarAppearance = UIToolbarAppearance()
|
||||
toolbar.standardAppearance = toolbarAppearance
|
||||
toolbar.compactAppearance = toolbarAppearance
|
||||
|
@ -12,10 +12,10 @@ class ModalNavigationController: UINavigationController {
|
||||
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
|
||||
|
||||
// This hack is to resolve https://github.com/brentsimmons/NetNewsWire/issues/1301
|
||||
let frame = navigationBar.frame
|
||||
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 {
|
||||
return CGSize(width: UIView.noIntrinsicMetric, height: UIView.noIntrinsicMetric)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -10,7 +10,7 @@
|
||||
import UIKit
|
||||
|
||||
final class PoppableGestureRecognizerDelegate: NSObject, UIGestureRecognizerDelegate {
|
||||
|
||||
|
||||
weak var navigationController: UINavigationController?
|
||||
|
||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
@ -20,12 +20,12 @@ final class PoppableGestureRecognizerDelegate: NSObject, UIGestureRecognizerDele
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldBeRequiredToFailBy otherGestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||
if otherGestureRecognizer is UIPanGestureRecognizer {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,17 +9,17 @@
|
||||
import UIKit
|
||||
|
||||
extension String {
|
||||
|
||||
|
||||
func height(withConstrainedWidth width: CGFloat, font: UIFont) -> CGFloat {
|
||||
let constraintRect = CGSize(width: width, height: .greatestFiniteMagnitude)
|
||||
let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil)
|
||||
return ceil(boundingBox.height)
|
||||
}
|
||||
|
||||
|
||||
func width(withConstrainedHeight height: CGFloat, font: UIFont) -> CGFloat {
|
||||
let constraintRect = CGSize(width: .greatestFiniteMagnitude, height: height)
|
||||
let boundingBox = self.boundingRect(with: constraintRect, options: [.usesLineFragmentOrigin, .usesFontLeading], attributes: [NSAttributedString.Key.font: font], context: nil)
|
||||
return ceil(boundingBox.width)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ class TickMarkSlider: UISlider {
|
||||
|
||||
private var enableFeedback = false
|
||||
private let feedbackGenerator = UISelectionFeedbackGenerator()
|
||||
|
||||
|
||||
private var roundedValue: Float?
|
||||
override var value: Float {
|
||||
didSet {
|
||||
@ -23,17 +23,17 @@ class TickMarkSlider: UISlider {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func addTickMarks() {
|
||||
|
||||
enableFeedback = true
|
||||
|
||||
|
||||
let numberOfGaps = Int(maximumValue) - Int(minimumValue)
|
||||
|
||||
|
||||
var gapLayoutGuides = [UILayoutGuide]()
|
||||
|
||||
|
||||
for i in 0...numberOfGaps {
|
||||
|
||||
|
||||
let tick = UIView()
|
||||
tick.translatesAutoresizingMaskIntoConstraints = false
|
||||
tick.backgroundColor = AppAssets.tickMarkColor
|
||||
@ -46,11 +46,11 @@ class TickMarkSlider: UISlider {
|
||||
if i == 0 {
|
||||
tick.leadingAnchor.constraint(equalTo: leadingAnchor).isActive = true
|
||||
}
|
||||
|
||||
|
||||
if let lastGapLayoutGuild = gapLayoutGuides.last {
|
||||
lastGapLayoutGuild.trailingAnchor.constraint(equalTo: tick.leadingAnchor).isActive = true
|
||||
}
|
||||
|
||||
|
||||
if i != numberOfGaps {
|
||||
let gapLayoutGuild = UILayoutGuide()
|
||||
gapLayoutGuides.append(gapLayoutGuild)
|
||||
@ -59,17 +59,17 @@ class TickMarkSlider: UISlider {
|
||||
} else {
|
||||
tick.trailingAnchor.constraint(equalTo: trailingAnchor).isActive = true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
if let firstGapLayoutGuild = gapLayoutGuides.first {
|
||||
for i in 1..<gapLayoutGuides.count {
|
||||
gapLayoutGuides[i].widthAnchor.constraint(equalTo: firstGapLayoutGuild.widthAnchor).isActive = true
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
override func continueTracking(_ touch: UITouch, with event: UIEvent?) -> Bool {
|
||||
let result = super.continueTracking(touch, with: event)
|
||||
value = value.rounded()
|
||||
@ -79,5 +79,5 @@ class TickMarkSlider: UISlider {
|
||||
override func endTracking(_ touch: UITouch?, with event: UIEvent?) {
|
||||
value = value.rounded()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ extension UIActivityViewController {
|
||||
convenience init(url: URL, title: String?, applicationActivities: [UIActivity]?) {
|
||||
let itemSource = ArticleActivityItemSource(url: url, subject: title)
|
||||
let titleSource = TitleActivityItemSource(title: title)
|
||||
|
||||
|
||||
self.init(activityItems: [titleSource, itemSource], applicationActivities: applicationActivities)
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
|
||||
public extension UIBarButtonItem {
|
||||
|
||||
|
||||
@IBInspectable var accEnabled: Bool {
|
||||
get {
|
||||
return isAccessibilityElement
|
||||
@ -18,7 +18,7 @@ public extension UIBarButtonItem {
|
||||
isAccessibilityElement = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@IBInspectable var accLabelText: String? {
|
||||
get {
|
||||
return accessibilityLabel
|
||||
@ -27,5 +27,5 @@ public extension UIBarButtonItem {
|
||||
accessibilityLabel = newValue
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,21 +9,21 @@
|
||||
import UIKit
|
||||
|
||||
extension UIFont {
|
||||
|
||||
func withTraits(traits:UIFontDescriptor.SymbolicTraits) -> UIFont {
|
||||
|
||||
func withTraits(traits: UIFontDescriptor.SymbolicTraits) -> UIFont {
|
||||
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 {
|
||||
return self
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func bold() -> UIFont {
|
||||
return withTraits(traits: .traitBold)
|
||||
}
|
||||
|
||||
|
||||
func italic() -> UIFont {
|
||||
return withTraits(traits: .traitItalic)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
|
||||
extension UIPageViewController {
|
||||
|
||||
|
||||
var scrollViewInsidePageControl: UIScrollView? {
|
||||
for view in view.subviews {
|
||||
if let scrollView = view as? UIScrollView {
|
||||
@ -18,5 +18,5 @@ extension UIPageViewController {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,39 +9,39 @@
|
||||
import UIKit
|
||||
|
||||
extension UIStoryboard {
|
||||
|
||||
|
||||
static let preferredContentSizeForFormSheetDisplay = CGSize(width: 460.0, height: 400.0)
|
||||
|
||||
|
||||
static var main: UIStoryboard {
|
||||
return UIStoryboard(name: "Main", bundle: nil)
|
||||
}
|
||||
|
||||
|
||||
static var add: UIStoryboard {
|
||||
return UIStoryboard(name: "Add", bundle: nil)
|
||||
}
|
||||
|
||||
|
||||
static var settings: UIStoryboard {
|
||||
return UIStoryboard(name: "Settings", bundle: nil)
|
||||
}
|
||||
|
||||
|
||||
static var inspector: UIStoryboard {
|
||||
return UIStoryboard(name: "Inspector", bundle: nil)
|
||||
}
|
||||
|
||||
|
||||
static var account: UIStoryboard {
|
||||
return UIStoryboard(name: "Account", bundle: nil)
|
||||
}
|
||||
|
||||
|
||||
func instantiateController<T>(ofType type: T.Type = T.self) -> T where T: UIViewController {
|
||||
|
||||
|
||||
let storyboardId = String(describing: type)
|
||||
guard let viewController = instantiateViewController(withIdentifier: storyboardId) as? T else {
|
||||
print("Unable to load view with Scene Identifier: \(storyboardId)")
|
||||
fatalError()
|
||||
}
|
||||
|
||||
|
||||
return viewController
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
|
||||
extension UITableView {
|
||||
|
||||
|
||||
/**
|
||||
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 {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
selectRow(at: indexPath, animated: animations.contains(.select), scrollPosition: .none)
|
||||
|
||||
if let visibleIndexPaths = indexPathsForRows(in: safeAreaLayoutGuide.layoutFrame) {
|
||||
@ -29,12 +29,12 @@ extension UITableView {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func cellCompletelyVisible(_ indexPath: IndexPath) -> Bool {
|
||||
let rect = rectForRow(at: indexPath)
|
||||
return safeAreaLayoutGuide.layoutFrame.contains(rect)
|
||||
}
|
||||
|
||||
|
||||
public func middleVisibleRow() -> IndexPath? {
|
||||
if let visibleIndexPaths = indexPathsForRows(in: safeAreaLayoutGuide.layoutFrame), visibleIndexPaths.count > 2 {
|
||||
return visibleIndexPaths[visibleIndexPaths.count / 2]
|
||||
|
@ -11,7 +11,7 @@ import RSCore
|
||||
import Account
|
||||
|
||||
extension UIViewController {
|
||||
|
||||
|
||||
func presentError(_ error: Error, dismiss: (() -> Void)? = nil) {
|
||||
if let accountError = error as? AccountError, accountError.isCredentialsError {
|
||||
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")
|
||||
informativeText = NSString.localizedStringWithFormat(localizedError as NSString, debugDescription) as String
|
||||
presentError(title: errorTitle, message: informativeText, dismiss: dismiss)
|
||||
|
||||
|
||||
default:
|
||||
informativeText = error.localizedDescription
|
||||
presentError(title: errorTitle, message: informativeText, dismiss: dismiss)
|
||||
@ -55,35 +55,35 @@ extension UIViewController {
|
||||
}
|
||||
|
||||
private extension UIViewController {
|
||||
|
||||
|
||||
func presentAccountError(_ error: AccountError, dismiss: (() -> Void)? = nil) {
|
||||
let title = NSLocalizedString("Account Error", comment: "Account Error")
|
||||
let alertController = UIAlertController(title: title, message: error.localizedDescription, preferredStyle: .alert)
|
||||
|
||||
|
||||
if error.account?.type == .feedbin {
|
||||
|
||||
let credentialsTitle = NSLocalizedString("Update Credentials", comment: "Update Credentials")
|
||||
let credentialsAction = UIAlertAction(title: credentialsTitle, style: .default) { [weak self] _ in
|
||||
dismiss?()
|
||||
|
||||
|
||||
let navController = UIStoryboard.account.instantiateViewController(withIdentifier: "FeedbinAccountNavigationViewController") as! UINavigationController
|
||||
navController.modalPresentationStyle = .formSheet
|
||||
let addViewController = navController.topViewController as! FeedbinAccountViewController
|
||||
addViewController.account = error.account
|
||||
self?.present(navController, animated: true)
|
||||
}
|
||||
|
||||
|
||||
alertController.addAction(credentialsAction)
|
||||
alertController.preferredAction = credentialsAction
|
||||
|
||||
}
|
||||
|
||||
|
||||
let dismissTitle = NSLocalizedString("OK", comment: "OK")
|
||||
let dismissAction = UIAlertAction(title: dismissTitle, style: .default) { _ in
|
||||
dismiss?()
|
||||
}
|
||||
alertController.addAction(dismissAction)
|
||||
|
||||
|
||||
self.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
|
||||
class VibrantButton: UIButton {
|
||||
|
||||
|
||||
@IBInspectable var backgroundHighlightColor: UIColor = AppAssets.secondaryAccentColor
|
||||
|
||||
override init(frame: CGRect) {
|
||||
@ -20,7 +20,7 @@ class VibrantButton: UIButton {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
private func commonInit() {
|
||||
setTitleColor(AppAssets.vibrantTextColor, for: .highlighted)
|
||||
let disabledColor = AppAssets.secondaryAccentColor.withAlphaComponent(0.5)
|
||||
@ -47,5 +47,5 @@ class VibrantButton: UIButton {
|
||||
isHighlighted = false
|
||||
super.touchesCancelled(touches, with: event)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -9,17 +9,17 @@
|
||||
import UIKit
|
||||
|
||||
class VibrantLabel: UILabel {
|
||||
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
private func commonInit() {
|
||||
highlightedTextColor = AppAssets.vibrantTextColor
|
||||
}
|
||||
|
@ -9,27 +9,27 @@
|
||||
import UIKit
|
||||
|
||||
class VibrantTableViewCell: UITableViewCell {
|
||||
|
||||
|
||||
static let duration: TimeInterval = 0.6
|
||||
|
||||
|
||||
var labelColor: UIColor {
|
||||
return isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.label
|
||||
}
|
||||
|
||||
|
||||
var secondaryLabelColor: UIColor {
|
||||
return isHighlighted || isSelected ? AppAssets.vibrantTextColor : UIColor.secondaryLabel
|
||||
}
|
||||
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
commonInit()
|
||||
}
|
||||
|
||||
|
||||
private func commonInit() {
|
||||
applyThemeProperties()
|
||||
}
|
||||
@ -43,7 +43,7 @@ class VibrantTableViewCell: UITableViewCell {
|
||||
super.setSelected(selected, animated: animated)
|
||||
updateVibrancy(animated: animated)
|
||||
}
|
||||
|
||||
|
||||
/// Subclass overrides should call super
|
||||
func applyThemeProperties() {
|
||||
let selectedBackgroundView = UIView(frame: .zero)
|
||||
@ -56,7 +56,7 @@ class VibrantTableViewCell: UITableViewCell {
|
||||
updateLabelVibrancy(textLabel, color: labelColor, animated: animated)
|
||||
updateLabelVibrancy(detailTextLabel, color: labelColor, animated: animated)
|
||||
}
|
||||
|
||||
|
||||
func updateLabelVibrancy(_ label: UILabel?, color: UIColor, animated: Bool) {
|
||||
guard let label = label else { return }
|
||||
if animated {
|
||||
@ -67,33 +67,33 @@ class VibrantTableViewCell: UITableViewCell {
|
||||
label.textColor = color
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
class VibrantBasicTableViewCell: VibrantTableViewCell {
|
||||
|
||||
|
||||
@IBOutlet private var label: UILabel!
|
||||
@IBOutlet private var detail: UILabel!
|
||||
@IBOutlet private var icon: UIImageView!
|
||||
|
||||
|
||||
@IBInspectable var imageNormal: UIImage?
|
||||
@IBInspectable var imageSelected: UIImage?
|
||||
|
||||
|
||||
var iconTint: UIColor {
|
||||
return isHighlighted || isSelected ? labelColor : AppAssets.primaryAccentColor
|
||||
}
|
||||
|
||||
|
||||
var iconImage: UIImage? {
|
||||
return isHighlighted || isSelected ? imageSelected : imageNormal
|
||||
}
|
||||
|
||||
|
||||
override func updateVibrancy(animated: Bool) {
|
||||
super.updateVibrancy(animated: animated)
|
||||
updateIconVibrancy(icon, color: iconTint, image: iconImage, animated: animated)
|
||||
updateLabelVibrancy(label, color: labelColor, animated: animated)
|
||||
updateLabelVibrancy(detail, color: secondaryLabelColor, animated: animated)
|
||||
}
|
||||
|
||||
|
||||
private func updateIconVibrancy(_ icon: UIImageView?, color: UIColor, image: UIImage?, animated: Bool) {
|
||||
guard let icon = icon else { return }
|
||||
if animated {
|
||||
@ -106,5 +106,5 @@ class VibrantBasicTableViewCell: VibrantTableViewCell {
|
||||
icon.image = image
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user