500 lines
17 KiB
Swift
500 lines
17 KiB
Swift
//
|
|
// SettingsViewController.swift
|
|
// NetNewsWire-iOS
|
|
//
|
|
// Created by Maurice Parker on 4/24/19.
|
|
// Copyright © 2019 Ranchero Software. All rights reserved.
|
|
//
|
|
|
|
import UIKit
|
|
import UIKitExtras
|
|
import Account
|
|
import CoreServices
|
|
import SafariServices
|
|
import SwiftUI
|
|
import UniformTypeIdentifiers
|
|
|
|
final class SettingsViewController: UITableViewController {
|
|
|
|
private weak var opmlAccount: Account?
|
|
|
|
@IBOutlet weak var timelineSortOrderSwitch: UISwitch!
|
|
@IBOutlet weak var groupByFeedSwitch: UISwitch!
|
|
@IBOutlet weak var refreshClearsReadArticlesSwitch: UISwitch!
|
|
@IBOutlet weak var articleThemeDetailLabel: UILabel!
|
|
@IBOutlet weak var confirmMarkAllAsReadSwitch: UISwitch!
|
|
@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)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(contentSizeCategoryDidChange), name: UIContentSizeCategory.didChangeNotification, object: nil)
|
|
|
|
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange), name: .UserDidAddAccount, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(accountsDidChange), name: .UserDidDeleteAccount, object: nil)
|
|
NotificationCenter.default.addObserver(self, selector: #selector(displayNameDidChange), name: .DisplayNameDidChange, object: nil)
|
|
|
|
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 {
|
|
timelineSortOrderSwitch.isOn = false
|
|
}
|
|
|
|
if AppDefaults.shared.timelineGroupByFeed {
|
|
groupByFeedSwitch.isOn = true
|
|
} else {
|
|
groupByFeedSwitch.isOn = false
|
|
}
|
|
|
|
if AppDefaults.shared.refreshClearsReadArticles {
|
|
refreshClearsReadArticlesSwitch.isOn = true
|
|
} else {
|
|
refreshClearsReadArticlesSwitch.isOn = false
|
|
}
|
|
|
|
|
|
articleThemeDetailLabel.text = ArticleThemesManager.shared.currentTheme.name
|
|
|
|
if AppDefaults.shared.confirmMarkAllAsRead {
|
|
confirmMarkAllAsReadSwitch.isOn = true
|
|
} else {
|
|
confirmMarkAllAsReadSwitch.isOn = false
|
|
}
|
|
|
|
if AppDefaults.shared.articleFullscreenAvailable {
|
|
showFullscreenArticlesSwitch.isOn = true
|
|
} 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)
|
|
buildLabel.textColor = UIColor.gray
|
|
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 {
|
|
case 1:
|
|
return AccountManager.shared.accounts.count + 1
|
|
case 2:
|
|
let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: section)
|
|
if AccountManager.shared.activeAccounts.isEmpty || AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() {
|
|
return defaultNumberOfRows - 1
|
|
}
|
|
return defaultNumberOfRows
|
|
case 4:
|
|
return traitCollection.userInterfaceIdiom == .phone ? 4 : 3
|
|
default:
|
|
return super.tableView(tableView, numberOfRowsInSection: section)
|
|
}
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
|
|
|
|
let cell: UITableViewCell
|
|
switch indexPath.section {
|
|
case 1:
|
|
|
|
let sortedAccounts = AccountManager.shared.sortedAccounts
|
|
if indexPath.row == sortedAccounts.count {
|
|
cell = tableView.dequeueReusableCell(withIdentifier: "SettingsTableViewCell", for: indexPath)
|
|
cell.textLabel?.text = NSLocalizedString("Add Account", comment: "Accounts")
|
|
} else {
|
|
let acctCell = tableView.dequeueReusableCell(withIdentifier: "SettingsComboTableViewCell", for: indexPath) as! SettingsComboTableViewCell
|
|
acctCell.applyThemeProperties()
|
|
let account = sortedAccounts[indexPath.row]
|
|
acctCell.comboImage?.image = AppAsset.Account.image(for: account.accountType)
|
|
acctCell.comboNameLabel?.text = account.nameForDisplay
|
|
cell = acctCell
|
|
}
|
|
default:
|
|
cell = super.tableView(tableView, cellForRowAt: indexPath)
|
|
|
|
}
|
|
|
|
return cell
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
|
|
|
|
switch indexPath.section {
|
|
case 0:
|
|
UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
case 1:
|
|
let sortedAccounts = AccountManager.shared.sortedAccounts
|
|
if indexPath.row == sortedAccounts.count {
|
|
let controller = UIStoryboard.settings.instantiateController(ofType: AddAccountViewController.self)
|
|
self.navigationController?.pushViewController(controller, animated: true)
|
|
} else {
|
|
let controller = UIStoryboard.inspector.instantiateController(ofType: AccountInspectorViewController.self)
|
|
controller.account = sortedAccounts[indexPath.row]
|
|
self.navigationController?.pushViewController(controller, animated: true)
|
|
}
|
|
case 2:
|
|
switch indexPath.row {
|
|
case 0:
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
if let sourceView = tableView.cellForRow(at: indexPath) {
|
|
let sourceRect = tableView.rectForRow(at: indexPath)
|
|
importOPML(sourceView: sourceView, sourceRect: sourceRect)
|
|
}
|
|
case 1:
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
if let sourceView = tableView.cellForRow(at: indexPath) {
|
|
let sourceRect = tableView.rectForRow(at: indexPath)
|
|
exportOPML(sourceView: sourceView, sourceRect: sourceRect)
|
|
}
|
|
case 2:
|
|
addFeed()
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
default:
|
|
break
|
|
}
|
|
case 3:
|
|
switch indexPath.row {
|
|
case 3:
|
|
let timeline = UIStoryboard.settings.instantiateController(ofType: TimelineCustomizerViewController.self)
|
|
self.navigationController?.pushViewController(timeline, animated: true)
|
|
default:
|
|
break
|
|
}
|
|
case 4:
|
|
switch indexPath.row {
|
|
case 0:
|
|
let articleThemes = UIStoryboard.settings.instantiateController(ofType: ArticleThemesTableViewController.self)
|
|
self.navigationController?.pushViewController(articleThemes, animated: true)
|
|
default:
|
|
break
|
|
}
|
|
case 5:
|
|
let colorPalette = UIStoryboard.settings.instantiateController(ofType: ColorPaletteTableViewController.self)
|
|
self.navigationController?.pushViewController(colorPalette, animated: true)
|
|
case 6:
|
|
switch indexPath.row {
|
|
case 0:
|
|
openURL("https://netnewswire.com/help/ios/6.1/en/")
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
case 1:
|
|
openURL("https://netnewswire.com/")
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
case 2:
|
|
openURL(URL.releaseNotes.absoluteString)
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
case 3:
|
|
openURL("https://github.com/brentsimmons/NetNewsWire/blob/main/Technotes/HowToSupportNetNewsWire.markdown")
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
case 4:
|
|
openURL("https://github.com/brentsimmons/NetNewsWire")
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
case 5:
|
|
openURL("https://github.com/brentsimmons/NetNewsWire/issues")
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
case 6:
|
|
openURL("https://github.com/brentsimmons/NetNewsWire/tree/main/Technotes")
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
case 7:
|
|
openURL("https://netnewswire.com/slack")
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
case 8:
|
|
let timeline = UIStoryboard.settings.instantiateController(ofType: AboutViewController.self)
|
|
self.navigationController?.pushViewController(timeline, animated: true)
|
|
default:
|
|
break
|
|
}
|
|
default:
|
|
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
|
|
}
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, canEditRowAt indexPath: IndexPath) -> Bool {
|
|
return false
|
|
}
|
|
|
|
override func tableView(_ tableView: UITableView, canMoveRowAt indexPath: IndexPath) -> Bool {
|
|
return false
|
|
}
|
|
|
|
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
|
|
} else {
|
|
AppDefaults.shared.timelineSortDirection = .orderedDescending
|
|
}
|
|
}
|
|
|
|
@IBAction func switchGroupByFeed(_ sender: Any) {
|
|
if groupByFeedSwitch.isOn {
|
|
AppDefaults.shared.timelineGroupByFeed = true
|
|
} else {
|
|
AppDefaults.shared.timelineGroupByFeed = false
|
|
}
|
|
}
|
|
|
|
@IBAction func switchClearsReadArticles(_ sender: Any) {
|
|
if refreshClearsReadArticlesSwitch.isOn {
|
|
AppDefaults.shared.refreshClearsReadArticles = true
|
|
} else {
|
|
AppDefaults.shared.refreshClearsReadArticles = false
|
|
}
|
|
}
|
|
|
|
@IBAction func switchConfirmMarkAllAsRead(_ sender: Any) {
|
|
if confirmMarkAllAsReadSwitch.isOn {
|
|
AppDefaults.shared.confirmMarkAllAsRead = true
|
|
} else {
|
|
AppDefaults.shared.confirmMarkAllAsRead = false
|
|
}
|
|
}
|
|
|
|
@IBAction func switchFullscreenArticles(_ sender: Any) {
|
|
if showFullscreenArticlesSwitch.isOn {
|
|
AppDefaults.shared.articleFullscreenAvailable = true
|
|
} else {
|
|
AppDefaults.shared.articleFullscreenAvailable = false
|
|
}
|
|
}
|
|
|
|
@IBAction func switchBrowserPreference(_ sender: Any) {
|
|
if openLinksInNetNewsWire.isOn {
|
|
AppDefaults.shared.useSystemBrowser = false
|
|
} else {
|
|
AppDefaults.shared.useSystemBrowser = true
|
|
}
|
|
}
|
|
|
|
|
|
// MARK: Notifications
|
|
|
|
@objc func contentSizeCategoryDidChange() {
|
|
tableView.reloadData()
|
|
}
|
|
|
|
@objc func accountsDidChange() {
|
|
tableView.reloadData()
|
|
}
|
|
|
|
@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 {
|
|
|
|
Task { @MainActor in
|
|
do {
|
|
try await opmlAccount?.importOPML(url)
|
|
} catch {
|
|
let title = NSLocalizedString("Import Failed", comment: "Import Failed")
|
|
let message = NSLocalizedString("We were unable to process the selected file. Please ensure that it is a properly formatted OPML file.", comment: "Import Failed Message")
|
|
self.presentError(title: title, message: message)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: Private
|
|
|
|
private extension SettingsViewController {
|
|
|
|
func addFeed() {
|
|
self.dismiss(animated: true)
|
|
|
|
let addNavViewController = UIStoryboard.add.instantiateViewController(withIdentifier: "AddFeedViewControllerNav") as! UINavigationController
|
|
let addViewController = addNavViewController.topViewController as! AddFeedViewController
|
|
addViewController.initialFeed = AccountManager.netNewsWireNewsURL
|
|
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:
|
|
presentError(title: "Error", message: NSLocalizedString("You must have at least one active account.", comment: "Missing active account"))
|
|
case 1:
|
|
opmlAccount = AccountManager.shared.activeAccounts.first
|
|
importOPMLDocumentPicker()
|
|
default:
|
|
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
|
|
self?.opmlAccount = account
|
|
self?.importOPMLDocumentPicker()
|
|
}
|
|
alert.addAction(action)
|
|
}
|
|
|
|
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
|
alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
|
|
|
|
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!
|
|
exportOPMLDocumentPicker()
|
|
} else {
|
|
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
|
|
self?.opmlAccount = account
|
|
self?.exportOPMLDocumentPicker()
|
|
}
|
|
alert.addAction(action)
|
|
}
|
|
|
|
let cancelTitle = NSLocalizedString("Cancel", comment: "Cancel")
|
|
alert.addAction(UIAlertAction(title: cancelTitle, style: .cancel))
|
|
|
|
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)
|
|
let opmlString = OPMLExporter.OPMLString(with: account, title: filename)
|
|
do {
|
|
try opmlString.write(to: tempFile, atomically: true, encoding: String.Encoding.utf8)
|
|
} catch {
|
|
self.presentError(title: "OPML Export Error", message: error.localizedDescription)
|
|
}
|
|
|
|
let documentPicker = UIDocumentPickerViewController(forExporting: [tempFile], asCopy: false)
|
|
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)
|
|
}
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|