diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index ebf1bd30d..aa4713f2a 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) { @@ -912,27 +910,37 @@ 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 var informativeText: String = "" if let decodingError = error as? DecodingError { switch decodingError { case .typeMismatch(let type, _): - informativeText = "Type '\(type)' mismatch." + 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 = "Value '\(value)' not found." + 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 = "Key '\(codingKey.stringValue)' not found." - 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 } @@ -944,9 +952,26 @@ 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 = informativeText + 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.buttons[0].keyEquivalent = "\033" + alert.buttons[1].keyEquivalent = "\r" + 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() {} 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}` diff --git a/iOS/UIKit Extensions/UIViewController-Extensions.swift b/iOS/UIKit Extensions/UIViewController-Extensions.swift index 9d06d7609..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 = "Type '\(type)' mismatch." - 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 = "Value '\(value)' not found." - 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 = "Key '\(codingKey.stringValue)' not found." - 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")