diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index d56800522..ebf1bd30d 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -125,6 +125,8 @@ 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 @@ -329,6 +331,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, shuttingDown = true saveState() + ArticleThemeDownloader.shared.cleanUp() + AccountManager.shared.sendArticleStatusAll() { self.isShutDownSyncDone = true } @@ -375,6 +379,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, @objc func didWakeNotification(_ note: Notification) { fireOldTimers() } + + @objc func importDownloadedTheme(_ note: Notification) { + guard let userInfo = note.userInfo, + let url = userInfo["url"] as? URL else { + return + } + DispatchQueue.main.async { + self.importTheme(filename: url.path) + } + } // MARK: Main Window @@ -765,7 +779,7 @@ extension AppDelegate { } -private extension AppDelegate { +internal extension AppDelegate { func fireOldTimers() { // It’s possible there’s a refresh timer set to go off in the past. @@ -800,85 +814,91 @@ private extension AppDelegate { func importTheme(filename: String) { guard let window = mainWindowController?.window else { return } - let theme = ArticleTheme(path: filename) - - let alert = NSAlert() - alert.alertStyle = .informational + do { + let theme = try ArticleTheme(path: filename) + let alert = NSAlert() + alert.alertStyle = .informational - let localizedMessageText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text") - alert.messageText = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name, theme.creatorName) as String - - var attrs = [NSAttributedString.Key : Any]() - attrs[.font] = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) - attrs[.foregroundColor] = NSColor.textColor - - 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)) - - 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 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 Theme", comment: "Install Theme")) - alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme")) + let localizedMessageText = NSLocalizedString("Install theme “%@” by %@?", comment: "Theme message text") + alert.messageText = NSString.localizedStringWithFormat(localizedMessageText as NSString, theme.name, theme.creatorName) as String - func importTheme() { - do { - try ArticleThemesManager.shared.importTheme(filename: filename) - confirmImportSuccess(themeName: theme.name) - } catch { - NSApplication.shared.presentError(error) + var attrs = [NSAttributedString.Key : Any]() + attrs[.font] = NSFont.systemFont(ofSize: NSFont.smallSystemFontSize) + attrs[.foregroundColor] = NSColor.textColor + + 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)) + + 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 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 Theme", comment: "Install Theme")) + alert.addButton(withTitle: NSLocalizedString("Cancel", comment: "Cancel Install Theme")) + + 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 { + + 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() + } + + } + } + } catch { + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error]) } - alert.beginSheetModal(for: window) { result in - if result == NSApplication.ModalResponse.alertFirstButtonReturn { - - 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() - } - - } - } + + } func confirmImportSuccess(themeName: String) { @@ -895,6 +915,41 @@ private extension AppDelegate { 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 { + return + } + + var informativeText: String = "" + if let decodingError = error as? DecodingError { + switch decodingError { + case .typeMismatch(let type, _): + informativeText = "Type '\(type)' mismatch." + case .valueNotFound(let value, _): + informativeText = "Value '\(value)' not found." + case .keyNotFound(let codingKey, _): + informativeText = "Key '\(codingKey.stringValue)' not found." + case .dataCorrupted( _): + informativeText = error.localizedDescription + 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 = NSLocalizedString("This theme cannot be imported due to the following error: \(informativeText)", comment: "Theme download error information") + alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) + alert.beginSheetModal(for: window) + } + } + } /* diff --git a/Mac/Resources/Info.plist b/Mac/Resources/Info.plist index ce803e9ed..be2d2bf9e 100644 --- a/Mac/Resources/Info.plist +++ b/Mac/Resources/Info.plist @@ -31,6 +31,7 @@ RSS Feed CFBundleURLSchemes + netnewswire feed feeds x-netnewswire-feed diff --git a/Mac/Scriptability/AppDelegate+Scriptability.swift b/Mac/Scriptability/AppDelegate+Scriptability.swift index 2b6b6f5db..132fe26be 100644 --- a/Mac/Scriptability/AppDelegate+Scriptability.swift +++ b/Mac/Scriptability/AppDelegate+Scriptability.swift @@ -18,6 +18,7 @@ import Foundation import Articles +import Zip protocol AppDelegateAppleEvents { func installAppleEventHandlers() @@ -44,6 +45,34 @@ extension AppDelegate : AppDelegateAppleEvents { return } + // Handle themes + if urlString.hasPrefix("netnewswire://theme") { + guard let comps = URLComponents(string: urlString), + let queryItems = comps.queryItems, + let themeURLString = queryItems.first(where: { $0.name == "url" })?.value else { + return + } + + if let themeURL = URL(string: themeURLString) { + let request = URLRequest(url: themeURL) + let task = URLSession.shared.downloadTask(with: request) { location, response, error in + guard let location = location else { + return + } + + do { + try ArticleThemeDownloader.shared.handleFile(at: location) + } catch { + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) + } + } + task.resume() + } + return + + } + + // Special case URL with specific scheme handler x-netnewswire-feed: intended to ensure we open // it regardless of which news reader may be set as the default let nnwScheme = "x-netnewswire-feed:" diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index ba2096bab..7d1644d2c 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -11,6 +11,8 @@ 1701E1E725689D1E009453D8 /* Localized.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1701E1E625689D1E009453D8 /* Localized.swift */; }; 1704053424E5985A00A00787 /* SceneNavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1704053324E5985A00A00787 /* SceneNavigationModel.swift */; }; 1704053524E5985A00A00787 /* SceneNavigationModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1704053324E5985A00A00787 /* SceneNavigationModel.swift */; }; + 17071EF026F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17071EEF26F8137400F5E71D /* ArticleTheme+Notifications.swift */; }; + 17071EF126F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17071EEF26F8137400F5E71D /* ArticleTheme+Notifications.swift */; }; 1710B9132552354E00679C0D /* AddAccountHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1710B9122552354E00679C0D /* AddAccountHelpView.swift */; }; 1710B9142552354E00679C0D /* AddAccountHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1710B9122552354E00679C0D /* AddAccountHelpView.swift */; }; 1710B929255246F900679C0D /* EnableExtensionPointHelpView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1710B928255246F900679C0D /* EnableExtensionPointHelpView.swift */; }; @@ -112,6 +114,12 @@ 1799E6AA24C2F93F00511E91 /* InspectorPlatformModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1799E6A824C2F93F00511E91 /* InspectorPlatformModifier.swift */; }; 1799E6CD24C320D600511E91 /* InspectorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1799E6CC24C320D600511E91 /* InspectorModel.swift */; }; 1799E6CE24C320D600511E91 /* InspectorModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1799E6CC24C320D600511E91 /* InspectorModel.swift */; }; + 179C39EA26F76B0500D4E741 /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = 179C39E926F76B0500D4E741 /* Zip */; }; + 179C39EB26F76B3800D4E741 /* ArticleThemePlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */; }; + 179D280B26F6F93D003B2E0A /* Zip in Frameworks */ = {isa = PBXBuildFile; productRef = 179D280A26F6F93D003B2E0A /* Zip */; }; + 179D280D26F73D83003B2E0A /* ArticleThemePlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */; }; + 179D280E26F73D83003B2E0A /* ArticleThemePlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */; }; + 179D280F26F73D83003B2E0A /* ArticleThemePlist.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */; }; 179DB1DFBCF9177104B12E0F /* AccountsNewsBlurWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DBBA2B22A659F81EED6F9 /* AccountsNewsBlurWindowController.swift */; }; 179DB3CE822BFCC2D774D9F4 /* AccountsNewsBlurWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 179DBBA2B22A659F81EED6F9 /* AccountsNewsBlurWindowController.swift */; }; 17A1597C24E3DEDD005DA32A /* RSCore in Frameworks */ = {isa = PBXBuildFile; productRef = 17A1597B24E3DEDD005DA32A /* RSCore */; }; @@ -132,6 +140,8 @@ 17D5F17124B0BC6700375168 /* SidebarToolbarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */; }; 17D5F17224B0BC6700375168 /* SidebarToolbarModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */; }; 17D5F19524B0C1DD00375168 /* SidebarToolbarModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 172199F024AB716900A31D04 /* SidebarToolbarModifier.swift */; }; + 17D643B126F8A436008D4C05 /* ArticleThemeDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D643B026F8A436008D4C05 /* ArticleThemeDownloader.swift */; }; + 17D643B226F8A436008D4C05 /* ArticleThemeDownloader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17D643B026F8A436008D4C05 /* ArticleThemeDownloader.swift */; }; 17D7586F2679C21800B17787 /* OnePasswordExtension.m in Sources */ = {isa = PBXBuildFile; fileRef = 17D7586E2679C21800B17787 /* OnePasswordExtension.m */; }; 17E0084625941887000C23F0 /* SizeCategories.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E0084525941887000C23F0 /* SizeCategories.swift */; }; 17E4DBD624BFC53E00FE462A /* AdvancedPreferencesModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17E4DBD524BFC53E00FE462A /* AdvancedPreferencesModel.swift */; }; @@ -1533,6 +1543,7 @@ 1701E1B62568983D009453D8 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; 1701E1E625689D1E009453D8 /* Localized.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Localized.swift; sourceTree = ""; }; 1704053324E5985A00A00787 /* SceneNavigationModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneNavigationModel.swift; sourceTree = ""; }; + 17071EEF26F8137400F5E71D /* ArticleTheme+Notifications.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ArticleTheme+Notifications.swift"; sourceTree = ""; }; 1710B9122552354E00679C0D /* AddAccountHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountHelpView.swift; sourceTree = ""; }; 1710B928255246F900679C0D /* EnableExtensionPointHelpView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnableExtensionPointHelpView.swift; sourceTree = ""; }; 1717535524BADF33004498C6 /* GeneralPreferencesModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeneralPreferencesModel.swift; sourceTree = ""; }; @@ -1592,12 +1603,14 @@ 17930ED324AF10EE00A9BA52 /* AddWebFeedView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedView.swift; sourceTree = ""; }; 1799E6A824C2F93F00511E91 /* InspectorPlatformModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorPlatformModifier.swift; sourceTree = ""; }; 1799E6CC24C320D600511E91 /* InspectorModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InspectorModel.swift; sourceTree = ""; }; + 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleThemePlist.swift; sourceTree = ""; }; 179DBBA2B22A659F81EED6F9 /* AccountsNewsBlurWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AccountsNewsBlurWindowController.swift; sourceTree = ""; }; 17B223DB24AC24D2001E4592 /* TimelineLayoutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineLayoutView.swift; sourceTree = ""; }; 17D0682B2564F47E00C0B37E /* Localizable.stringsdict */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; path = Localizable.stringsdict; sourceTree = ""; }; 17D232A724AFF10A0005F075 /* AddWebFeedModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddWebFeedModel.swift; sourceTree = ""; }; 17D3CEE2257C4D2300E74939 /* AddAccountSignUp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AddAccountSignUp.swift; sourceTree = ""; }; 17D5F17024B0BC6700375168 /* SidebarToolbarModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidebarToolbarModel.swift; sourceTree = ""; }; + 17D643B026F8A436008D4C05 /* ArticleThemeDownloader.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ArticleThemeDownloader.swift; sourceTree = ""; }; 17D7586C2679C21700B17787 /* NetNewsWire-iOS-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "NetNewsWire-iOS-Bridging-Header.h"; sourceTree = ""; }; 17D7586D2679C21800B17787 /* OnePasswordExtension.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = OnePasswordExtension.h; sourceTree = ""; }; 17D7586E2679C21800B17787 /* OnePasswordExtension.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = OnePasswordExtension.m; sourceTree = ""; }; @@ -2289,6 +2302,7 @@ files = ( 5138E94924D3416D00AFF0FE /* RSCore in Frameworks */, 5138E95824D3419000AFF0FE /* RSWeb in Frameworks */, + 179D280B26F6F93D003B2E0A /* Zip in Frameworks */, 516B695F24D2F33B00B5702F /* Account in Frameworks */, 5138E95224D3418100AFF0FE /* RSParser in Frameworks */, 5138E94C24D3417A00AFF0FE /* RSDatabase in Frameworks */, @@ -2315,6 +2329,7 @@ 51C4CFF624D37DD500AF9874 /* Secrets in Frameworks */, 51A737AE24DB19730015FA66 /* RSCore in Frameworks */, 51A737C824DB19CC0015FA66 /* RSParser in Frameworks */, + 179C39EA26F76B0500D4E741 /* Zip in Frameworks */, 51E4DAED2425F6940091EB5B /* CloudKit.framework in Frameworks */, 514C16E124D2EF38009A3AFA /* RSCoreResources in Frameworks */, 514C16CE24D2E63F009A3AFA /* Account in Frameworks */, @@ -3449,6 +3464,9 @@ children = ( 849A97871ED9ECEF007D329B /* ArticleTheme.swift */, 849A97881ED9ECEF007D329B /* ArticleThemesManager.swift */, + 17D643B026F8A436008D4C05 /* ArticleThemeDownloader.swift */, + 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */, + 17071EEF26F8137400F5E71D /* ArticleTheme+Notifications.swift */, ); name = "Article Styles"; path = Shared/ArticleStyles; @@ -4146,6 +4164,7 @@ 513F32732593EE6F0003048F /* ArticlesDatabase */, 513F32762593EE6F0003048F /* Secrets */, 513F32792593EE6F0003048F /* SyncDatabase */, + 179D280A26F6F93D003B2E0A /* Zip */, ); productName = "NetNewsWire-iOS"; productReference = 840D617C2029031C009BC708 /* NetNewsWire.app */; @@ -4187,6 +4206,7 @@ 5132775D2590FC640064F1E7 /* Articles */, 513277602590FC640064F1E7 /* ArticlesDatabase */, 513277632590FC640064F1E7 /* SyncDatabase */, + 179C39E926F76B0500D4E741 /* Zip */, ); productName = NetNewsWire; productReference = 849C64601ED37A5D003D8FC0 /* NetNewsWire.app */; @@ -4318,6 +4338,7 @@ 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */, 17192AD82567B3D500AAEACA /* XCRemoteSwiftPackageReference "Sparkle-Binary" */, 519CA8E325841DB700EB079A /* XCRemoteSwiftPackageReference "plcrashreporter" */, + 179D280926F6F93D003B2E0A /* XCRemoteSwiftPackageReference "Zip" */, ); productRefGroup = 849C64611ED37A5D003D8FC0 /* Products */; projectDirPath = ""; @@ -5061,6 +5082,7 @@ 51E4991924A8090A00B667CB /* CacheCleaner.swift in Sources */, 51E498F724A8085D00B667CB /* SearchTimelineFeedDelegate.swift in Sources */, 175942AA24AD533200585066 /* RefreshInterval.swift in Sources */, + 179D280E26F73D83003B2E0A /* ArticleThemePlist.swift in Sources */, 51E4993524A867E800B667CB /* AppNotifications.swift in Sources */, 6535ECFC2680F9FF00C01CB5 /* IconImageCache.swift in Sources */, 51C0515E24A77DF800194D5E /* MainApp.swift in Sources */, @@ -5172,6 +5194,7 @@ 51E499D924A912C200B667CB /* SceneModel.swift in Sources */, 51919FB424AAB97900541E64 /* FeedIconImageLoader.swift in Sources */, 1769E32524BC5A65000E1E8E /* AddAccountView.swift in Sources */, + 179D280F26F73D83003B2E0A /* ArticleThemePlist.swift in Sources */, 51E4994A24A8734C00B667CB /* ExtensionPointManager.swift in Sources */, 514E6C0324AD29A300AC6F6E /* TimelineItemStatusView.swift in Sources */, 1724126A257BBEBB00ACCEBC /* AddFeedbinViewModel.swift in Sources */, @@ -5477,6 +5500,7 @@ 5186A635235EF3A800C97195 /* VibrantLabel.swift in Sources */, 51F85BF92274AA7B00C787DC /* UIBarButtonItem-Extensions.swift in Sources */, 51B62E68233186730085F949 /* IconView.swift in Sources */, + 179D280D26F73D83003B2E0A /* ArticleThemePlist.swift in Sources */, 51C45296226509D300C03939 /* OPMLExporter.swift in Sources */, 51C45291226509C800C03939 /* SmartFeed.swift in Sources */, 51C452A722650A3D00C03939 /* RSImage-Extensions.swift in Sources */, @@ -5569,6 +5593,7 @@ 512392BE24E33A3C00F11704 /* RedditSelectAccountTableViewController.swift in Sources */, 515A517B243E90260089E588 /* ExtensionPoint.swift in Sources */, 51C4529C22650A1000C03939 /* SingleFaviconDownloader.swift in Sources */, + 17D643B226F8A436008D4C05 /* ArticleThemeDownloader.swift in Sources */, 51E595A6228CC36500FCC42B /* ArticleStatusSyncTimer.swift in Sources */, 51F9F3F723DF6DB200A314FD /* ArticleIconSchemeHandler.swift in Sources */, 512392C524E3451400F11704 /* TwitterEnterDetailTableViewController.swift in Sources */, @@ -5609,6 +5634,7 @@ C5A6ED5223C9AF4300AB6BE2 /* TitleActivityItemSource.swift in Sources */, 17D7586F2679C21800B17787 /* OnePasswordExtension.m in Sources */, 51DC37092402F1470095D371 /* MasterFeedDataSourceOperation.swift in Sources */, + 17071EF126F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */, 51C4529B22650A1000C03939 /* FaviconDownloader.swift in Sources */, 84DEE56622C32CA4005FC42C /* SmartFeedDelegate.swift in Sources */, 512E09012268907400BDCFDD /* MasterFeedTableViewSectionHeader.swift in Sources */, @@ -5663,6 +5689,7 @@ 844B5B5B1FEA00FB00C7C76A /* TimelineKeyboardDelegate.swift in Sources */, 842E45DD1ED8C54B000A8B52 /* Browser.swift in Sources */, 84216D0322128B9D0049B9B9 /* DetailWebViewController.swift in Sources */, + 17071EF026F8137400F5E71D /* ArticleTheme+Notifications.swift in Sources */, 8444C8F21FED81840051386C /* OPMLExporter.swift in Sources */, 849A975E1ED9EB72007D329B /* MainWindowController.swift in Sources */, 84F2D53A1FC2308B00998D64 /* UnreadFeed.swift in Sources */, @@ -5699,6 +5726,7 @@ 8426118A1FCB67AA0086A189 /* WebFeedIconDownloader.swift in Sources */, 84C9FC7B22629E1200D921D6 /* PreferencesControlsBackgroundView.swift in Sources */, 84162A152038C12C00035290 /* MarkCommandValidationStatus.swift in Sources */, + 17D643B126F8A436008D4C05 /* ArticleThemeDownloader.swift in Sources */, 84E95D241FB1087500552D99 /* ArticlePasteboardWriter.swift in Sources */, 849A975B1ED9EB0D007D329B /* ArticleUtilities.swift in Sources */, 849ADEE8235981A0000E1B81 /* NNW3OpenPanelAccessoryViewController.swift in Sources */, @@ -5807,6 +5835,7 @@ 510C418124E5D1AE008226FD /* ExtensionContainers.swift in Sources */, 51EC114C2149FE3300B296E3 /* FolderTreeMenu.swift in Sources */, 849ADEE42359817E000E1B81 /* NNW3ImportController.swift in Sources */, + 179C39EB26F76B3800D4E741 /* ArticleThemePlist.swift in Sources */, 849A97A31ED9F180007D329B /* FolderTreeControllerDelegate.swift in Sources */, 51126DA4225FDE2F00722696 /* RSImage-Extensions.swift in Sources */, 510C43F7243D035C009F70C3 /* ExtensionPoint.swift in Sources */, @@ -6403,6 +6432,14 @@ minimumVersion = 2.0.0; }; }; + 179D280926F6F93D003B2E0A /* XCRemoteSwiftPackageReference "Zip" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/marmelroy/Zip.git"; + requirement = { + kind = revision; + revision = 059e7346082d02de16220cd79df7db18ddeba8c3; + }; + }; 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Ranchero-Software/RSCore.git"; @@ -6502,6 +6539,16 @@ package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */; productName = RSCoreResources; }; + 179C39E926F76B0500D4E741 /* Zip */ = { + isa = XCSwiftPackageProductDependency; + package = 179D280926F6F93D003B2E0A /* XCRemoteSwiftPackageReference "Zip" */; + productName = Zip; + }; + 179D280A26F6F93D003B2E0A /* Zip */ = { + isa = XCSwiftPackageProductDependency; + package = 179D280926F6F93D003B2E0A /* XCRemoteSwiftPackageReference "Zip" */; + productName = Zip; + }; 17A1597B24E3DEDD005DA32A /* RSCore */ = { isa = XCSwiftPackageProductDependency; package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */; diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index 612c7e32c..dc8075713 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -117,6 +117,15 @@ "revision": "9483a5d459b45c3ffd059f7b55f9638e268632fd", "version": "1.5.0" } + }, + { + "package": "Zip", + "repositoryURL": "https://github.com/marmelroy/Zip.git", + "state": { + "branch": null, + "revision": "059e7346082d02de16220cd79df7db18ddeba8c3", + "version": null + } } ] }, diff --git a/Shared/ArticleStyles/ArticleTheme+Notifications.swift b/Shared/ArticleStyles/ArticleTheme+Notifications.swift new file mode 100644 index 000000000..4c2391ff4 --- /dev/null +++ b/Shared/ArticleStyles/ArticleTheme+Notifications.swift @@ -0,0 +1,15 @@ +// +// ArticleTheme+Notifications.swift +// ArticleTheme+Notifications +// +// Created by Stuart Breckenridge on 20/09/2021. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +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/ArticleTheme.swift b/Shared/ArticleStyles/ArticleTheme.swift index a32e37af2..dc03e816f 100644 --- a/Shared/ArticleStyles/ArticleTheme.swift +++ b/Shared/ArticleStyles/ArticleTheme.swift @@ -9,13 +9,13 @@ import Foundation struct ArticleTheme: Equatable { - + static let defaultTheme = ArticleTheme() static let nnwThemeSuffix = ".nnwtheme" - + private static let defaultThemeName = NSLocalizedString("Default", comment: "Default") private static let unknownValue = NSLocalizedString("Unknown", comment: "Unknown Value") - + let path: String? let template: String? let css: String? @@ -26,37 +26,39 @@ struct ArticleTheme: Equatable { } var creatorHomePage: String { - return info?["CreatorHomePage"] as? String ?? Self.unknownValue + return info?.creatorHomePage ?? Self.unknownValue } var creatorName: String { - return info?["CreatorName"] as? String ?? Self.unknownValue + return info?.creatorName ?? Self.unknownValue } var version: String { - return info?["Version"] as? String ?? "0.0" + return String(describing: info?.version ?? 0) } - private let info: NSDictionary? - + private let info: ArticleThemePlist? + init() { self.path = nil; - self.info = ["CreatorHomePage": "https://netnewswire.com/", "CreatorName": "Ranchero Software", "Version": "1.0"] - + self.info = ArticleThemePlist(name: "Article Theme", themeIdentifier: "com.ranchero.netnewswire.theme.article", creatorHomePage: "https://netnewswire.com/", creatorName: "Ranchero Software", version: 1) + let corePath = Bundle.main.path(forResource: "core", ofType: "css")! let stylesheetPath = Bundle.main.path(forResource: "stylesheet", ofType: "css")! css = Self.stringAtPath(corePath)! + "\n" + Self.stringAtPath(stylesheetPath)! - + let templatePath = Bundle.main.path(forResource: "template", ofType: "html")! template = Self.stringAtPath(templatePath)! } - - init(path: String) { + + init(path: String) throws { self.path = path - + let infoPath = (path as NSString).appendingPathComponent("Info.plist") - self.info = NSDictionary(contentsOfFile: infoPath) - + 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) { @@ -64,7 +66,7 @@ struct ArticleTheme: Equatable { } else { self.css = nil } - + let templatePath = (path as NSString).appendingPathComponent("template.html") self.template = Self.stringAtPath(templatePath) } @@ -73,22 +75,22 @@ struct ArticleTheme: Equatable { if !FileManager.default.fileExists(atPath: f) { return nil } - + if let s = try? NSString(contentsOfFile: f, usedEncoding: nil) as String { return s } return nil } - + static func filenameWithThemeSuffixRemoved(_ filename: String) -> String { return filename.stripping(suffix: Self.nnwThemeSuffix) } - + static func themeNameForPath(_ f: String) -> String { let filename = (f as NSString).lastPathComponent return filenameWithThemeSuffixRemoved(filename) } - + static func pathIsPathForThemeName(_ themeName: String, path: String) -> Bool { let filename = (path as NSString).lastPathComponent return filenameWithThemeSuffixRemoved(filename) == themeName diff --git a/Shared/ArticleStyles/ArticleThemeDownloader.swift b/Shared/ArticleStyles/ArticleThemeDownloader.swift new file mode 100644 index 000000000..1dd775ee1 --- /dev/null +++ b/Shared/ArticleStyles/ArticleThemeDownloader.swift @@ -0,0 +1,93 @@ +// +// ArticleThemeDownloader.swift +// ArticleThemeDownloader +// +// Created by Stuart Breckenridge on 20/09/2021. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import Foundation +import Zip + +public class ArticleThemeDownloader { + + public enum ArticleThemeDownloaderError: LocalizedError { + case noThemeFile + + public var errorDescription: String? { + switch self { + case .noThemeFile: + return "There is no NetNewsWire theme available." + } + } + } + + + public static let shared = ArticleThemeDownloader() + private init() {} + + public func handleFile(at location: URL) throws { + createDownloadDirectoryIfRequired() + let movedFileLocation = try moveTheme(from: location) + let unzippedFileLocation = try unzipFile(at: movedFileLocation) + NotificationCenter.default.post(name: .didEndDownloadingTheme, object: nil, userInfo: ["url" : unzippedFileLocation]) + } + + + /// Creates `Application Support/NetNewsWire/Downloads` if needed. + private func createDownloadDirectoryIfRequired() { + try? FileManager.default.createDirectory(at: downloadDirectory(), withIntermediateDirectories: true, attributes: nil) + } + + /// Moves the downloaded `.tmp` file to the `downloadDirectory` and renames it a `.zip` + /// - Parameter location: The temporary file location. + /// - Returns: Destination `URL`. + private func moveTheme(from location: URL) throws -> URL { + var tmpFileName = location.lastPathComponent + tmpFileName = tmpFileName.replacingOccurrences(of: ".tmp", with: ".zip") + let fileUrl = downloadDirectory().appendingPathComponent("\(tmpFileName)") + try FileManager.default.moveItem(at: location, to: fileUrl) + return fileUrl + } + + /// Unzips the zip file + /// - Parameter location: Location of the zip archive. + /// - Returns: Enclosed `.nnwtheme` file. + private func unzipFile(at location: URL) throws -> URL { + do { + let unzipDirectory = URL(fileURLWithPath: location.path.replacingOccurrences(of: ".zip", with: "")) + try Zip.unzipFile(location, destination: unzipDirectory, overwrite: true, password: nil, progress: nil, fileOutputHandler: nil) // Unzips to folder in Application Support/NetNewsWire/Downloads + try FileManager.default.removeItem(at: location) // Delete zip in Cache + let themeFilePath = FileManager.default.filenames(inFolder: unzipDirectory.path)?.first(where: { $0.contains(".nnwtheme") }) + if themeFilePath == nil { + throw ArticleThemeDownloaderError.noThemeFile + } + return URL(fileURLWithPath: unzipDirectory.appendingPathComponent(themeFilePath!).path) + } catch { + try? FileManager.default.removeItem(at: location) + throw error + } + } + + /// The download directory used by the theme downloader: `Application Suppport/NetNewsWire/Downloads` + /// - Returns: `URL` + private func downloadDirectory() -> URL { + FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first!.appendingPathComponent("NetNewsWire/Downloads", isDirectory: true) + } + + /// Removes downloaded themes, where themes == folders, from `Application Suppport/NetNewsWire/Downloads`. + public func cleanUp() { + guard let filenames = try? FileManager.default.contentsOfDirectory(atPath: downloadDirectory().path) else { + return + } + for path in filenames { + do { + if FileManager.default.isFolder(atPath: downloadDirectory().appendingPathComponent(path).path) { + try FileManager.default.removeItem(atPath: downloadDirectory().appendingPathComponent(path).path) + } + } catch { + print(error) + } + } + } +} diff --git a/Shared/ArticleStyles/ArticleThemePlist.swift b/Shared/ArticleStyles/ArticleThemePlist.swift new file mode 100644 index 000000000..f324cdbde --- /dev/null +++ b/Shared/ArticleStyles/ArticleThemePlist.swift @@ -0,0 +1,25 @@ +// +// ArticleThemePlist.swift +// NetNewsWire +// +// Created by Stuart Breckenridge on 19/09/2021. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import Foundation + +public struct ArticleThemePlist: Codable, Equatable { + public var name: String + public var themeIdentifier: String + public var creatorHomePage: String + public var creatorName: String + public var version: Int + + enum CodingKeys: String, CodingKey { + case name = "Name" + case themeIdentifier = "ThemeIdentifier" + case creatorHomePage = "CreatorHomePage" + case creatorName = "CreatorName" + case version = "Version" + } +} diff --git a/Shared/ArticleStyles/ArticleThemesManager.swift b/Shared/ArticleStyles/ArticleThemesManager.swift index e49fe5d9f..6e2b6078c 100644 --- a/Shared/ArticleStyles/ArticleThemesManager.swift +++ b/Shared/ArticleStyles/ArticleThemesManager.swift @@ -133,8 +133,13 @@ private extension ArticleThemesManager { guard let path = pathForThemeName(themeName, folder: folderPath) else { return nil } - - return ArticleTheme(path: path) + do { + return try ArticleTheme(path: path) + } catch { + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) + return nil + } + } func defaultArticleTheme() -> ArticleTheme { diff --git a/Technotes/Themes.md b/Technotes/Themes.md new file mode 100644 index 000000000..741e5e963 --- /dev/null +++ b/Technotes/Themes.md @@ -0,0 +1,11 @@ +# Themes + +## Add Themes Directly to NetNewsWire +Theme developers: on iOS and macOS, themes can be opened directly in NetNewsWire using the below URL scheme: + +`netnewswire://theme/add?url={url}` + +When using this URL scheme the theme being shared must be zipped. + +Parameters: +- `url`: (mandatory, URL-encoded): The theme's location. diff --git a/iOS/AppDelegate.swift b/iOS/AppDelegate.swift index d93eaa6ed..807d1a340 100644 --- a/iOS/AppDelegate.swift +++ b/iOS/AppDelegate.swift @@ -338,6 +338,7 @@ private extension AppDelegate { AccountManager.shared.suspendNetworkAll() AccountManager.shared.suspendDatabaseAll() + ArticleThemeDownloader.shared.cleanUp() CoalescingQueue.standard.performCallsImmediately() for scene in UIApplication.shared.connectedScenes { diff --git a/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index 16d63ee5c..f3a2c1abc 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -323,7 +323,8 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange(_:)), name: UserDefaults.didChangeNotification, object: nil) 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 start(for size: CGSize) -> UIViewController { @@ -587,6 +588,27 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { queueFetchAndMergeArticles() } } + + @objc func importDownloadedTheme(_ note: Notification) { + guard let userInfo = note.userInfo, + let url = userInfo["url"] as? URL else { + return + } + + DispatchQueue.main.async { + self.importTheme(filename: url.path) + } + } + + @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 @@ -1295,7 +1317,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func importTheme(filename: String) { - ArticleThemeImporter.importTheme(controller: rootSplitViewController, filename: filename); + do { + try ArticleThemeImporter.importTheme(controller: rootSplitViewController, filename: filename) + } catch { + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error" : error]) + } + } } diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 7ae1674e3..6812578ac 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -9,15 +9,16 @@ import UIKit import UserNotifications import Account +import Zip class SceneDelegate: UIResponder, UIWindowSceneDelegate { - var window: UIWindow? + var window: UIWindow? var coordinator = SceneCoordinator() - // UIWindowScene delegate - - func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { + // UIWindowScene delegate + + func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { window = UIWindow(windowScene: scene as! UIWindowScene) window!.tintColor = AppAssets.primaryAccentColor @@ -46,12 +47,12 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { return } - if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { + if let userActivity = connectionOptions.userActivities.first ?? session.stateRestorationActivity { coordinator.handle(userActivity) } window!.makeKeyAndVisible() - } + } func windowScene(_ windowScene: UIWindowScene, performActionFor shortcutItem: UIApplicationShortcutItem, completionHandler: @escaping (Bool) -> Void) { appDelegate.resumeDatabaseProcessingIfNecessary() @@ -79,9 +80,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { coordinator.resetFocus() } - func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { + func stateRestorationActivity(for scene: UIScene) -> NSUserActivity? { return coordinator.stateRestorationActivity - } + } // API @@ -89,7 +90,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { appDelegate.resumeDatabaseProcessingIfNecessary() coordinator.handle(response) } - + func suspend() { coordinator.suspend() } @@ -165,11 +166,44 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let filename = context.url.standardizedFileURL.path if filename.hasSuffix(ArticleTheme.nnwThemeSuffix) { self.coordinator.importTheme(filename: filename) + return + } + + // Handle theme URLs: netnewswire://theme/add?url={url} + guard let comps = URLComponents(url: context.url, resolvingAgainstBaseURL: false), + "theme" == comps.host, + let queryItems = comps.queryItems else { + return + } + + if let providedThemeURL = queryItems.first(where: { $0.name == "url" })?.value { + if let themeURL = URL(string: providedThemeURL) { + let request = URLRequest(url: themeURL) + + DispatchQueue.main.async { + 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 } + + do { + try ArticleThemeDownloader.shared.handleFile(at: location) + } catch { + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) + } + } + task.resume() + } else { + print("No theme URL") + return + } + } else { + return } } } - } private extension SceneDelegate { @@ -204,4 +238,6 @@ private extension SceneDelegate { } } + + } diff --git a/iOS/Settings/ArticleThemeImporter.swift b/iOS/Settings/ArticleThemeImporter.swift index 7c2950622..135d19989 100644 --- a/iOS/Settings/ArticleThemeImporter.swift +++ b/iOS/Settings/ArticleThemeImporter.swift @@ -10,8 +10,8 @@ import UIKit struct ArticleThemeImporter { - static func importTheme(controller: UIViewController, filename: String) { - let theme = ArticleTheme(path: filename) + static func importTheme(controller: UIViewController, filename: String) throws { + let theme = try 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 @@ -28,7 +28,7 @@ struct ArticleThemeImporter { let visitSiteTitle = NSLocalizedString("Show Website", comment: "Show Website") let visitSiteAction = UIAlertAction(title: visitSiteTitle, style: .default) { action in UIApplication.shared.open(url) - Self.importTheme(controller: controller, filename: filename) + try? Self.importTheme(controller: controller, filename: filename) } alertController.addAction(visitSiteAction) } diff --git a/iOS/Settings/ArticleThemesTableViewController.swift b/iOS/Settings/ArticleThemesTableViewController.swift index 1ae2ef2a3..26e28944f 100644 --- a/iOS/Settings/ArticleThemesTableViewController.swift +++ b/iOS/Settings/ArticleThemesTableViewController.swift @@ -112,7 +112,11 @@ extension ArticleThemesTableViewController: UIDocumentPickerDelegate { func documentPicker(_ controller: UIDocumentPickerViewController, didPickDocumentsAt urls: [URL]) { guard let url = urls.first else { return } - ArticleThemeImporter.importTheme(controller: self, filename: url.standardizedFileURL.path) + do { + try ArticleThemeImporter.importTheme(controller: self, filename: url.standardizedFileURL.path) + } catch { + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) + } } } diff --git a/iOS/UIKit Extensions/UIViewController-Extensions.swift b/iOS/UIKit Extensions/UIViewController-Extensions.swift index a21bc05eb..9d06d7609 100644 --- a/iOS/UIKit Extensions/UIViewController-Extensions.swift +++ b/iOS/UIKit Extensions/UIViewController-Extensions.swift @@ -15,6 +15,23 @@ extension UIViewController { func presentError(_ error: Error, dismiss: (() -> Void)? = nil) { if let accountError = error as? AccountError, accountError.isCredentialsError { presentAccountError(accountError, dismiss: dismiss) + } else if let decodingError = error as? DecodingError { + let errorTitle = NSLocalizedString("Error", comment: "Error") + switch decodingError { + case .typeMismatch(let type, _): + let str = "Type '\(type)' mismatch." + presentError(title: errorTitle, message: str, dismiss: dismiss) + case .valueNotFound(let value, _): + let str = "Value '\(value)' not found." + presentError(title: errorTitle, message: str, 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) + default: + presentError(title: errorTitle, message: error.localizedDescription, dismiss: dismiss) + } } else { let errorTitle = NSLocalizedString("Error", comment: "Error") presentError(title: errorTitle, message: error.localizedDescription, dismiss: dismiss)