- Removes unused classes
- Posts a notification when the app is opened via external context which allows the SwiftUI SettingsView to dismiss itself.
This commit is contained in:
Stuart Breckenridge
2022-12-14 07:15:22 +08:00
parent 30e5b5485d
commit 369c346139
8 changed files with 18 additions and 979 deletions

View File

@@ -1,42 +0,0 @@
//
// ColorPaletteTableViewController.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 3/15/20.
// Copyright © 2020 Ranchero Software. All rights reserved.
//
import UIKit
class ColorPaletteTableViewController: UITableViewController {
// MARK: - Table view data source
override func numberOfSections(in tableView: UITableView) -> Int {
return 1
}
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return UserInterfaceColorPalette.allCases.count
}
override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
let rowColorPalette = UserInterfaceColorPalette.allCases[indexPath.row]
cell.textLabel?.text = String(describing: rowColorPalette)
if rowColorPalette == AppDefaults.userInterfaceColorPalette {
cell.accessoryType = .checkmark
} else {
cell.accessoryType = .none
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
if let colorPalette = UserInterfaceColorPalette(rawValue: indexPath.row) {
AppDefaults.userInterfaceColorPalette = colorPalette
}
navigationController?.popViewController(animated: true)
}
}

View File

@@ -1,61 +0,0 @@
//
// NotificationsTableViewCell.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 26/01/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import UIKit
import Account
import UserNotifications
class NotificationsTableViewCell: VibrantBasicTableViewCell {
@IBOutlet weak var notificationsSwitch: UISwitch!
@IBOutlet weak var notificationsLabel: UILabel!
@IBOutlet weak var notificationsImageView: UIImageView!
weak var feed: WebFeed?
override func awakeFromNib() {
super.awakeFromNib()
// Initialization code
}
override func setSelected(_ selected: Bool, animated: Bool) {
super.setSelected(selected, animated: animated)
// Configure the view for the selected state
}
func configure(_ webFeed: WebFeed) {
self.feed = webFeed
var isOn = false
if webFeed.isNotifyAboutNewArticles == nil {
isOn = false
} else {
isOn = webFeed.isNotifyAboutNewArticles!
}
notificationsSwitch.isOn = isOn
notificationsSwitch.addTarget(self, action: #selector(toggleWebFeedNotification(_:)), for: .touchUpInside)
notificationsLabel.text = webFeed.nameForDisplay
notificationsImageView.image = IconImageCache.shared.imageFor(webFeed.feedID!)?.image
notificationsImageView.layer.cornerRadius = 4
}
@objc
private func toggleWebFeedNotification(_ sender: Any) {
guard let feed = feed else {
return
}
if feed.isNotifyAboutNewArticles == nil {
feed.isNotifyAboutNewArticles = true
}
else {
feed.isNotifyAboutNewArticles!.toggle()
}
}
}

View File

@@ -1,335 +0,0 @@
//
// NotificationsViewController.swift
// NetNewsWire-iOS
//
// Created by Stuart Breckenridge on 26/01/2022.
// Copyright © 2022 Ranchero Software. All rights reserved.
//
import UIKit
import SwiftUI
import Account
import UserNotifications
struct NotificationsViewControllerRepresentable: UIViewControllerRepresentable {
func makeUIViewController(context: Context) -> NotificationsViewController {
let storyboard = UIStoryboard(name: "Settings", bundle: .main)
let controller = storyboard.instantiateViewController(withIdentifier: "NotificationsViewController") as! NotificationsViewController
context.coordinator.parentObserver = controller.observe(\.parent, changeHandler: { vc, _ in
vc.parent?.title = vc.title
vc.parent?.navigationItem.rightBarButtonItems = vc.navigationItem.rightBarButtonItems
})
return controller
}
func updateUIViewController(_ uiViewController: NotificationsViewController, context: Context) {
//
}
typealias UIViewControllerType = NotificationsViewController
class Coordinator {
var parentObserver: NSKeyValueObservation?
}
func makeCoordinator() -> Self.Coordinator { Coordinator() }
}
class NotificationsViewController: UIViewController {
@IBOutlet weak var notificationsTableView: UITableView!
private lazy var searchController: UISearchController = {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchBar.placeholder = NSLocalizedString("Find a feed", comment: "Find a feed")
searchController.searchBar.searchBarStyle = .minimal
searchController.delegate = self
searchController.searchBar.delegate = self
searchController.searchBar.sizeToFit()
searchController.obscuresBackgroundDuringPresentation = false
searchController.hidesNavigationBarDuringPresentation = false
self.definesPresentationContext = true
return searchController
}()
private var status: UNAuthorizationStatus = .notDetermined
private var newArticleNotificationFilter: Bool = false {
didSet {
filterButton.menu = notificationFilterMenu()
}
}
private var filterButton: UIBarButtonItem!
override func viewDidLoad() {
super.viewDidLoad()
title = NSLocalizedString("New Article Notifications", comment: "Notifications")
navigationItem.searchController = searchController
notificationsTableView.isPrefetchingEnabled = false
filterButton = UIBarButtonItem(
title: nil,
image: AppAssets.moreImage,
primaryAction: nil,
menu: notificationFilterMenu())
navigationItem.rightBarButtonItem = filterButton
reloadNotificationTableView()
NotificationCenter.default.addObserver(self, selector: #selector(updateCellsFrom(_:)), name: .WebFeedIconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reloadVisibleCells(_:)), name: .FaviconDidBecomeAvailable, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(reloadNotificationTableView(_:)), name: UIScene.willEnterForegroundNotification, object: nil)
}
@objc
private func reloadNotificationTableView(_ sender: Any? = nil) {
UNUserNotificationCenter.current().getNotificationSettings { settings in
DispatchQueue.main.async {
self.status = settings.authorizationStatus
if self.status != .authorized {
self.filterButton.isEnabled = false
self.newArticleNotificationFilter = false
}
self.notificationsTableView.reloadData()
}
}
}
@objc
private func updateCellsFrom(_ notification: Notification) {
guard let webFeed = notification.userInfo?[UserInfoKey.webFeed] as? WebFeed else { return }
if let visibleIndexPaths = notificationsTableView.indexPathsForVisibleRows {
for path in visibleIndexPaths {
if let cell = notificationsTableView.cellForRow(at: path) as? NotificationsTableViewCell {
if cell.feed! == webFeed {
cell.configure(webFeed)
return
}
}
}
}
}
@objc
private func reloadVisibleCells(_ notification: Notification) {
guard let faviconURLString = notification.userInfo?["faviconURL"] as? String,
let faviconHost = URL(string: faviconURLString)?.host else {
return
}
for cell in notificationsTableView.visibleCells {
if let notificationCell = cell as? NotificationsTableViewCell {
if let feedURLHost = URL(string: notificationCell.feed!.url)?.host {
if faviconHost == feedURLHost {
notificationCell.configure(notificationCell.feed!)
return
}
}
}
}
}
private func notificationFilterMenu() -> UIMenu {
if filterButton != nil {
if newArticleNotificationFilter {
filterButton.image = AppAssets.moreImageFill
} else {
filterButton.image = AppAssets.moreImage
}
}
let filterMenu = UIMenu(title: "",
image: nil,
identifier: nil,
options: [.displayInline],
children: [
UIAction(
title: NSLocalizedString("Show Feeds with Notifications Enabled", comment: "Feeds with Notifications"),
image: nil,
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: newArticleNotificationFilter ? .on : .off,
handler: { [weak self] _ in
self?.newArticleNotificationFilter.toggle()
self?.notificationsTableView.reloadData()
})])
var menus = [UIMenuElement]()
menus.append(filterMenu)
for account in AccountManager.shared.sortedActiveAccounts {
let accountMenu = UIMenu(title: account.nameForDisplay, image: nil, identifier: nil, options: .singleSelection, children: [enableAllAction(for: account), disableAllAction(for: account)])
menus.append(accountMenu)
}
let combinedMenu = UIMenu(title: "",
image: nil,
identifier: nil,
options: .displayInline,
children: menus)
return combinedMenu
}
private func enableAllAction(for account: Account) -> UIAction {
let action = UIAction(title: NSLocalizedString("Enable All Notifications", comment: "Enable All"),
image: nil,
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: .off) { [weak self] _ in
for feed in account.flattenedWebFeeds() {
feed.isNotifyAboutNewArticles = true
}
self?.notificationsTableView.reloadData()
self?.filterButton.menu = self?.notificationFilterMenu()
}
return action
}
private func disableAllAction(for account: Account) -> UIAction {
let action = UIAction(title: NSLocalizedString("Disable All Notifications", comment: "Disable All"),
image: nil,
identifier: nil,
discoverabilityTitle: nil,
attributes: [],
state: .off) { [weak self] _ in
for feed in account.flattenedWebFeeds() {
feed.isNotifyAboutNewArticles = false
}
self?.notificationsTableView.reloadData()
self?.filterButton.menu = self?.notificationFilterMenu()
}
return action
}
// MARK: - Feed Filtering
private func sortedWebFeedsForAccount(_ account: Account) -> [WebFeed] {
return Array(account.flattenedWebFeeds()).sorted(by: { $0.nameForDisplay.caseInsensitiveCompare($1.nameForDisplay) == .orderedAscending })
}
private func filteredWebFeeds(_ searchText: String? = "", account: Account) -> [WebFeed] {
sortedWebFeedsForAccount(account).filter { feed in
return feed.nameForDisplay.lowercased().contains(searchText!.lowercased())
}
}
private func feedsWithNotificationsEnabled(_ account: Account) -> [WebFeed] {
sortedWebFeedsForAccount(account).filter { feed in
return feed.isNotifyAboutNewArticles == true
}
}
}
// MARK: - UITableViewDataSource
extension NotificationsViewController: UITableViewDataSource {
func numberOfSections(in tableView: UITableView) -> Int {
if status == .denied { return 1 }
return 1 + AccountManager.shared.activeAccounts.count
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
if section == 0 {
if status == .denied { return 1 }
return 0
}
if searchController.isActive {
return filteredWebFeeds(searchController.searchBar.text, account: AccountManager.shared.sortedActiveAccounts[section - 1]).count
} else if newArticleNotificationFilter == true {
return feedsWithNotificationsEnabled(AccountManager.shared.sortedActiveAccounts[section - 1]).count
} else {
return AccountManager.shared.sortedActiveAccounts[section - 1].flattenedWebFeeds().count
}
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
if indexPath.section == 0 {
let openSettingsCell = tableView.dequeueReusableCell(withIdentifier: "OpenSettingsCell") as! VibrantBasicTableViewCell
return openSettingsCell
} else {
if searchController.isActive {
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
cell.configure(filteredWebFeeds(searchController.searchBar.text, account: account)[indexPath.row])
return cell
} else if newArticleNotificationFilter == true {
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
cell.configure(feedsWithNotificationsEnabled(account)[indexPath.row])
return cell
} else {
let cell = tableView.dequeueReusableCell(withIdentifier: "NotificationsCell") as! NotificationsTableViewCell
let account = AccountManager.shared.sortedActiveAccounts[indexPath.section - 1]
cell.configure(sortedWebFeedsForAccount(account)[indexPath.row])
return cell
}
}
}
func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? {
if section == 0 { return nil }
return AccountManager.shared.sortedActiveAccounts[section - 1].nameForDisplay
}
func tableView(_ tableView: UITableView, titleForFooterInSection section: Int) -> String? {
if section == 0 {
if status == .denied {
return NSLocalizedString("Notification permissions are currently denied. Enable notifications in the Settings app.", comment: "Notifications denied.")
}
}
return nil
}
}
// MARK: - UITableViewDelegate
extension NotificationsViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
if indexPath.section == 0 {
UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
}
}
}
// MARK: - UISearchControllerDelegate
extension NotificationsViewController: UISearchControllerDelegate {
func didDismissSearchController(_ searchController: UISearchController) {
print(#function)
searchController.isActive = false
notificationsTableView.reloadData()
}
}
// MARK: - UISearchBarDelegate
extension NotificationsViewController: UISearchBarDelegate {
func searchBarTextDidBeginEditing(_ searchBar: UISearchBar) {
searchController.isActive = true
newArticleNotificationFilter = false
notificationsTableView.reloadData()
}
func searchBar(_ searchBar: UISearchBar, textDidChange searchText: String) {
notificationsTableView.reloadData()
}
}

View File

@@ -1,515 +0,0 @@
//
// SettingsViewController.swift
// NetNewsWire-iOS
//
// Created by Maurice Parker on 4/24/19.
// Copyright © 2019 Ranchero Software. All rights reserved.
//
import UIKit
import Account
import CoreServices
import SafariServices
import SwiftUI
import UniformTypeIdentifiers
import UserNotifications
import RSCore
class SettingsViewController: UITableViewController, Logging {
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?
var notificationStatus: UNAuthorizationStatus = .notDetermined
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)
NotificationCenter.default.addObserver(self, selector: #selector(activeExtensionPointsDidChange), name: .ActiveExtensionPointsDidChange, object: nil)
NotificationCenter.default.addObserver(self, selector: #selector(refreshNotificationStatus(_:)), name: UIScene.willEnterForegroundNotification, object: nil)
tableView.register(UINib(nibName: "SettingsComboTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsComboTableViewCell")
tableView.register(UINib(nibName: "SettingsTableViewCell", bundle: nil), forCellReuseIdentifier: "SettingsTableViewCell")
refreshNotificationStatus()
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
}
colorPaletteDetailLabel.text = String(describing: AppDefaults.userInterfaceColorPalette)
openLinksInNetNewsWire.isOn = !AppDefaults.shared.useSystemBrowser
}
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
}
}
@objc
func refreshNotificationStatus(_ sender: Any? = nil) {
UNUserNotificationCenter.current().getNotificationSettings { settings in
DispatchQueue.main.async {
self.notificationStatus = settings.authorizationStatus
self.tableView.reloadData()
}
}
}
// MARK: UITableView
override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
switch section {
case 0:
if notificationStatus == .authorized { return 2 }
return 1
case 1:
return AccountManager.shared.accounts.count + 1
case 2:
return ExtensionPointManager.shared.activeExtensionPoints.count + 1
case 3:
let defaultNumberOfRows = super.tableView(tableView, numberOfRowsInSection: section)
if AccountManager.shared.activeAccounts.isEmpty || AccountManager.shared.anyAccountHasNetNewsWireNewsSubscription() {
return defaultNumberOfRows - 1
}
return defaultNumberOfRows
case 5:
return 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 = AppAssets.image(for: account.type)
acctCell.comboNameLabel?.text = account.nameForDisplay
cell = acctCell
}
case 2:
let extensionPoints = Array(ExtensionPointManager.shared.activeExtensionPoints.values)
if indexPath.row == extensionPoints.count {
cell = tableView.dequeueReusableCell(withIdentifier: "SettingsTableViewCell", for: indexPath)
cell.textLabel?.text = NSLocalizedString("Add Extension", comment: "Extensions")
} else {
let acctCell = tableView.dequeueReusableCell(withIdentifier: "SettingsComboTableViewCell", for: indexPath) as! SettingsComboTableViewCell
acctCell.applyThemeProperties()
let extensionPoint = extensionPoints[indexPath.row]
acctCell.comboImage?.image = extensionPoint.image
acctCell.comboNameLabel?.text = extensionPoint.title
cell = acctCell
}
default:
cell = super.tableView(tableView, cellForRowAt: indexPath)
}
return cell
}
override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
switch indexPath.section {
case 0:
if indexPath.row == 0 {
UIApplication.shared.open(URL(string: "\(UIApplication.openSettingsURLString)")!)
tableView.selectRow(at: nil, animated: true, scrollPosition: .none)
} else {
let controller = UIStoryboard.settings.instantiateController(ofType: NotificationsViewController.self)
self.navigationController?.pushViewController(controller, animated: true)
}
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:
let extensionPoints = Array(ExtensionPointManager.shared.activeExtensionPoints.values)
if indexPath.row == extensionPoints.count {
let controller = UIStoryboard.settings.instantiateController(ofType: AddExtensionPointViewController.self)
self.navigationController?.pushViewController(controller, animated: true)
} else {
let controller = UIStoryboard.inspector.instantiateController(ofType: ExtensionPointInspectorViewController.self)
controller.extensionPoint = extensionPoints[indexPath.row]
self.navigationController?.pushViewController(controller, animated: true)
}
case 3:
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 4:
switch indexPath.row {
case 3:
let timeline = UIStoryboard.settings.instantiateController(ofType: TimelineCustomizerViewController.self)
self.navigationController?.pushViewController(timeline, animated: true)
default:
break
}
case 5:
switch indexPath.row {
case 0:
let articleThemes = UIStoryboard.settings.instantiateController(ofType: ArticleThemesTableViewController.self)
self.navigationController?.pushViewController(articleThemes, animated: true)
default:
break
}
case 6:
let colorPalette = UIStoryboard.settings.instantiateController(ofType: ColorPaletteTableViewController.self)
self.navigationController?.pushViewController(colorPalette, animated: true)
case 7:
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 hosting = UIHostingController(rootView: AboutView())
self.navigationController?.pushViewController(hosting, 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 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 activeExtensionPointsDidChange() {
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
switch result {
case .success:
break
case .failure:
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: "AddWebFeedViewControllerNav") 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 utiArray = UTType.types(tag: "opml", tagClass: .filenameExtension, conformingTo: nil)
let docPicker = UIDocumentPickerViewController(forOpeningContentTypes: utiArray, asCopy: true)
docPicker.delegate = self
docPicker.modalPresentationStyle = .formSheet
self.present(docPicker, 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)
logger.error("OPML Export Error: \(error.localizedDescription, privacy: .public)")
}
let docPicker = UIDocumentPickerViewController(forExporting: [tempFile], asCopy: true)
docPicker.modalPresentationStyle = .formSheet
self.present(docPicker, animated: true)
}
func openURL(_ urlString: String) {
let vc = SFSafariViewController(url: URL(string: urlString)!)
vc.modalPresentationStyle = .pageSheet
present(vc, animated: true)
}
}

View File

@@ -13,6 +13,7 @@ import UserNotifications
struct SettingsView: View {
@Environment(\.dismiss) var dismiss
@StateObject private var appDefaults = AppDefaults.shared
@StateObject private var viewModel = SettingsViewModel()
@@ -100,6 +101,9 @@ struct SettingsView: View {
}
}
}
.onReceive(NotificationCenter.default.publisher(for: .LaunchedFromExternalAction), perform: { _ in
dismiss()
})
.fileImporter(isPresented: $viewModel.showImportView, allowedContentTypes: OPMLDocument.readableContentTypes) { result in
switch result {
case .success(let url):