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)