From 4fab4ffa7c7b2aa7a60564c1529b81fa1d9a2dd1 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Sun, 19 Sep 2021 17:48:29 +0800 Subject: [PATCH 01/11] download themes using url scheme this build enables iOS functionality only. --- NetNewsWire.xcodeproj/project.pbxproj | 25 ++++++ .../xcshareddata/swiftpm/Package.resolved | 9 ++ Shared/ArticleStyles/ArticleThemePlist.swift | 25 ++++++ iOS/SceneDelegate.swift | 82 ++++++++++++++++--- 4 files changed, 131 insertions(+), 10 deletions(-) create mode 100644 Shared/ArticleStyles/ArticleThemePlist.swift diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 5a51bdb4a..eb9d1aa9b 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -112,6 +112,10 @@ 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 */; }; + 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 */; }; @@ -1591,6 +1595,7 @@ 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 = ""; }; @@ -2287,6 +2292,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 */, @@ -3446,6 +3452,7 @@ children = ( 849A97871ED9ECEF007D329B /* ArticleTheme.swift */, 849A97881ED9ECEF007D329B /* ArticleThemesManager.swift */, + 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */, ); name = "Article Styles"; path = Shared/ArticleStyles; @@ -4143,6 +4150,7 @@ 513F32732593EE6F0003048F /* ArticlesDatabase */, 513F32762593EE6F0003048F /* Secrets */, 513F32792593EE6F0003048F /* SyncDatabase */, + 179D280A26F6F93D003B2E0A /* Zip */, ); productName = "NetNewsWire-iOS"; productReference = 840D617C2029031C009BC708 /* NetNewsWire.app */; @@ -4315,6 +4323,7 @@ 51B0DF2324D2C7FA000AD99E /* XCRemoteSwiftPackageReference "RSParser" */, 17192AD82567B3D500AAEACA /* XCRemoteSwiftPackageReference "Sparkle-Binary" */, 519CA8E325841DB700EB079A /* XCRemoteSwiftPackageReference "plcrashreporter" */, + 179D280926F6F93D003B2E0A /* XCRemoteSwiftPackageReference "Zip" */, ); productRefGroup = 849C64611ED37A5D003D8FC0 /* Products */; projectDirPath = ""; @@ -5058,6 +5067,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 */, @@ -5169,6 +5179,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 */, @@ -5474,6 +5485,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 */, @@ -6399,6 +6411,14 @@ minimumVersion = 2.0.0; }; }; + 179D280926F6F93D003B2E0A /* XCRemoteSwiftPackageReference "Zip" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/marmelroy/Zip.git"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Ranchero-Software/RSCore.git"; @@ -6498,6 +6518,11 @@ package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */; productName = RSCoreResources; }; + 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..fc2737cdd 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": "bd19d974e8a38cc8d3a88c90c8a107386c3b8ccf", + "version": "2.1.1" + } } ] }, diff --git a/Shared/ArticleStyles/ArticleThemePlist.swift b/Shared/ArticleStyles/ArticleThemePlist.swift new file mode 100644 index 000000000..9525afa94 --- /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 { + 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 = "ThemeIdentifer" // FIXME: Spelling error! + case creatorHomePage = "CreatorHomePage" + case creatorName = "CreatorName" + case version = "Version" + } +} diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 7ae1674e3..3b850f22a 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 { +class SceneDelegate: UIResponder, UIWindowSceneDelegate, URLSessionDownloadDelegate { - 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,8 +166,69 @@ 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) + let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) + let downloadTask = session.downloadTask(with: request) + downloadTask.resume() + } else { + print("No theme URL") + return + } + } else { + return + } + + } + } + + // MARK: - URLSessionDownloadDelegate + + func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { + var downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! + try? FileManager.default.createDirectory(at: downloadDirectory, withIntermediateDirectories: true, attributes: nil) + let tmpFileName = UUID().uuidString + ".zip" + downloadDirectory.appendPathComponent("\(tmpFileName)") + + do { + try FileManager.default.moveItem(at: location, to: downloadDirectory) + + var unzippedDir = downloadDirectory + unzippedDir = unzippedDir.deletingLastPathComponent() + unzippedDir.appendPathComponent("newtheme.nnwtheme") + + try Zip.unzipFile(downloadDirectory, destination: unzippedDir, overwrite: true, password: nil, progress: nil, fileOutputHandler: nil) + try FileManager.default.removeItem(at: downloadDirectory) + + let decoder = PropertyListDecoder() + let plistURL = URL(fileURLWithPath: unzippedDir.appendingPathComponent("Info.plist").path) + + let data = try Data(contentsOf: plistURL) + let plist = try decoder.decode(ArticleThemePlist.self, from: data) + + // rename + var renamedUnzippedDir = unzippedDir.deletingLastPathComponent() + renamedUnzippedDir.appendPathComponent(plist.name + ".nnwtheme") + if FileManager.default.fileExists(atPath: renamedUnzippedDir.path) { + try FileManager.default.removeItem(at: renamedUnzippedDir) + } + try FileManager.default.moveItem(at: unzippedDir, to: renamedUnzippedDir) + DispatchQueue.main.async { + self.coordinator.importTheme(filename: renamedUnzippedDir.path) + } + } catch { + print(error) } } From 7994b34551c85e1345889d81d0b1a570f93dfc00 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Sun, 19 Sep 2021 21:18:23 +0800 Subject: [PATCH 02/11] adds mac theme downloading --- Mac/AppDelegate.swift | 2 +- Mac/Resources/Info.plist | 1 + .../AppDelegate+Scriptability.swift | 58 +++++++++++++++++++ NetNewsWire.xcodeproj/project.pbxproj | 10 ++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index d56800522..afdbbfd56 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -765,7 +765,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. 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..81eba91c2 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,63 @@ 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 + var downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! + try? FileManager.default.createDirectory(at: downloadDirectory, withIntermediateDirectories: true, attributes: nil) + let tmpFileName = UUID().uuidString + ".zip" + downloadDirectory.appendPathComponent("\(tmpFileName)") + + if location == nil { + return + } + + do { + try FileManager.default.moveItem(at: location!, to: downloadDirectory) + + var unzippedDir = downloadDirectory + unzippedDir = unzippedDir.deletingLastPathComponent() + unzippedDir.appendPathComponent("newtheme.nnwtheme") + + try Zip.unzipFile(downloadDirectory, destination: unzippedDir, overwrite: true, password: nil, progress: nil, fileOutputHandler: nil) + try FileManager.default.removeItem(at: downloadDirectory) + + let decoder = PropertyListDecoder() + let plistURL = URL(fileURLWithPath: unzippedDir.appendingPathComponent("Info.plist").path) + + let data = try Data(contentsOf: plistURL) + let plist = try decoder.decode(ArticleThemePlist.self, from: data) + + // rename + var renamedUnzippedDir = unzippedDir.deletingLastPathComponent() + renamedUnzippedDir.appendPathComponent(plist.name + ".nnwtheme") + if FileManager.default.fileExists(atPath: renamedUnzippedDir.path) { + try FileManager.default.removeItem(at: renamedUnzippedDir) + } + try FileManager.default.moveItem(at: unzippedDir, to: renamedUnzippedDir) + DispatchQueue.main.async { + self.importTheme(filename: renamedUnzippedDir.path) + } + } catch { + print(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 eb9d1aa9b..1e1787b22 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -112,6 +112,8 @@ 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 */; }; @@ -2319,6 +2321,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 */, @@ -4192,6 +4195,7 @@ 5132775D2590FC640064F1E7 /* Articles */, 513277602590FC640064F1E7 /* ArticlesDatabase */, 513277632590FC640064F1E7 /* SyncDatabase */, + 179C39E926F76B0500D4E741 /* Zip */, ); productName = NetNewsWire; productReference = 849C64601ED37A5D003D8FC0 /* NetNewsWire.app */; @@ -5815,6 +5819,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 */, @@ -6518,6 +6523,11 @@ package = 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */; productName = RSCoreResources; }; + 179C39E926F76B0500D4E741 /* Zip */ = { + isa = XCSwiftPackageProductDependency; + package = 179D280926F6F93D003B2E0A /* XCRemoteSwiftPackageReference "Zip" */; + productName = Zip; + }; 179D280A26F6F93D003B2E0A /* Zip */ = { isa = XCSwiftPackageProductDependency; package = 179D280926F6F93D003B2E0A /* XCRemoteSwiftPackageReference "Zip" */; From 0166d47a1e8b344fabeceb8866e35c3f302b595a Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Mon, 20 Sep 2021 06:22:34 +0800 Subject: [PATCH 03/11] Add URL Scheme documentation --- Technotes/Themes.md | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 Technotes/Themes.md 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. From afd952fbc2212047e71b99c57d79488cf7319466 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Mon, 20 Sep 2021 09:36:09 +0800 Subject: [PATCH 04/11] refactors iOS theme downloads --- .../AppDelegate+Scriptability.swift | 1 - NetNewsWire.xcodeproj/project.pbxproj | 6 + .../ArticleTheme+Notifications.swift | 15 +++ iOS/SceneDelegate.swift | 104 +++++++++++------- 4 files changed, 85 insertions(+), 41 deletions(-) create mode 100644 Shared/ArticleStyles/ArticleTheme+Notifications.swift diff --git a/Mac/Scriptability/AppDelegate+Scriptability.swift b/Mac/Scriptability/AppDelegate+Scriptability.swift index 81eba91c2..b8ad7ee3d 100644 --- a/Mac/Scriptability/AppDelegate+Scriptability.swift +++ b/Mac/Scriptability/AppDelegate+Scriptability.swift @@ -60,7 +60,6 @@ extension AppDelegate : AppDelegateAppleEvents { try? FileManager.default.createDirectory(at: downloadDirectory, withIntermediateDirectories: true, attributes: nil) let tmpFileName = UUID().uuidString + ".zip" downloadDirectory.appendPathComponent("\(tmpFileName)") - if location == nil { return } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 3ba5cf4f7..80ff1568c 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 */; }; @@ -1539,6 +1541,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 = ""; }; @@ -3459,6 +3462,7 @@ 849A97871ED9ECEF007D329B /* ArticleTheme.swift */, 849A97881ED9ECEF007D329B /* ArticleThemesManager.swift */, 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */, + 17071EEF26F8137400F5E71D /* ArticleTheme+Notifications.swift */, ); name = "Article Styles"; path = Shared/ArticleStyles; @@ -5625,6 +5629,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 */, @@ -5679,6 +5684,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 */, diff --git a/Shared/ArticleStyles/ArticleTheme+Notifications.swift b/Shared/ArticleStyles/ArticleTheme+Notifications.swift new file mode 100644 index 000000000..08337aab4 --- /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 didEndDownloadingThemeWithError = Notification.Name("didEndDownloadingThemeWithError") +} diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 3b850f22a..6658c3e55 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -11,7 +11,7 @@ import UserNotifications import Account import Zip -class SceneDelegate: UIResponder, UIWindowSceneDelegate, URLSessionDownloadDelegate { +class SceneDelegate: UIResponder, UIWindowSceneDelegate { var window: UIWindow? var coordinator = SceneCoordinator() @@ -179,9 +179,29 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, URLSessionDownloadDeleg if let providedThemeURL = queryItems.first(where: { $0.name == "url" })?.value { if let themeURL = URL(string: providedThemeURL) { let request = URLRequest(url: themeURL) - let session = URLSession(configuration: .default, delegate: self, delegateQueue: nil) - let downloadTask = session.downloadTask(with: request) - downloadTask.resume() + + 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 self = self, let location = location else { return } + self.createDownloadDirectoryIfRequired() + do { + let movedFileLocation = try self.moveTheme(from: location) + let unzippedFileLocation = try self.unzipFile(at: movedFileLocation) + let renamedFileLocation = try self.renameFileToThemeName(at: unzippedFileLocation) + DispatchQueue.main.async { + NotificationCenter.default.post(name: .didEndDownloadingTheme, object: nil) + self.coordinator.importTheme(filename: renamedFileLocation.path) + } + } catch { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .didEndDownloadingThemeWithError, object: nil, userInfo: ["error" : error]) + self.showAlert(error) + } + } + } + task.resume() } else { print("No theme URL") return @@ -193,45 +213,49 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, URLSessionDownloadDeleg } } - // MARK: - URLSessionDownloadDelegate - - func urlSession(_ session: URLSession, downloadTask: URLSessionDownloadTask, didFinishDownloadingTo location: URL) { - var downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! + // MARK: - Theme Downloader + private func createDownloadDirectoryIfRequired() { + let downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! try? FileManager.default.createDirectory(at: downloadDirectory, withIntermediateDirectories: true, attributes: nil) - let tmpFileName = UUID().uuidString + ".zip" - downloadDirectory.appendPathComponent("\(tmpFileName)") - - do { - try FileManager.default.moveItem(at: location, to: downloadDirectory) - - var unzippedDir = downloadDirectory - unzippedDir = unzippedDir.deletingLastPathComponent() - unzippedDir.appendPathComponent("newtheme.nnwtheme") - - try Zip.unzipFile(downloadDirectory, destination: unzippedDir, overwrite: true, password: nil, progress: nil, fileOutputHandler: nil) - try FileManager.default.removeItem(at: downloadDirectory) - - let decoder = PropertyListDecoder() - let plistURL = URL(fileURLWithPath: unzippedDir.appendingPathComponent("Info.plist").path) - - let data = try Data(contentsOf: plistURL) - let plist = try decoder.decode(ArticleThemePlist.self, from: data) - - // rename - var renamedUnzippedDir = unzippedDir.deletingLastPathComponent() - renamedUnzippedDir.appendPathComponent(plist.name + ".nnwtheme") - if FileManager.default.fileExists(atPath: renamedUnzippedDir.path) { - try FileManager.default.removeItem(at: renamedUnzippedDir) - } - try FileManager.default.moveItem(at: unzippedDir, to: renamedUnzippedDir) - DispatchQueue.main.async { - self.coordinator.importTheme(filename: renamedUnzippedDir.path) - } - } catch { - print(error) - } } + private func moveTheme(from location: URL) throws -> URL { + var downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! + let tmpFileName = UUID().uuidString + ".zip" + downloadDirectory.appendPathComponent("\(tmpFileName)") + try FileManager.default.moveItem(at: location, to: downloadDirectory) + return downloadDirectory + } + + private func unzipFile(at location: URL) throws -> URL { + var unzippedDir = location.deletingLastPathComponent() + unzippedDir.appendPathComponent("newtheme.nnwtheme") + try Zip.unzipFile(location, destination: unzippedDir, overwrite: true, password: nil, progress: nil, fileOutputHandler: nil) + try FileManager.default.removeItem(at: location) + return unzippedDir + } + + private func renameFileToThemeName(at location: URL) throws -> URL { + let decoder = PropertyListDecoder() + let plistURL = URL(fileURLWithPath: location.appendingPathComponent("Info.plist").path) + let data = try Data(contentsOf: plistURL) + let plist = try decoder.decode(ArticleThemePlist.self, from: data) + var renamedUnzippedDir = location.deletingLastPathComponent() + renamedUnzippedDir.appendPathComponent(plist.name + ".nnwtheme") + if FileManager.default.fileExists(atPath: renamedUnzippedDir.path) { + try FileManager.default.removeItem(at: renamedUnzippedDir) + } + try FileManager.default.moveItem(at: location, to: renamedUnzippedDir) + return renamedUnzippedDir + } + + private func showAlert(_ error: Error) { + let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"), + message: error.localizedDescription, + preferredStyle: .alert) + alert.addAction(UIAlertAction(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil)) + self.window?.rootViewController?.present(alert, animated: true, completion: nil) + } } private extension SceneDelegate { From eb8f27b457e45c843c5e55a64640882afb7e595a Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Mon, 20 Sep 2021 09:48:31 +0800 Subject: [PATCH 05/11] Removes files if unzip is not successful --- iOS/SceneDelegate.swift | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 6658c3e55..54fc4dcc3 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -230,9 +230,14 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { private func unzipFile(at location: URL) throws -> URL { var unzippedDir = location.deletingLastPathComponent() unzippedDir.appendPathComponent("newtheme.nnwtheme") - try Zip.unzipFile(location, destination: unzippedDir, overwrite: true, password: nil, progress: nil, fileOutputHandler: nil) - try FileManager.default.removeItem(at: location) - return unzippedDir + do { + try Zip.unzipFile(location, destination: unzippedDir, overwrite: true, password: nil, progress: nil, fileOutputHandler: nil) + try FileManager.default.removeItem(at: location) + return unzippedDir + } catch { + try? FileManager.default.removeItem(at: location) + throw error + } } private func renameFileToThemeName(at location: URL) throws -> URL { From a1b01384d334ace1cd3b80e089c217960b0c82e7 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Mon, 20 Sep 2021 19:34:25 +0800 Subject: [PATCH 06/11] refactors downloader code for macOS & iOS More consistent code across platforms. --- Mac/AppDelegate.swift | 11 +++ .../AppDelegate+Scriptability.swift | 32 +-------- NetNewsWire.xcodeproj/project.pbxproj | 6 ++ .../ArticleThemeDownloader.swift | 70 +++++++++++++++++++ iOS/SceneDelegate.swift | 66 +++++------------ 5 files changed, 106 insertions(+), 79 deletions(-) create mode 100644 Shared/ArticleStyles/ArticleThemeDownloader.swift diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index afdbbfd56..b7b41e015 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -125,6 +125,7 @@ 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) NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil) appDelegate = self @@ -375,6 +376,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 diff --git a/Mac/Scriptability/AppDelegate+Scriptability.swift b/Mac/Scriptability/AppDelegate+Scriptability.swift index b8ad7ee3d..41a7e22bc 100644 --- a/Mac/Scriptability/AppDelegate+Scriptability.swift +++ b/Mac/Scriptability/AppDelegate+Scriptability.swift @@ -56,40 +56,12 @@ extension AppDelegate : AppDelegateAppleEvents { if let themeURL = URL(string: themeURLString) { let request = URLRequest(url: themeURL) let task = URLSession.shared.downloadTask(with: request) { location, response, error in - var downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! - try? FileManager.default.createDirectory(at: downloadDirectory, withIntermediateDirectories: true, attributes: nil) - let tmpFileName = UUID().uuidString + ".zip" - downloadDirectory.appendPathComponent("\(tmpFileName)") - if location == nil { + guard let location = location else { return } do { - try FileManager.default.moveItem(at: location!, to: downloadDirectory) - - var unzippedDir = downloadDirectory - unzippedDir = unzippedDir.deletingLastPathComponent() - unzippedDir.appendPathComponent("newtheme.nnwtheme") - - try Zip.unzipFile(downloadDirectory, destination: unzippedDir, overwrite: true, password: nil, progress: nil, fileOutputHandler: nil) - try FileManager.default.removeItem(at: downloadDirectory) - - let decoder = PropertyListDecoder() - let plistURL = URL(fileURLWithPath: unzippedDir.appendingPathComponent("Info.plist").path) - - let data = try Data(contentsOf: plistURL) - let plist = try decoder.decode(ArticleThemePlist.self, from: data) - - // rename - var renamedUnzippedDir = unzippedDir.deletingLastPathComponent() - renamedUnzippedDir.appendPathComponent(plist.name + ".nnwtheme") - if FileManager.default.fileExists(atPath: renamedUnzippedDir.path) { - try FileManager.default.removeItem(at: renamedUnzippedDir) - } - try FileManager.default.moveItem(at: unzippedDir, to: renamedUnzippedDir) - DispatchQueue.main.async { - self.importTheme(filename: renamedUnzippedDir.path) - } + try ArticleThemeDownloader.handleFile(at: location) } catch { print(error) } diff --git a/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index 80ff1568c..be116d4ca 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -140,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 */; }; @@ -1608,6 +1610,7 @@ 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 = ""; }; @@ -3461,6 +3464,7 @@ children = ( 849A97871ED9ECEF007D329B /* ArticleTheme.swift */, 849A97881ED9ECEF007D329B /* ArticleThemesManager.swift */, + 17D643B026F8A436008D4C05 /* ArticleThemeDownloader.swift */, 179D280C26F73D83003B2E0A /* ArticleThemePlist.swift */, 17071EEF26F8137400F5E71D /* ArticleTheme+Notifications.swift */, ); @@ -5589,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 */, @@ -5721,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 */, diff --git a/Shared/ArticleStyles/ArticleThemeDownloader.swift b/Shared/ArticleStyles/ArticleThemeDownloader.swift new file mode 100644 index 000000000..8ee711cde --- /dev/null +++ b/Shared/ArticleStyles/ArticleThemeDownloader.swift @@ -0,0 +1,70 @@ +// +// ArticleThemeDownloader.swift +// ArticleThemeDownloader +// +// Created by Stuart Breckenridge on 20/09/2021. +// Copyright © 2021 Ranchero Software. All rights reserved. +// + +import Foundation +import Zip + +public struct ArticleThemeDownloader { + + static func handleFile(at location: URL) throws { + #if os(iOS) + createDownloadDirectoryIfRequired() + #endif + let movedFileLocation = try moveTheme(from: location) + let unzippedFileLocation = try unzipFile(at: movedFileLocation) + let renamedFile = try renameFileToThemeName(at: unzippedFileLocation) + NotificationCenter.default.post(name: .didEndDownloadingTheme, object: nil, userInfo: ["url" : renamedFile]) + } + + private static func createDownloadDirectoryIfRequired() { + let downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! + try? FileManager.default.createDirectory(at: downloadDirectory, withIntermediateDirectories: true, attributes: nil) + } + + private static func moveTheme(from location: URL) throws -> URL { + #if os(iOS) + var downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! + #else + var downloadDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! + #endif + let tmpFileName = UUID().uuidString + ".zip" + downloadDirectory.appendPathComponent("\(tmpFileName)") + try FileManager.default.moveItem(at: location, to: downloadDirectory) + return downloadDirectory + } + + private static func unzipFile(at location: URL) throws -> URL { + var unzippedDir = location.deletingLastPathComponent() + unzippedDir.appendPathComponent("newtheme.nnwtheme") + do { + try Zip.unzipFile(location, destination: unzippedDir, overwrite: true, password: nil, progress: nil, fileOutputHandler: nil) + try FileManager.default.removeItem(at: location) + return unzippedDir + } catch { + try? FileManager.default.removeItem(at: location) + throw error + } + } + + private static func renameFileToThemeName(at location: URL) throws -> URL { + let decoder = PropertyListDecoder() + let plistURL = URL(fileURLWithPath: location.appendingPathComponent("Info.plist").path) + let data = try Data(contentsOf: plistURL) + let plist = try decoder.decode(ArticleThemePlist.self, from: data) + var renamedUnzippedDir = location.deletingLastPathComponent() + renamedUnzippedDir.appendPathComponent(plist.name + ".nnwtheme") + if FileManager.default.fileExists(atPath: renamedUnzippedDir.path) { + try FileManager.default.removeItem(at: renamedUnzippedDir) + } + try FileManager.default.moveItem(at: location, to: renamedUnzippedDir) + return renamedUnzippedDir + } + + + +} diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index 54fc4dcc3..de24868d5 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -29,6 +29,8 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange), name: UserDefaults.didChangeNotification, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil) + if let _ = connectionOptions.urlContexts.first?.url { window?.makeKeyAndVisible() self.scene(scene, openURLContexts: connectionOptions.urlContexts) @@ -184,19 +186,13 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { NotificationCenter.default.post(name: .didBeginDownloadingTheme, object: nil) } let task = URLSession.shared.downloadTask(with: request) { [weak self] location, response, error in - guard let self = self, let location = location else { return } - self.createDownloadDirectoryIfRequired() + guard let self = self, + let location = location else { return } + do { - let movedFileLocation = try self.moveTheme(from: location) - let unzippedFileLocation = try self.unzipFile(at: movedFileLocation) - let renamedFileLocation = try self.renameFileToThemeName(at: unzippedFileLocation) - DispatchQueue.main.async { - NotificationCenter.default.post(name: .didEndDownloadingTheme, object: nil) - self.coordinator.importTheme(filename: renamedFileLocation.path) - } + try ArticleThemeDownloader.handleFile(at: location) } catch { DispatchQueue.main.async { - NotificationCenter.default.post(name: .didEndDownloadingThemeWithError, object: nil, userInfo: ["error" : error]) self.showAlert(error) } } @@ -213,46 +209,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } - // MARK: - Theme Downloader - private func createDownloadDirectoryIfRequired() { - let downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! - try? FileManager.default.createDirectory(at: downloadDirectory, withIntermediateDirectories: true, attributes: nil) - } - private func moveTheme(from location: URL) throws -> URL { - var downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! - let tmpFileName = UUID().uuidString + ".zip" - downloadDirectory.appendPathComponent("\(tmpFileName)") - try FileManager.default.moveItem(at: location, to: downloadDirectory) - return downloadDirectory - } - - private func unzipFile(at location: URL) throws -> URL { - var unzippedDir = location.deletingLastPathComponent() - unzippedDir.appendPathComponent("newtheme.nnwtheme") - do { - try Zip.unzipFile(location, destination: unzippedDir, overwrite: true, password: nil, progress: nil, fileOutputHandler: nil) - try FileManager.default.removeItem(at: location) - return unzippedDir - } catch { - try? FileManager.default.removeItem(at: location) - throw error - } - } - - private func renameFileToThemeName(at location: URL) throws -> URL { - let decoder = PropertyListDecoder() - let plistURL = URL(fileURLWithPath: location.appendingPathComponent("Info.plist").path) - let data = try Data(contentsOf: plistURL) - let plist = try decoder.decode(ArticleThemePlist.self, from: data) - var renamedUnzippedDir = location.deletingLastPathComponent() - renamedUnzippedDir.appendPathComponent(plist.name + ".nnwtheme") - if FileManager.default.fileExists(atPath: renamedUnzippedDir.path) { - try FileManager.default.removeItem(at: renamedUnzippedDir) - } - try FileManager.default.moveItem(at: location, to: renamedUnzippedDir) - return renamedUnzippedDir - } private func showAlert(_ error: Error) { let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"), @@ -295,4 +252,15 @@ private extension SceneDelegate { } } + @objc func importDownloadedTheme(_ note: Notification) { + guard let userInfo = note.userInfo, + let url = userInfo["url"] as? URL else { + return + } + + DispatchQueue.main.async { + self.coordinator.importTheme(filename: url.path) + } + } + } From 1e5fd6499eb525e9ac31708c7efeaad8b830fa21 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Tue, 21 Sep 2021 06:16:38 +0800 Subject: [PATCH 07/11] Corrects identifier spelling --- Shared/ArticleStyles/ArticleThemePlist.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Shared/ArticleStyles/ArticleThemePlist.swift b/Shared/ArticleStyles/ArticleThemePlist.swift index 9525afa94..84ff5e23e 100644 --- a/Shared/ArticleStyles/ArticleThemePlist.swift +++ b/Shared/ArticleStyles/ArticleThemePlist.swift @@ -17,7 +17,7 @@ public struct ArticleThemePlist: Codable { enum CodingKeys: String, CodingKey { case name = "Name" - case themeIdentifier = "ThemeIdentifer" // FIXME: Spelling error! + case themeIdentifier = "ThemeIdentifier" case creatorHomePage = "CreatorHomePage" case creatorName = "CreatorName" case version = "Version" From 7986e1caee2b3bf75d2960cbf2ab346556e09f2f Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Tue, 21 Sep 2021 06:18:15 +0800 Subject: [PATCH 08/11] Adds optional theme description --- Shared/ArticleStyles/ArticleThemePlist.swift | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Shared/ArticleStyles/ArticleThemePlist.swift b/Shared/ArticleStyles/ArticleThemePlist.swift index 84ff5e23e..a0f4e1886 100644 --- a/Shared/ArticleStyles/ArticleThemePlist.swift +++ b/Shared/ArticleStyles/ArticleThemePlist.swift @@ -11,6 +11,7 @@ import Foundation public struct ArticleThemePlist: Codable { public var name: String public var themeIdentifier: String + public var themeDescription: String? public var creatorHomePage: String public var creatorName: String public var version: Int @@ -18,6 +19,7 @@ public struct ArticleThemePlist: Codable { enum CodingKeys: String, CodingKey { case name = "Name" case themeIdentifier = "ThemeIdentifier" + case themeDescription = "ThemeDescription" case creatorHomePage = "CreatorHomePage" case creatorName = "CreatorName" case version = "Version" From 78e0595708889a7e0a646e9a3426599ee77f3676 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Tue, 21 Sep 2021 09:10:56 +0800 Subject: [PATCH 09/11] Various ThemeDownloader Updates - `try` added where applicable to ArticleTheme inits - `ArticleThemePlist` has fixed spelling of theme identifier and conforms to Equatable - `ArticleTheme` now uses `ArticleThemePlist` - `ArticleThemeDownloader` is now a class - `ArticleThemeDownloader` will now download themes to Application Support/NetNewsWire/Downloads on macOS and iOS. - `ArticleThemeDownloader` will remove downloaded themes from the Download folder when the application is closed. - macOS app delegate now observes for theme download fails - Error display code moved from SceneDelegate to SceneCoordinator so that it can use existing presentError on rootVC. --- Mac/AppDelegate.swift | 27 ++++- .../AppDelegate+Scriptability.swift | 4 +- Shared/ArticleStyles/ArticleTheme.swift | 17 +-- .../ArticleThemeDownloader.swift | 101 +++++++++++------- Shared/ArticleStyles/ArticleThemePlist.swift | 4 +- .../ArticleStyles/ArticleThemesManager.swift | 2 +- iOS/AppDelegate.swift | 1 + iOS/SceneCoordinator.swift | 26 ++++- iOS/SceneDelegate.swift | 29 +---- iOS/Settings/ArticleThemeImporter.swift | 6 +- .../ArticleThemesTableViewController.swift | 2 +- 11 files changed, 130 insertions(+), 89 deletions(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index b7b41e015..e3e44e4cb 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -126,6 +126,7 @@ 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: .didEndDownloadingThemeWithError, object: nil) NSWorkspace.shared.notificationCenter.addObserver(self, selector: #selector(didWakeNotification(_:)), name: NSWorkspace.didWakeNotification, object: nil) appDelegate = self @@ -322,7 +323,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, func application(_ sender: NSApplication, openFile filename: String) -> Bool { guard filename.hasSuffix(ArticleTheme.nnwThemeSuffix) else { return false } - importTheme(filename: filename) + try? importTheme(filename: filename) return true } @@ -330,6 +331,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, shuttingDown = true saveState() + ArticleThemeDownloader.shared.cleanUp() + AccountManager.shared.sendArticleStatusAll() { self.isShutDownSyncDone = true } @@ -383,7 +386,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, return } DispatchQueue.main.async { - self.importTheme(filename: url.path) + try? self.importTheme(filename: url.path) } } @@ -808,10 +811,10 @@ internal extension AppDelegate { groupArticlesByFeedMenuItem.state = groupByFeedEnabled ? .on : .off } - func importTheme(filename: String) { + func importTheme(filename: String) throws { guard let window = mainWindowController?.window else { return } - let theme = ArticleTheme(path: filename) + let theme = try ArticleTheme(path: filename) let alert = NSAlert() alert.alertStyle = .informational @@ -906,6 +909,22 @@ internal 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 + } + DispatchQueue.main.async { + let alert = NSAlert() + alert.alertStyle = .warning + alert.messageText = NSLocalizedString("Theme Download Error", comment: "Theme download error") + alert.informativeText = NSLocalizedString("This theme cannot be downloaded due to the following error: \(error.localizedDescription)", comment: "Theme download error information") + alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) + alert.beginSheetModal(for: window) + } + } + } /* diff --git a/Mac/Scriptability/AppDelegate+Scriptability.swift b/Mac/Scriptability/AppDelegate+Scriptability.swift index 41a7e22bc..ffa1686bd 100644 --- a/Mac/Scriptability/AppDelegate+Scriptability.swift +++ b/Mac/Scriptability/AppDelegate+Scriptability.swift @@ -61,9 +61,9 @@ extension AppDelegate : AppDelegateAppleEvents { } do { - try ArticleThemeDownloader.handleFile(at: location) + try ArticleThemeDownloader.shared.handleFile(at: location) } catch { - print(error) + NotificationCenter.default.post(name: .didEndDownloadingThemeWithError, object: nil, userInfo: ["error": error]) } } task.resume() diff --git a/Shared/ArticleStyles/ArticleTheme.swift b/Shared/ArticleStyles/ArticleTheme.swift index a32e37af2..7d4e16bde 100644 --- a/Shared/ArticleStyles/ArticleTheme.swift +++ b/Shared/ArticleStyles/ArticleTheme.swift @@ -26,22 +26,22 @@ 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")! @@ -51,12 +51,13 @@ struct ArticleTheme: Equatable { 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) { diff --git a/Shared/ArticleStyles/ArticleThemeDownloader.swift b/Shared/ArticleStyles/ArticleThemeDownloader.swift index 8ee711cde..1dd775ee1 100644 --- a/Shared/ArticleStyles/ArticleThemeDownloader.swift +++ b/Shared/ArticleStyles/ArticleThemeDownloader.swift @@ -9,62 +9,85 @@ import Foundation import Zip -public struct ArticleThemeDownloader { +public class ArticleThemeDownloader { - static func handleFile(at location: URL) throws { - #if os(iOS) + 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() - #endif let movedFileLocation = try moveTheme(from: location) let unzippedFileLocation = try unzipFile(at: movedFileLocation) - let renamedFile = try renameFileToThemeName(at: unzippedFileLocation) - NotificationCenter.default.post(name: .didEndDownloadingTheme, object: nil, userInfo: ["url" : renamedFile]) + NotificationCenter.default.post(name: .didEndDownloadingTheme, object: nil, userInfo: ["url" : unzippedFileLocation]) } - private static func createDownloadDirectoryIfRequired() { - let downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! - try? FileManager.default.createDirectory(at: downloadDirectory, withIntermediateDirectories: true, attributes: nil) + + /// Creates `Application Support/NetNewsWire/Downloads` if needed. + private func createDownloadDirectoryIfRequired() { + try? FileManager.default.createDirectory(at: downloadDirectory(), withIntermediateDirectories: true, attributes: nil) } - private static func moveTheme(from location: URL) throws -> URL { - #if os(iOS) - var downloadDirectory = FileManager.default.urls(for: .downloadsDirectory, in: .userDomainMask).first! - #else - var downloadDirectory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! - #endif - let tmpFileName = UUID().uuidString + ".zip" - downloadDirectory.appendPathComponent("\(tmpFileName)") - try FileManager.default.moveItem(at: location, to: downloadDirectory) - return downloadDirectory + /// 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 } - private static func unzipFile(at location: URL) throws -> URL { - var unzippedDir = location.deletingLastPathComponent() - unzippedDir.appendPathComponent("newtheme.nnwtheme") + /// Unzips the zip file + /// - Parameter location: Location of the zip archive. + /// - Returns: Enclosed `.nnwtheme` file. + private func unzipFile(at location: URL) throws -> URL { do { - try Zip.unzipFile(location, destination: unzippedDir, overwrite: true, password: nil, progress: nil, fileOutputHandler: nil) - try FileManager.default.removeItem(at: location) - return unzippedDir + 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 } } - private static func renameFileToThemeName(at location: URL) throws -> URL { - let decoder = PropertyListDecoder() - let plistURL = URL(fileURLWithPath: location.appendingPathComponent("Info.plist").path) - let data = try Data(contentsOf: plistURL) - let plist = try decoder.decode(ArticleThemePlist.self, from: data) - var renamedUnzippedDir = location.deletingLastPathComponent() - renamedUnzippedDir.appendPathComponent(plist.name + ".nnwtheme") - if FileManager.default.fileExists(atPath: renamedUnzippedDir.path) { - try FileManager.default.removeItem(at: renamedUnzippedDir) - } - try FileManager.default.moveItem(at: location, to: renamedUnzippedDir) - return renamedUnzippedDir + /// 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 index a0f4e1886..f324cdbde 100644 --- a/Shared/ArticleStyles/ArticleThemePlist.swift +++ b/Shared/ArticleStyles/ArticleThemePlist.swift @@ -8,10 +8,9 @@ import Foundation -public struct ArticleThemePlist: Codable { +public struct ArticleThemePlist: Codable, Equatable { public var name: String public var themeIdentifier: String - public var themeDescription: String? public var creatorHomePage: String public var creatorName: String public var version: Int @@ -19,7 +18,6 @@ public struct ArticleThemePlist: Codable { enum CodingKeys: String, CodingKey { case name = "Name" case themeIdentifier = "ThemeIdentifier" - case themeDescription = "ThemeDescription" case creatorHomePage = "CreatorHomePage" case creatorName = "CreatorName" case version = "Version" diff --git a/Shared/ArticleStyles/ArticleThemesManager.swift b/Shared/ArticleStyles/ArticleThemesManager.swift index e49fe5d9f..0ce833f42 100644 --- a/Shared/ArticleStyles/ArticleThemesManager.swift +++ b/Shared/ArticleStyles/ArticleThemesManager.swift @@ -134,7 +134,7 @@ private extension ArticleThemesManager { return nil } - return ArticleTheme(path: path) + return try? ArticleTheme(path: path) } func defaultArticleTheme() -> ArticleTheme { 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..d0c9fea94 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: .didEndDownloadingThemeWithError, 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,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func importTheme(filename: String) { - ArticleThemeImporter.importTheme(controller: rootSplitViewController, filename: filename); + try? ArticleThemeImporter.importTheme(controller: rootSplitViewController, filename: filename); } } diff --git a/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index de24868d5..534cd0db2 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -29,8 +29,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { NotificationCenter.default.addObserver(self, selector: #selector(userDefaultsDidChange), name: UserDefaults.didChangeNotification, object: nil) - NotificationCenter.default.addObserver(self, selector: #selector(importDownloadedTheme(_:)), name: .didEndDownloadingTheme, object: nil) - if let _ = connectionOptions.urlContexts.first?.url { window?.makeKeyAndVisible() self.scene(scene, openURLContexts: connectionOptions.urlContexts) @@ -190,11 +188,9 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { let location = location else { return } do { - try ArticleThemeDownloader.handleFile(at: location) + try ArticleThemeDownloader.shared.handleFile(at: location) } catch { - DispatchQueue.main.async { - self.showAlert(error) - } + NotificationCenter.default.post(name: .didEndDownloadingThemeWithError, object: nil, userInfo: ["error": error]) } } task.resume() @@ -208,16 +204,6 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { } } - - - - private func showAlert(_ error: Error) { - let alert = UIAlertController(title: NSLocalizedString("Error", comment: "Error"), - message: error.localizedDescription, - preferredStyle: .alert) - alert.addAction(UIAlertAction(title: NSLocalizedString("Dismiss", comment: "Dismiss"), style: .cancel, handler: nil)) - self.window?.rootViewController?.present(alert, animated: true, completion: nil) - } } private extension SceneDelegate { @@ -252,15 +238,6 @@ private extension SceneDelegate { } } - @objc func importDownloadedTheme(_ note: Notification) { - guard let userInfo = note.userInfo, - let url = userInfo["url"] as? URL else { - return - } - - DispatchQueue.main.async { - self.coordinator.importTheme(filename: url.path) - } - } + } 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..d8b7e4104 100644 --- a/iOS/Settings/ArticleThemesTableViewController.swift +++ b/iOS/Settings/ArticleThemesTableViewController.swift @@ -112,7 +112,7 @@ 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) + try? ArticleThemeImporter.importTheme(controller: self, filename: url.standardizedFileURL.path) } } From c29afd26771a7c4d7e0691543e964ddd23218fdf Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Tue, 21 Sep 2021 09:22:45 +0800 Subject: [PATCH 10/11] try? changed to try with error handling --- Mac/AppDelegate.swift | 162 +++++++++--------- .../AppDelegate+Scriptability.swift | 2 +- .../ArticleTheme+Notifications.swift | 2 +- .../ArticleStyles/ArticleThemesManager.swift | 9 +- iOS/SceneCoordinator.swift | 9 +- iOS/SceneDelegate.swift | 2 +- .../ArticleThemesTableViewController.swift | 6 +- 7 files changed, 106 insertions(+), 86 deletions(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index e3e44e4cb..69636e937 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -126,7 +126,7 @@ 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: .didEndDownloadingThemeWithError, 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 @@ -811,88 +811,94 @@ internal extension AppDelegate { groupArticlesByFeedMenuItem.state = groupByFeedEnabled ? .on : .off } - func importTheme(filename: String) throws { + func importTheme(filename: String) { guard let window = mainWindowController?.window else { return } - let theme = try 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) { @@ -918,8 +924,8 @@ internal extension AppDelegate { DispatchQueue.main.async { let alert = NSAlert() alert.alertStyle = .warning - alert.messageText = NSLocalizedString("Theme Download Error", comment: "Theme download error") - alert.informativeText = NSLocalizedString("This theme cannot be downloaded due to the following error: \(error.localizedDescription)", comment: "Theme download error information") + alert.messageText = NSLocalizedString("Theme Error", comment: "Theme download error") + alert.informativeText = NSLocalizedString("This theme cannot be imported due to the following error: \(error.localizedDescription)", comment: "Theme download error information") alert.addButton(withTitle: NSLocalizedString("OK", comment: "OK")) alert.beginSheetModal(for: window) } diff --git a/Mac/Scriptability/AppDelegate+Scriptability.swift b/Mac/Scriptability/AppDelegate+Scriptability.swift index ffa1686bd..132fe26be 100644 --- a/Mac/Scriptability/AppDelegate+Scriptability.swift +++ b/Mac/Scriptability/AppDelegate+Scriptability.swift @@ -63,7 +63,7 @@ extension AppDelegate : AppDelegateAppleEvents { do { try ArticleThemeDownloader.shared.handleFile(at: location) } catch { - NotificationCenter.default.post(name: .didEndDownloadingThemeWithError, object: nil, userInfo: ["error": error]) + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) } } task.resume() diff --git a/Shared/ArticleStyles/ArticleTheme+Notifications.swift b/Shared/ArticleStyles/ArticleTheme+Notifications.swift index 08337aab4..4c2391ff4 100644 --- a/Shared/ArticleStyles/ArticleTheme+Notifications.swift +++ b/Shared/ArticleStyles/ArticleTheme+Notifications.swift @@ -11,5 +11,5 @@ import Foundation extension Notification.Name { static let didBeginDownloadingTheme = Notification.Name("didBeginDownloadingTheme") static let didEndDownloadingTheme = Notification.Name("didEndDownloadingTheme") - static let didEndDownloadingThemeWithError = Notification.Name("didEndDownloadingThemeWithError") + static let didFailToImportThemeWithError = Notification.Name("didFailToImportThemeWithError") } diff --git a/Shared/ArticleStyles/ArticleThemesManager.swift b/Shared/ArticleStyles/ArticleThemesManager.swift index 0ce833f42..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 try? 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/iOS/SceneCoordinator.swift b/iOS/SceneCoordinator.swift index d0c9fea94..f3a2c1abc 100644 --- a/iOS/SceneCoordinator.swift +++ b/iOS/SceneCoordinator.swift @@ -324,7 +324,7 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { 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: .didEndDownloadingThemeWithError, object: nil) + NotificationCenter.default.addObserver(self, selector: #selector(themeDownloadDidFail(_:)), name: .didFailToImportThemeWithError, object: nil) } func start(for size: CGSize) -> UIViewController { @@ -1317,7 +1317,12 @@ class SceneCoordinator: NSObject, UndoableCommandRunner, UnreadCountProvider { } func importTheme(filename: String) { - try? 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 534cd0db2..af4069522 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -190,7 +190,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { do { try ArticleThemeDownloader.shared.handleFile(at: location) } catch { - NotificationCenter.default.post(name: .didEndDownloadingThemeWithError, object: nil, userInfo: ["error": error]) + NotificationCenter.default.post(name: .didFailToImportThemeWithError, object: nil, userInfo: ["error": error]) } } task.resume() diff --git a/iOS/Settings/ArticleThemesTableViewController.swift b/iOS/Settings/ArticleThemesTableViewController.swift index d8b7e4104..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 } - try? 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]) + } } } From 82a62712ced895aca17a83cacc9aac2298562fe0 Mon Sep 17 00:00:00 2001 From: Stuart Breckenridge Date: Tue, 21 Sep 2021 10:43:12 +0800 Subject: [PATCH 11/11] Better error messages for decoding issues --- Mac/AppDelegate.swift | 25 ++++++++++++++--- NetNewsWire.xcodeproj/project.pbxproj | 4 +-- .../xcshareddata/swiftpm/Package.resolved | 4 +-- Shared/ArticleStyles/ArticleTheme.swift | 27 ++++++++++--------- iOS/SceneDelegate.swift | 2 +- .../UIViewController-Extensions.swift | 17 ++++++++++++ 6 files changed, 58 insertions(+), 21 deletions(-) diff --git a/Mac/AppDelegate.swift b/Mac/AppDelegate.swift index 69636e937..ebf1bd30d 100644 --- a/Mac/AppDelegate.swift +++ b/Mac/AppDelegate.swift @@ -323,7 +323,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, func application(_ sender: NSApplication, openFile filename: String) -> Bool { guard filename.hasSuffix(ArticleTheme.nnwThemeSuffix) else { return false } - try? importTheme(filename: filename) + importTheme(filename: filename) return true } @@ -386,7 +386,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations, return } DispatchQueue.main.async { - try? self.importTheme(filename: url.path) + self.importTheme(filename: url.path) } } @@ -921,11 +921,30 @@ internal extension AppDelegate { 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: \(error.localizedDescription)", comment: "Theme download error information") + 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/NetNewsWire.xcodeproj/project.pbxproj b/NetNewsWire.xcodeproj/project.pbxproj index be116d4ca..7d1644d2c 100644 --- a/NetNewsWire.xcodeproj/project.pbxproj +++ b/NetNewsWire.xcodeproj/project.pbxproj @@ -6436,8 +6436,8 @@ isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/marmelroy/Zip.git"; requirement = { - kind = upToNextMajorVersion; - minimumVersion = 2.0.0; + kind = revision; + revision = 059e7346082d02de16220cd79df7db18ddeba8c3; }; }; 5102AE4324D17E820050839C /* XCRemoteSwiftPackageReference "RSCore" */ = { diff --git a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index fc2737cdd..dc8075713 100644 --- a/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/NetNewsWire.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -123,8 +123,8 @@ "repositoryURL": "https://github.com/marmelroy/Zip.git", "state": { "branch": null, - "revision": "bd19d974e8a38cc8d3a88c90c8a107386c3b8ccf", - "version": "2.1.1" + "revision": "059e7346082d02de16220cd79df7db18ddeba8c3", + "version": null } } ] diff --git a/Shared/ArticleStyles/ArticleTheme.swift b/Shared/ArticleStyles/ArticleTheme.swift index 7d4e16bde..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? @@ -38,26 +38,27 @@ struct ArticleTheme: Equatable { } private let info: ArticleThemePlist? - + init() { self.path = nil; 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) throws { self.path = path - + let infoPath = (path as NSString).appendingPathComponent("Info.plist") 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) { @@ -65,7 +66,7 @@ struct ArticleTheme: Equatable { } else { self.css = nil } - + let templatePath = (path as NSString).appendingPathComponent("template.html") self.template = Self.stringAtPath(templatePath) } @@ -74,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/iOS/SceneDelegate.swift b/iOS/SceneDelegate.swift index af4069522..6812578ac 100644 --- a/iOS/SceneDelegate.swift +++ b/iOS/SceneDelegate.swift @@ -184,7 +184,7 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate { NotificationCenter.default.post(name: .didBeginDownloadingTheme, object: nil) } let task = URLSession.shared.downloadTask(with: request) { [weak self] location, response, error in - guard let self = self, + guard let location = location else { return } do { 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)