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