diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index f0b22efd6..4dbe1da45 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -110,8 +110,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, private var crashReporter: PLCrashReporter! #endif - private var themeImportPath: String? - override init() { NSWindow.allowsAutomaticWindowTabbing = false super.init() @@ -130,7 +128,6 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, NotificationCenter.default.addObserver(self, selector: #selector(unreadCountDidChange(_:)), name: .UnreadCountDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(inspectableObjectsDidChange(_:)), name: .InspectableObjectsDidChange, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(themeImportError(_:)), name: .didFailToImportThemeWithError, object: nil) NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil) appDelegate = self @@ -348,7 +345,51 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, while !isShutDownSyncDone && RunLoop.current.run(mode: .default, before: timeout) && timeout > Date() { } } + func presentThemeImportError(_ error: Error) { + var informativeText: String = "" + + if let decodingError = error as? DecodingError { + switch decodingError { + case .typeMismatch(let type, _): + 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, _): + 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, _): + 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 underlyingError = context.underlyingError as NSError?, + let debugDescription = underlyingError.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 + } + } else { + informativeText = error.localizedDescription + } + + DispatchQueue.main.async { + let alert = NSAlert() + alert.alertStyle = .warning + alert.messageText = NSLocalizedString("Theme Error", comment: "Theme error") + alert.informativeText = informativeText + alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) + + alert.buttons[0].keyEquivalent = "\r" + + let response = alert.runModal() + } + } + // MARK: Notifications + @objc func unreadCountDidChange(_ note: Notification) { if note.object is AccountManager { unreadCount = AccountManager.shared.unreadCount @@ -949,8 +990,7 @@ internal extension AppDelegate { } } } catch { - NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error, "path": filename]) - logger.error("Error importing theme: \(error.localizedDescription, privacy: .public)") + presentThemeImportError(error) } } @@ -969,67 +1009,6 @@ internal extension AppDelegate { alert.beginSheetModal(for: window) } - @objc func themeImportError(_ note: Notification) { - guard let userInfo = note.userInfo, - 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, _): - 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, _): - 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, _): - 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 underlyingError = context.underlyingError as NSError?, - let debugDescription = underlyingError.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 - } - } else { - informativeText = error.localizedDescription - } - - DispatchQueue.main.async { - let alert = NSAlert() - alert.alertStyle = .warning - alert.messageText = NSLocalizedString("Theme Error", comment: "Theme download error") - 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() - } - } - - @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/Mac/Scriptability/AppDelegate+Scriptability.swift b/Mac/Scriptability/AppDelegate+Scriptability.swift index cfd8310f5..07f3057c2 100644 --- a/Mac/Scriptability/AppDelegate+Scriptability.swift +++ b/Mac/Scriptability/AppDelegate+Scriptability.swift @@ -56,15 +56,14 @@ extension AppDelegate : AppDelegateAppleEvents { if let themeURL = URL(string: themeURLString) { let request = URLRequest(url: themeURL) let task = URLSession.shared.downloadTask(with: request) { [weak self] location, response, error in - guard let location = location else { + guard let self, let location else { return } do { try ArticleThemeDownloader.shared.handleFile(at: location) } catch { - NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) - self?.logger.error("Failed to import theme: \(error.localizedDescription, privacy: .public)") + self.presentThemeImportError(error) } } task.resume() diff --git a/Shared/ArticleStyles/ArticleTheme+Notifications.swift b/Shared/ArticleStyles/ArticleTheme+Notifications.swift index 4c2391ff4..298a28c47 100644 --- a/Shared/ArticleStyles/ArticleTheme+Notifications.swift +++ b/Shared/ArticleStyles/ArticleTheme+Notifications.swift @@ -11,5 +11,4 @@ import Foundation extension Notification.Name { static let didBeginDownloadingTheme = Notification.Name("didBeginDownloadingTheme") static let didEndDownloadingTheme = Notification.Name("didEndDownloadingTheme") - static let didFailToImportThemeWithError = Notification.Name("didFailToImportThemeWithError") } diff --git a/Shared/ArticleStyles/ArticleThemesManager.swift b/Shared/ArticleStyles/ArticleThemesManager.swift index dea37cb62..254cccbd1 100644 --- a/Shared/ArticleStyles/ArticleThemesManager.swift +++ b/Shared/ArticleStyles/ArticleThemesManager.swift @@ -30,13 +30,24 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging { } set { if newValue != currentThemeName { - AppDefaults.shared.currentThemeName = newValue - currentTheme = articleThemeWithThemeName(newValue) + do { + currentTheme = try articleThemeWithThemeName(newValue) + AppDefaults.shared.currentThemeName = newValue + } catch { + logger.error("Unable to set new theme: \(error.localizedDescription, privacy: .public)") + } } } } - lazy var currentTheme = { articleThemeWithThemeName(currentThemeName) }() { + lazy var currentTheme = { + do { + return try articleThemeWithThemeName(currentThemeName) + } catch { + logger.error("Unable to load theme \(self.currentThemeName): \(error.localizedDescription, privacy: .public)") + return ArticleTheme.defaultTheme + } + }() { didSet { NotificationCenter.default.post(name: .CurrentArticleThemeDidChangeNotification, object: self) } @@ -66,7 +77,11 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging { func presentedSubitemDidChange(at url: URL) { themeNames = buildThemeNames() - currentTheme = articleThemeWithThemeName(currentThemeName) + do { + currentTheme = try articleThemeWithThemeName(currentThemeName) + } catch { + appDelegate.presentThemeImportError(error) + } } // MARK: API @@ -88,7 +103,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging { try FileManager.default.copyItem(atPath: filename, toPath: toFilename) } - func articleThemeWithThemeName(_ themeName: String) -> ArticleTheme { + func articleThemeWithThemeName(_ themeName: String) throws -> ArticleTheme { if themeName == AppDefaults.defaultThemeName { return ArticleTheme.defaultTheme } @@ -105,14 +120,7 @@ final class ArticleThemesManager: NSObject, NSFilePresenter, Logging { return ArticleTheme.defaultTheme } - do { - return try ArticleTheme(path: path, isAppTheme: isAppTheme) - } catch { - NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) - logger.error("Failed to import theme: \(error.localizedDescription, privacy: .public)") - return ArticleTheme.defaultTheme - } - + return try ArticleTheme(path: path, isAppTheme: isAppTheme) } func deleteTheme(themeName: String) { diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index 3f9b865d1..0bac49d03 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -219,6 +219,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD } + func presentThemeImportError(_ error: Error) { + let windowScene = { + let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene } + return scenes.filter { $0.activationState == .foregroundActive }.first ?? scenes.first + }() + guard let sceneDelegate = windowScene?.delegate as? SceneDelegate else { return } + sceneDelegate.presentError(error) + } + } // MARK: App Initialization diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index a251350fd..772af21a8 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -326,7 +326,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(willEnterForeground(_:)), name: UIApplication.willEnterForegroundNotification, object: nil) NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(themeDownloadDidFail(_:)), name: .didFailToImportThemeWithError, object: nil) } func restoreWindowState(_ activity: NSUserActivity?) { @@ -543,16 +542,6 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } } - @objc func themeDownloadDidFail(_ note: Notification) { - guard let userInfo = note.userInfo, - let error = userInfo["error"] as? Error else { - return - } - DispatchQueue.main.async { - self.rootSplitViewController.presentError(error, dismiss: nil) - } - } - // MARK: API func suspend() { @@ -1275,13 +1264,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, Logging { } func importTheme(filename: String) { - do { - try ArticleThemeImporter.importTheme(controller: rootSplitViewController, filename: filename) - } catch { - NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error]) - logger.error("Failed to import theme with error: \(error.localizedDescription, privacy: .public)") - } - + ArticleThemeImporter.importTheme(controller: rootSplitViewController, filename: filename) } /// This will dismiss the foremost view controller if the user diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 0eb98e50a..4c2fc6966 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -96,6 +96,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, Logging { coordinator.cleanUp(conditional: conditional) } + func presentError(_ error: Error) { + self.window!.rootViewController?.presentError(error) + } + // Handle Opening of URLs func scene(_ scene: UIScene, openURLContexts urlContexts: Set) { @@ -186,14 +190,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, Logging { NotificationCenter.default.post(name: .didBeginDownloadingTheme, object: nil) } let task = URLSession.shared.downloadTask(with: request) { [weak self] location, response, error in - guard - let location = location else { return } + guard let self, let location else { return } do { try ArticleThemeDownloader.shared.handleFile(at: location) } catch { - NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) - self?.logger.error("Failed to import theme with error: \(error.localizedDescription, privacy: .public)") + self.presentError(error) } } task.resume() @@ -205,7 +207,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, Logging { return } - } } } diff --git a/iOS/Settings/ArticleThemeImporter.swift b/iOS/Settings/ArticleThemeImporter.swift index 1f71e6033..8e184842a 100644 --- a/iOS/Settings/ArticleThemeImporter.swift +++ b/iOS/Settings/ArticleThemeImporter.swift @@ -11,8 +11,14 @@ import RSCore struct ArticleThemeImporter: Logging { - static func importTheme(controller: UIViewController, filename: String) throws { - let theme = try ArticleTheme(path: filename, isAppTheme: false) + static func importTheme(controller: UIViewController, filename: String) { + let theme: ArticleTheme + do { + theme = try ArticleTheme(path: filename, isAppTheme: false) + } catch { + controller.presentError(error) + return + } let localizedTitleText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text") let title = NSString.localizedStringWithFormat(localizedTitleText as NSString, theme.name, theme.creatorName) as String @@ -29,7 +35,7 @@ struct ArticleThemeImporter: Logging { let visitSiteTitle = NSLocalizedString("Show Website", comment: "Show Website") let visitSiteAction = UIAlertAction(title: visitSiteTitle, style: .default) { action in UIApplication.shared.open(url) - try? Self.importTheme(controller: controller, filename: filename) + Self.importTheme(controller: controller, filename: filename) } alertController.addAction(visitSiteAction) } diff --git a/iOS/Settings/ArticleThemesTableViewController.swift b/iOS/Settings/ArticleThemesTableViewController.swift index cc2bcc465..deefb41e9 100644 --- a/iOS/Settings/ArticleThemesTableViewController.swift +++ b/iOS/Settings/ArticleThemesTableViewController.swift @@ -74,7 +74,7 @@ class ArticleThemesTableViewController: UITableViewController, Logging { guard let cell = tableView.cellForRow(at: indexPath), let themeName = cell.textLabel?.text else { return nil } - guard !ArticleThemesManager.shared.articleThemeWithThemeName(themeName).isAppTheme else { return nil } + guard let theme = try? ArticleThemesManager.shared.articleThemeWithThemeName(themeName), !theme.isAppTheme else { return nil } let deleteTitle = NSLocalizedString("Delete", comment: "Delete") let deleteAction = UIContextualAction(style: .normal, title: deleteTitle) { [weak self] (action, view, completion) in @@ -114,12 +114,7 @@ extension ArticleThemesTableViewController: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let url = urls.first else { return } - do { - try ArticleThemeImporter.importTheme(controller: self, filename: url.standardizedFileURL.path) - } catch { - NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) - logger.error("Did fail to import theme: \(error.localizedDescription, privacy: .public)") - } + try ArticleThemeImporter.importTheme(controller: self, filename: url.standardizedFileURL.path) } }