diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift
index dba09c5e2..d56800522 100644
--- a/Mac/AppDelegate.swift
+++ b/Mac/AppDelegate.swift
@@ -320,7 +320,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
}
func application(_ sender: NSApplication, openFile filename: String) -> Bool {
- guard filename.hasSuffix(".nnwtheme") else { return false }
+ guard filename.hasSuffix(ArticleTheme.nnwThemeSuffix) else { return false }
importTheme(filename: filename)
return true
}
@@ -812,36 +812,71 @@ private extension AppDelegate {
attrs[.font] = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize)
attrs[.foregroundColor] = NSColor.textColor
- let titleParagraphStyle = NSMutableParagraphStyle()
- titleParagraphStyle.alignment = .center
- attrs[.paragraphStyle] = titleParagraphStyle
+ if #available(macOS 11.0, *) {
+ let titleParagraphStyle = NSMutableParagraphStyle()
+ titleParagraphStyle.alignment = .center
+ attrs[.paragraphStyle] = titleParagraphStyle
+ }
let websiteText = NSMutableAttributedString()
websiteText.append(NSAttributedString(string: NSLocalizedString("Author's Website", comment: "Author's Website"), attributes: attrs))
- websiteText.append(NSAttributedString(string: "\n"))
+
+ if #available(macOS 11.0, *) {
+ websiteText.append(NSAttributedString(string: "\n"))
+ } else {
+ websiteText.append(NSAttributedString(string: " "))
+ }
attrs[.link] = theme.creatorHomePage
websiteText.append(NSAttributedString(string: theme.creatorHomePage, attributes: attrs))
- let textView = NSTextView(frame: CGRect(x: 0, y: 0, width: 200, height: 15))
+ let textViewWidth: CGFloat
+ if #available(macOS 11.0, *) {
+ textViewWidth = 200
+ } else {
+ textViewWidth = 400
+ }
+
+ let textView = NSTextView(frame: CGRect(x: 0, y: 0, width: textViewWidth, height: 15))
textView.isEditable = false
textView.drawsBackground = false
textView.textStorage?.setAttributedString(websiteText)
alert.accessoryView = textView
- alert.addButton(withTitle: NSLocalizedString("Install Style", comment: "Install Style"))
- alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Style"))
+ alert.addButton(withTitle: NSLocalizedString("Install Theme", comment: "Install Theme"))
+ alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme"))
- alert.beginSheetModal(for: window) { [weak self] result in
+ func importTheme() {
+ do {
+ try ArticleThemesManager.shared.importTheme(filename: filename)
+ confirmImportSuccess(themeName: theme.name)
+ } catch {
+ NSApplication.shared.presentError(error)
+ }
+ }
+
+ alert.beginSheetModal(for: window) { result in
if result == NSApplication.ModalResponse.alertFirstButtonReturn {
- guard let self = self else { return }
-
- do {
- try ArticleThemesManager.shared.importTheme(filename: filename)
- self.confirmImportSuccess(themeName: theme.name)
- } catch {
- NSApplication.shared.presentError(error)
+
+ if ArticleThemesManager.shared.themeExists(filename: filename) {
+ let alert = NSAlert()
+ alert.alertStyle = .warning
+
+ let localizedMessageText = NSLocalizedString("The theme “%@” already exists. Overwrite it?", comment: "Overwrite theme")
+ alert.messageText = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name) as String
+
+ alert.addButton(withTitle: NSLocalizedString("Overwrite", comment: "Overwrite"))
+ alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme"))
+
+ alert.beginSheetModal(for: window) { result in
+ if result == NSApplication.ModalResponse.alertFirstButtonReturn {
+ importTheme()
+ }
+ }
+ } else {
+ importTheme()
}
+
}
}
}
diff --git a/Mac/Base.lproj/Main.storyboard b/Mac/Base.lproj/Main.storyboard
index a1f665aa2..1de711d58 100644
--- a/Mac/Base.lproj/Main.storyboard
+++ b/Mac/Base.lproj/Main.storyboard
@@ -157,6 +157,18 @@
+
+
+[[date_medium]]
+[[external_link]]
+[[body]]
+
diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift
index 278097ee7..1267bfd02 100644
--- a/iOS/SceneCoordinator.swift
+++ b/iOS/SceneCoordinator.swift
@@ -156,6 +156,21 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
private var exceptionArticleFetcher: ArticleFetcher?
private(set) var timelineFeed: Feed?
+ // We have to defer the selecting of the feed and article due to a behavior (bug?) in iOS 15.
+ // iOS 15 will crash if you are in landscape on an iPad and are restoring article state. We
+ // have no idea why this is, but it happens when you do a select on a UITableView right before
+ // doing a diffable datasource apply.
+ //
+ // Steps to recreate:
+ //
+ // * Try to relaunch the app in the sim.
+ // * Press the Stop button in Xcode
+ // * Wait for all the app suspension activities to complete (widget data, etc)
+ // * Once the article has loaded, navigate to the iPad home screen
+ // * While in landscape, select a feed and then select an article
+ // * Install a fresh build of NNW to an iPad simulator (11 or 12.9' will do) running iPadOS 15
+ private var deferredFeedAndArticleSelect: (feedIndexPath: IndexPath, articleID: String)?
+
var timelineMiddleIndexPath: IndexPath?
private(set) var showFeedNames = ShowFeedName.none
@@ -437,6 +452,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
}
rebuildBackingStores(initialLoad: true)
treeControllerDelegate.resetFilterExceptions()
+
+ if let (feedIndexPath, articleID) = deferredFeedAndArticleSelect {
+ selectFeed(indexPath: feedIndexPath) {
+ self.selectArticleInCurrentFeed(articleID)
+ }
+ }
}
@objc func unreadCountDidChange(_ note: Notification) {
@@ -1261,6 +1282,72 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider {
self.selectArticle(article)
}
}
+
+ func importTheme(filename: String) {
+ let theme = ArticleTheme(path: filename)
+
+ let localizedTitleText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text")
+ let title = NSString.localizedStringWithFormat(localizedTitleText as NSString, theme.name, theme.creatorName) as String
+
+ let localizedMessageText = NSLocalizedString("Author's Website:\n%@", comment: "Authors website")
+ 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 url = URL(string: theme.creatorHomePage) {
+ let visitSiteTitle = NSLocalizedString("Show Website", comment: "Show Website")
+ let visitSiteAction = UIAlertAction(title: visitSiteTitle, style: .default) { [weak self] action in
+ UIApplication.shared.open(url)
+ self?.importTheme(filename: filename)
+ }
+ alertController.addAction(visitSiteAction)
+ }
+
+ func importTheme() {
+ do {
+ try ArticleThemesManager.shared.importTheme(filename: filename)
+ confirmImportSuccess(themeName: theme.name)
+ } catch {
+ rootSplitViewController.presentError(error)
+ }
+ }
+
+ let installThemeTitle = NSLocalizedString("Install Theme", comment: "Install Theme")
+ let installThemeAction = UIAlertAction(title: installThemeTitle, style: .default) { [weak self] action in
+
+ if ArticleThemesManager.shared.themeExists(filename: filename) {
+ let title = NSLocalizedString("Duplicate Theme", comment: "Duplicate Theme")
+ let localizedMessageText = NSLocalizedString("The theme “%@” already exists. Overwrite it?", comment: "Overwrite theme")
+ let message = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name) as String
+
+ let alertController = UIAlertController(title: title, message: message, preferredStyle: .alert)
+
+ 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
+ importTheme()
+ }
+ alertController.addAction(overwriteAction)
+ alertController.preferredAction = overwriteAction
+
+ self?.rootSplitViewController.present(alertController, animated: true)
+ } else {
+ importTheme()
+ }
+
+ }
+
+ alertController.addAction(installThemeAction)
+ alertController.preferredAction = installThemeAction
+
+ rootSplitViewController.present(alertController, animated: true)
+
+ }
+
}
// MARK: UISplitViewControllerDelegate
@@ -2269,12 +2356,24 @@ private extension SceneCoordinator {
func selectFeedAndArticle(feedNode: Node, articleID: String) -> Bool {
if let feedIndexPath = indexPathFor(feedNode) {
- selectFeed(indexPath: feedIndexPath) {
- self.selectArticleInCurrentFeed(articleID)
- }
+ deferredFeedAndArticleSelect = (feedIndexPath, articleID)
return true
}
return false
}
+ func confirmImportSuccess(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))
+
+ rootSplitViewController.present(alertController, animated: true)
+ }
+
}
diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift
index 39a26aa2d..7ae1674e3 100644
--- a/iOS/SceneDelegate.swift
+++ b/iOS/SceneDelegate.swift
@@ -162,6 +162,11 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate {
}
}
+ let filename = context.url.standardizedFileURL.path
+ if filename.hasSuffix(ArticleTheme.nnwThemeSuffix) {
+ self.coordinator.importTheme(filename: filename)
+ }
+
}
}
diff --git a/iOS/Settings/ArticleThemesTableViewController.swift b/iOS/Settings/ArticleThemesTableViewController.swift
new file mode 100644
index 000000000..7bc123341
--- /dev/null
+++ b/iOS/Settings/ArticleThemesTableViewController.swift
@@ -0,0 +1,94 @@
+//
+// ArticleThemesTableViewController.swift
+// NetNewsWire-iOS
+//
+// Created by Maurice Parker on 9/12/21.
+// Copyright © 2021 Ranchero Software. All rights reserved.
+//
+
+import Foundation
+
+import UIKit
+
+class ArticleThemesTableViewController: UITableViewController {
+
+ override func viewDidLoad() {
+ NotificationCenter.default.addObserver(self, selector: #selector(articleThemeNamesDidChangeNotification(_:)), name: .ArticleThemeNamesDidChangeNotification, object: nil)
+ }
+
+ @objc func articleThemeNamesDidChangeNotification(_ note: Notification) {
+ tableView.reloadData()
+ }
+
+ // MARK: - Table view data source
+
+ override func numberOfSections(in tableView: UITableView) -> Int {
+ return 1
+ }
+
+ override func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
+ return ArticleThemesManager.shared.themeNames.count + 1
+ }
+
+ 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
+ }
+
+ override func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
+ guard let cell = tableView.cellForRow(at: indexPath), let themeName = cell.textLabel?.text else { return }
+ ArticleThemesManager.shared.currentThemeName = themeName
+ navigationController?.popViewController(animated: true)
+ }
+
+ override func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
+ guard indexPath.row != 0,
+ let cell = tableView.cellForRow(at: indexPath),
+ let themeName = cell.textLabel?.text else { return nil }
+
+ let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
+ let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, 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
+ completion(true)
+ }
+ alertController.addAction(cancelAction)
+
+ let deleteTitle = NSLocalizedString("Delete", comment: "Delete")
+ let deleteAction = UIAlertAction(title: deleteTitle, style: .destructive) { action 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])
+ }
+}
diff --git a/iOS/Settings/Settings.storyboard b/iOS/Settings/Settings.storyboard
index c15ab7f81..9cb9246b5 100644
--- a/iOS/Settings/Settings.storyboard
+++ b/iOS/Settings/Settings.storyboard
@@ -264,9 +264,41 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -298,7 +330,7 @@
-
+
@@ -332,7 +364,7 @@
-
+
@@ -377,20 +409,20 @@
-
+
-
+
-
+
-
+
@@ -1161,6 +1194,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -1172,7 +1244,7 @@
-
+
diff --git a/iOS/Settings/SettingsViewController.swift b/iOS/Settings/SettingsViewController.swift
index 50ad9e7bf..23811c861 100644
--- a/iOS/Settings/SettingsViewController.swift
+++ b/iOS/Settings/SettingsViewController.swift
@@ -19,18 +19,15 @@ class SettingsViewController: UITableViewController {
@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)
@@ -69,6 +66,8 @@ class SettingsViewController: UITableViewController {
} else {
refreshClearsReadArticlesSwitch.isOn = false
}
+
+ articleThemeDetailLabel.text = ArticleThemesManager.shared.currentTheme.name
if AppDefaults.shared.confirmMarkAllAsRead {
confirmMarkAllAsReadSwitch.isOn = true
@@ -128,7 +127,7 @@ class SettingsViewController: UITableViewController {
}
return defaultNumberOfRows
case 5:
- return traitCollection.userInterfaceIdiom == .phone ? 3 : 2
+ return traitCollection.userInterfaceIdiom == .phone ? 4 : 3
default:
return super.tableView(tableView, numberOfRowsInSection: section)
}
@@ -229,6 +228,14 @@ class SettingsViewController: UITableViewController {
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)