Fix lint issues.

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

View File

@ -12,7 +12,7 @@ import Account
enum CloudKitAccountViewControllerError: LocalizedError {
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

View File

@ -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\nDont 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
}
}

View File

@ -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
}
}

View File

@ -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\nDont have a NewsBlur account?", comment: "NewsBlur")
}
@ -93,7 +93,7 @@ class NewsBlurAccountViewController: UITableViewController {
showError(NSLocalizedString("There is already a NewsBlur account with that username created.", comment: "Duplicate Error"))
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)

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}
}

View File

@ -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

View File

@ -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")
}
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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 {
}
})
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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()")
}
}

View File

@ -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)
}
}

View File

@ -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 ?? ""
}
}

View File

@ -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)
}

View File

@ -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)

View File

@ -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

View File

@ -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
}
}

View File

@ -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)
}

View File

@ -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)")
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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]
}
}

View File

@ -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)
}
}
}
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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()
}
}

View File

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

View File

@ -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
}
}

View File

@ -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. Its easiest.
@ -60,5 +60,5 @@ final class SingleLineUILabelSizer {
static func emptyCache() {
sizers = [UIFont: SingleLineUILabelSizer]()
}
}

View File

@ -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
}
}
}

View File

@ -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

View File

@ -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)
}
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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

View File

@ -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 {
}
}
}
}

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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()
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

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

View File

@ -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)
}
}
}

View File

@ -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)"
}
}
}

View File

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

View File

@ -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

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -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

View File

@ -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])
}
}

View File

@ -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

View File

@ -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)
}
}

View File

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

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -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
}
}
}

View File

@ -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)
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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]

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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
}

View File

@ -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
}
}
}