From e9b84d92194e7dcadeaf574d698bdd0574629319 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 23 Sep 2021 20:12:35 +0800 Subject: [PATCH 1/5] Theme Import/Opening Changes Mac: - Better error messages - The alert displayed contains an additional button to open the theme's folder (when clicked it will not dismiss the alert). --- Mac/AppDelegate.swift | 36 +++++++++++++------ Shared/ArticleStyles/ArticleTheme.swift | 1 - .../ArticleThemeDownloader.swift | 1 - 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index ebf1bd30d..cfb108eed 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -107,6 +107,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, private var softwareUpdater: SPUUpdater! private var crashReporter: PLCrashReporter! #endif + + private var themeImportPath: String? override init() { NSWindow.allowsAutomaticWindowTabbing = false @@ -890,15 +892,11 @@ internal extension AppDelegate { } else { importTheme() } - } } } catch { - NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error]) + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error, "path": filename]) } - - - } func confirmImportSuccess(themeName: String) { @@ -921,16 +919,16 @@ internal extension AppDelegate { let window = mainWindowController?.window else { return } - + themeImportPath = userInfo["path"] as? String var informativeText: String = "" if let decodingError = error as? DecodingError { switch decodingError { case .typeMismatch(let type, _): - informativeText = "Type '\(type)' mismatch." + informativeText = "the type—'\(type)'—is mismatched." case .valueNotFound(let value, _): - informativeText = "Value '\(value)' not found." + informativeText = "the value—'\(value)'—is not found." case .keyNotFound(let codingKey, _): - informativeText = "Key '\(codingKey.stringValue)' not found." + informativeText = "the key—'\(codingKey.stringValue)'—is not found." case .dataCorrupted( _): informativeText = error.localizedDescription default: @@ -944,9 +942,25 @@ internal extension AppDelegate { let alert = NSAlert() alert.alertStyle = .warning alert.messageText = NSLocalizedString("Theme Error", comment: "Theme download error") - alert.informativeText = NSLocalizedString("This theme cannot be imported due to the following error: \(informativeText)", comment: "Theme download error information") + alert.informativeText = NSLocalizedString("This theme cannot be used because \(informativeText)", comment: "Theme download error information") + alert.addButton(withTitle: NSLocalizedString("Open Theme Folder", comment: "Open Theme Folder")) alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) - alert.beginSheetModal(for: window) + + let button = alert.buttons.first + button?.target = self + button?.action = #selector(self.openThemesFolder(_:)) + + alert.runModal() + } + } + + @objc func openThemesFolder(_ sender: Any) { + if themeImportPath == nil { + let url = URL(fileURLWithPath: ArticleThemesManager.shared.folderPath) + NSWorkspace.shared.open(url) + } else { + let url = URL(fileURLWithPath: themeImportPath!) + NSWorkspace.shared.open(url.deletingLastPathComponent()) } } diff --git a/Shared/ArticleStyles/ArticleTheme.swift b/Shared/ArticleStyles/ArticleTheme.swift index dc03e816f..e40bde46f 100644 --- a/Shared/ArticleStyles/ArticleTheme.swift +++ b/Shared/ArticleStyles/ArticleTheme.swift @@ -58,7 +58,6 @@ struct ArticleTheme: Equatable { let data = try Data(contentsOf: URL(fileURLWithPath: infoPath)) self.info = try PropertyListDecoder().decode(ArticleThemePlist.self, from: data) - let corePath = Bundle.main.path(forResource: "core", ofType: "css")! let stylesheetPath = (path as NSString).appendingPathComponent("stylesheet.css") if let stylesheetCSS = Self.stringAtPath(stylesheetPath) { diff --git a/Shared/ArticleStyles/ArticleThemeDownloader.swift b/Shared/ArticleStyles/ArticleThemeDownloader.swift index 0e94437ee..7c3e5534c 100644 --- a/Shared/ArticleStyles/ArticleThemeDownloader.swift +++ b/Shared/ArticleStyles/ArticleThemeDownloader.swift @@ -22,7 +22,6 @@ public class ArticleThemeDownloader { } } - public static let shared = ArticleThemeDownloader() private init() {} From 88674d2fc4f379070714773a0e98c12e8b5f7a40 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 23 Sep 2021 20:16:09 +0800 Subject: [PATCH 2/5] Consistent theme error messages on iOS --- iOS/UIKit Extensions/UIViewController-Extensions.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iOS/UIKit Extensions/UIViewController-Extensions.swift b/iOS/UIKit Extensions/UIViewController-Extensions.swift index 9d06d7609..4f257e7b5 100644 --- a/iOS/UIKit Extensions/UIViewController-Extensions.swift +++ b/iOS/UIKit Extensions/UIViewController-Extensions.swift @@ -19,13 +19,13 @@ extension UIViewController { let errorTitle = NSLocalizedString("Error", comment: "Error") switch decodingError { case .typeMismatch(let type, _): - let str = "Type '\(type)' mismatch." + let str = "This theme cannot be used because the type—'\(type)'—is mismatched." presentError(title: errorTitle, message: str, dismiss: dismiss) case .valueNotFound(let value, _): - let str = "Value '\(value)' not found." + let str = "This theme cannot be used because the value—'\(value)'—is not found." presentError(title: errorTitle, message: str, dismiss: dismiss) case .keyNotFound(let codingKey, _): - let str = "Key '\(codingKey.stringValue)' not found." + let str = "This theme cannot be used because the key—'\(codingKey.stringValue)'—is not found." presentError(title: errorTitle, message: str, dismiss: dismiss) case .dataCorrupted( _): presentError(title: errorTitle, message: error.localizedDescription, dismiss: dismiss) From bd6095517ba60928638311dc15e321fd3c42d252 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 23 Sep 2021 20:19:41 +0800 Subject: [PATCH 3/5] is missing instead of not found --- Mac/AppDelegate.swift | 4 ++-- iOS/UIKit Extensions/UIViewController-Extensions.swift | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index cfb108eed..e52f1be3b 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -926,9 +926,9 @@ internal extension AppDelegate { case .typeMismatch(let type, _): informativeText = "the type—'\(type)'—is mismatched." case .valueNotFound(let value, _): - informativeText = "the value—'\(value)'—is not found." + informativeText = "the value—'\(value)'—is missing in the Info.plist." case .keyNotFound(let codingKey, _): - informativeText = "the key—'\(codingKey.stringValue)'—is not found." + informativeText = "the key—'\(codingKey.stringValue)'—is missing in the Info.plist." case .dataCorrupted( _): informativeText = error.localizedDescription default: diff --git a/iOS/UIKit Extensions/UIViewController-Extensions.swift b/iOS/UIKit Extensions/UIViewController-Extensions.swift index 4f257e7b5..11e166e24 100644 --- a/iOS/UIKit Extensions/UIViewController-Extensions.swift +++ b/iOS/UIKit Extensions/UIViewController-Extensions.swift @@ -19,13 +19,13 @@ extension UIViewController { let errorTitle = NSLocalizedString("Error", comment: "Error") switch decodingError { case .typeMismatch(let type, _): - let str = "This theme cannot be used because the type—'\(type)'—is mismatched." + let str = "This theme cannot be used because the type—'\(type)'—is mismatched in the Info.plist." presentError(title: errorTitle, message: str, dismiss: dismiss) case .valueNotFound(let value, _): - let str = "This theme cannot be used because the value—'\(value)'—is not found." + let str = "This theme cannot be used because the value—'\(value)'—is missing in the Info.plist." presentError(title: errorTitle, message: str, dismiss: dismiss) case .keyNotFound(let codingKey, _): - let str = "This theme cannot be used because the key—'\(codingKey.stringValue)'—is not found." + let str = "This theme cannot be used because the key—'\(codingKey.stringValue)'—is missing in the Info.plist." presentError(title: errorTitle, message: str, dismiss: dismiss) case .dataCorrupted( _): presentError(title: errorTitle, message: error.localizedDescription, dismiss: dismiss) From 709d163e9c1e517037d50d6c42bb5a514ec6f735 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Thu, 23 Sep 2021 21:25:25 +0800 Subject: [PATCH 4/5] addtional notes in Themes.md --- Technotes/Themes.md | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/Technotes/Themes.md b/Technotes/Themes.md index 741e5e963..387c3cb65 100644 --- a/Technotes/Themes.md +++ b/Technotes/Themes.md @@ -1,7 +1,31 @@ # Themes -## Add Themes Directly to NetNewsWire -Theme developers: on iOS and macOS, themes can be opened directly in NetNewsWire using the below URL scheme: +## `.nnwtheme` Structure + +An `.nnwtheme` comprises of three files: +- `Info.plist` +- `template.html` +- `stylesheet.css` + +### Info.plist +The `Info.plist` requires the following keys/types: + +|Key|Type|Notes| +|---|---|---| +|`ThemeIdentifier`|`String`|Unique identifier for the theme, e.g. using reverse domain name.| +|`Name`|`String`|Theme name| +|`CreatorHomePage`|`String`|| +|`CreatorName`|`String`|| +|`Version`|`Integer`|| + +### template.html +This provides a starting point for editing the structure of the page. Theme variables are documented in the header. + +### stylesheet.css +This provides a starting point for editing the style of the page. + +## Add Themes Directly to NetNewsWire with URL Scheme +On iOS and macOS, themes can be opened directly in NetNewsWire using the below URL scheme: `netnewswire://theme/add?url={url}` From 25f989683279d0d535ed6d2cba3251dc88f2dc52 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Fri, 24 Sep 2021 09:28:32 +0800 Subject: [PATCH 5/5] Localized strings for errors --- Mac/AppDelegate.swift | 29 +++++++++++------ .../UIViewController-Extensions.swift | 32 +++++++++++++------ 2 files changed, 43 insertions(+), 18 deletions(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index e52f1be3b..aa4713f2a 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -910,13 +910,13 @@ internal extension AppDelegate { alert.informativeText = NSString.localizedStringWithFormat(localizedInformativeText as NSString, themeName) as String alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) + alert.beginSheetModal(for: window) } @objc func themeImportError(_ note: Notification) { guard let userInfo = note.userInfo, - let error = userInfo["error"] as? Error, - let window = mainWindowController?.window else { + let error = userInfo["error"] as? Error else { return } themeImportPath = userInfo["path"] as? String @@ -924,13 +924,23 @@ internal extension AppDelegate { if let decodingError = error as? DecodingError { switch decodingError { case .typeMismatch(let type, _): - informativeText = "the type—'\(type)'—is mismatched." + let localizedError = NSLocalizedString("This theme cannot be used because the the type—“%@”—is mismatched in the Info.plist", comment: "Type mismatch") + informativeText = NSString.localizedStringWithFormat(localizedError as NSString, type as! CVarArg) as String case .valueNotFound(let value, _): - informativeText = "the value—'\(value)'—is missing in the Info.plist." + let localizedError = NSLocalizedString("This theme cannot be used because the the value—“%@”—is not found in the Info.plist.", comment: "Decoding value missing") + informativeText = NSString.localizedStringWithFormat(localizedError as NSString, value as! CVarArg) as String case .keyNotFound(let codingKey, _): - informativeText = "the key—'\(codingKey.stringValue)'—is missing in the Info.plist." - case .dataCorrupted( _): - informativeText = error.localizedDescription + let localizedError = NSLocalizedString("This theme cannot be used because the the key—“%@”—is not found in the Info.plist.", comment: "Decoding key missing") + informativeText = NSString.localizedStringWithFormat(localizedError as NSString, codingKey.stringValue) as String + case .dataCorrupted(let context): + guard let error = context.underlyingError as NSError?, + let debugDescription = error.userInfo["NSDebugDescription"] as? String else { + informativeText = error.localizedDescription + break + } + 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 + default: informativeText = error.localizedDescription } @@ -942,14 +952,15 @@ internal extension AppDelegate { let alert = NSAlert() alert.alertStyle = .warning alert.messageText = NSLocalizedString("Theme Error", comment: "Theme download error") - alert.informativeText = NSLocalizedString("This theme cannot be used because \(informativeText)", comment: "Theme download error information") + alert.informativeText = informativeText alert.addButton(withTitle: NSLocalizedString("Open Theme Folder", comment: "Open Theme Folder")) alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) let button = alert.buttons.first button?.target = self button?.action = #selector(self.openThemesFolder(_:)) - + alert.buttons[0].keyEquivalent = "\033" + alert.buttons[1].keyEquivalent = "\r" alert.runModal() } } diff --git a/iOS/UIKit Extensions/UIViewController-Extensions.swift b/iOS/UIKit Extensions/UIViewController-Extensions.swift index 11e166e24..e81f3745e 100644 --- a/iOS/UIKit Extensions/UIViewController-Extensions.swift +++ b/iOS/UIKit Extensions/UIViewController-Extensions.swift @@ -17,20 +17,34 @@ extension UIViewController { presentAccountError(accountError, dismiss: dismiss) } else if let decodingError = error as? DecodingError { let errorTitle = NSLocalizedString("Error", comment: "Error") + let infromativeText: String = "" switch decodingError { case .typeMismatch(let type, _): - let str = "This theme cannot be used because the type—'\(type)'—is mismatched in the Info.plist." - presentError(title: errorTitle, message: str, dismiss: dismiss) + let localizedError = NSLocalizedString("This theme cannot be used because the the type—“%@”—is mismatched in the Info.plist", comment: "Type mismatch") + informativeText = NSString.localizedStringWithFormat(localizedError as NSString, type as! CVarArg) as String + presentError(title: title, message: infromativeText, dismiss: dismiss) case .valueNotFound(let value, _): - let str = "This theme cannot be used because the value—'\(value)'—is missing in the Info.plist." - presentError(title: errorTitle, message: str, dismiss: dismiss) + let localizedError = NSLocalizedString("This theme cannot be used because the the value—“%@”—is not found in the Info.plist.", comment: "Decoding value missing") + informativeText = NSString.localizedStringWithFormat(localizedError as NSString, value as! CVarArg) as String + presentError(title: title, message: infromativeText, dismiss: dismiss) case .keyNotFound(let codingKey, _): - let str = "This theme cannot be used because the key—'\(codingKey.stringValue)'—is missing in the Info.plist." - presentError(title: errorTitle, message: str, dismiss: dismiss) - case .dataCorrupted( _): - presentError(title: errorTitle, message: error.localizedDescription, dismiss: dismiss) + let localizedError = NSLocalizedString("This theme cannot be used because the the key—“%@”—is not found in the Info.plist.", comment: "Decoding key missing") + informativeText = NSString.localizedStringWithFormat(localizedError as NSString, codingKey.stringValue) as String + presentError(title: title, message: infromativeText, dismiss: dismiss) + case .dataCorrupted(let context): + guard let error = context.underlyingError as NSError?, + let debugDescription = error.userInfo["NSDebugDescription"] as? String else { + informativeText = error.localizedDescription + presentError(title: title, message: infromativeText, dismiss: dismiss) + return + } + 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: title, message: infromativeText, dismiss: dismiss) + default: - presentError(title: errorTitle, message: error.localizedDescription, dismiss: dismiss) + informativeText = error.localizedDescription + presentError(title: title, message: infromativeText, dismiss: dismiss) } } else { let errorTitle = NSLocalizedString("Error", comment: "Error")