2018-02-08 09:11:52 +01:00
|
|
|
//
|
|
|
|
// AppDelegate+Scriptability.swift
|
2018-08-29 07:18:24 +02:00
|
|
|
// NetNewsWire
|
2018-02-08 09:11:52 +01:00
|
|
|
//
|
|
|
|
// Created by Olof Hellman on 2/7/18.
|
2018-03-05 04:01:58 +01:00
|
|
|
// Copyright © 2018 Olof Hellman. All rights reserved.
|
2018-02-08 09:11:52 +01:00
|
|
|
//
|
|
|
|
|
|
|
|
/*
|
|
|
|
Note: strictly, the AppDelegate doesn't appear as part of the scripting model,
|
|
|
|
so this file is rather unlike the other Object+Scriptability.swift files.
|
|
|
|
However, the AppDelegate object is the de facto scripting accessor for some
|
|
|
|
application elements and properties. For, example, the main window is accessed
|
|
|
|
via the AppDelegate's MainWindowController, and the main window itself has
|
|
|
|
selected feeds, selected articles and a current article. This file supplies the glue to access
|
|
|
|
these scriptable objects, while being completely separate from the core AppDelegate code,
|
|
|
|
*/
|
|
|
|
|
|
|
|
import Foundation
|
2018-07-24 03:29:08 +02:00
|
|
|
import Articles
|
2021-09-19 15:18:23 +02:00
|
|
|
import Zip
|
2018-02-08 09:11:52 +01:00
|
|
|
|
2018-02-11 10:20:30 +01:00
|
|
|
protocol AppDelegateAppleEvents {
|
|
|
|
func installAppleEventHandlers()
|
|
|
|
func getURL(_ event: NSAppleEventDescriptor, _ withReplyEvent: NSAppleEventDescriptor)
|
|
|
|
}
|
|
|
|
|
2018-02-08 09:11:52 +01:00
|
|
|
protocol ScriptingAppDelegate {
|
|
|
|
var scriptingCurrentArticle: Article? {get}
|
|
|
|
var scriptingSelectedArticles: [Article] {get}
|
|
|
|
var scriptingMainWindowController:ScriptingMainWindowController? {get}
|
|
|
|
}
|
|
|
|
|
2018-02-11 10:20:30 +01:00
|
|
|
extension AppDelegate : AppDelegateAppleEvents {
|
|
|
|
|
|
|
|
// MARK: GetURL Apple Event
|
|
|
|
|
|
|
|
func installAppleEventHandlers() {
|
|
|
|
NSAppleEventManager.shared().setEventHandler(self, andSelector: #selector(AppDelegate.getURL(_:_:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL))
|
|
|
|
}
|
|
|
|
|
|
|
|
@objc func getURL(_ event: NSAppleEventDescriptor, _ withReplyEvent: NSAppleEventDescriptor) {
|
|
|
|
|
2021-06-23 20:50:25 +02:00
|
|
|
guard var urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue else {
|
2018-02-11 10:20:30 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2021-09-19 15:18:23 +02:00
|
|
|
// 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
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2021-06-23 20:50:25 +02:00
|
|
|
// 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:"
|
|
|
|
if urlString.hasPrefix(nnwScheme) {
|
|
|
|
urlString = urlString.replacingOccurrences(of: nnwScheme, with: "feed:")
|
|
|
|
}
|
|
|
|
|
2020-01-17 03:09:18 +01:00
|
|
|
let normalizedURLString = urlString.normalizedURL
|
|
|
|
if !normalizedURLString.mayBeURL {
|
2018-02-11 10:20:30 +01:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
2020-04-22 04:25:45 +02:00
|
|
|
self.addWebFeed(normalizedURLString)
|
2018-02-11 10:20:30 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-29 07:18:24 +02:00
|
|
|
class NetNewsWireCreateElementCommand : NSCreateCommand {
|
2018-02-20 09:26:46 +01:00
|
|
|
override func performDefaultImplementation() -> Any? {
|
|
|
|
let classDescription = self.createClassDescription
|
2019-11-15 22:46:43 +01:00
|
|
|
if (classDescription.className == "webFeed") {
|
|
|
|
return ScriptableWebFeed.handleCreateElement(command:self)
|
2018-03-05 03:43:29 +01:00
|
|
|
} else if (classDescription.className == "folder") {
|
|
|
|
return ScriptableFolder.handleCreateElement(command:self)
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
NSDeleteCommand is kind of an oddball AppleScript command in that the command dispatch
|
|
|
|
goes to the container of the object(s) to be deleted, and the container needs to
|
|
|
|
figure out what to delete. In the code below, 'receivers' is the container object(s)
|
|
|
|
and keySpecifier is the thing to delete, relative to the container(s). Because there
|
|
|
|
is ambiguity about whether specifiers are lists or single objects, the code switches
|
|
|
|
based on which it is.
|
|
|
|
*/
|
2018-08-29 07:18:24 +02:00
|
|
|
class NetNewsWireDeleteCommand : NSDeleteCommand {
|
2018-03-05 03:43:29 +01:00
|
|
|
|
|
|
|
/*
|
|
|
|
delete(objectToDelete:, from container:)
|
|
|
|
At this point in handling the command, we know what the container is.
|
|
|
|
Here the code unravels the case of objectToDelete being a list or a single object,
|
|
|
|
ultimately calling container.deleteElement(element) for each element to delete
|
|
|
|
*/
|
|
|
|
func delete(objectToDelete:Any, from container:ScriptingObjectContainer) {
|
|
|
|
if let objectList = objectToDelete as? [Any] {
|
|
|
|
for nthObject in objectList {
|
|
|
|
self.delete(objectToDelete:nthObject, from:container)
|
|
|
|
}
|
|
|
|
} else if let element = objectToDelete as? ScriptingObject {
|
|
|
|
container.deleteElement(element)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
delete(specifier:, from container:)
|
|
|
|
At this point in handling the command, the container could be a list or a single object,
|
|
|
|
and what to delete is still an unresolved NSScriptObjectSpecifier.
|
|
|
|
Here the code unravels the case of container being a list or a single object. Once the
|
|
|
|
container(s) is known, it is possible to resolve the keySpecifier based on that container.
|
|
|
|
After resolving, we call delete(objectToDelete:, from container:) with the container and
|
|
|
|
the resolved objects
|
|
|
|
*/
|
|
|
|
func delete(specifier:NSScriptObjectSpecifier, from container:Any) {
|
|
|
|
if let containerList = container as? [Any] {
|
|
|
|
for nthObject in containerList {
|
|
|
|
self.delete(specifier:specifier, from:nthObject)
|
|
|
|
}
|
|
|
|
} else if let container = container as? ScriptingObjectContainer {
|
|
|
|
if let resolvedObjects = specifier.objectsByEvaluating(withContainers:container) {
|
|
|
|
self.delete(objectToDelete:resolvedObjects, from:container)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
performDefaultImplementation()
|
|
|
|
This is where handling the delete event starts. receiversSpecifier should be the container(s) of
|
|
|
|
the item to be deleted. keySpecifier is the thing in that container(s) to be deleted
|
|
|
|
The first step is to resolve the receiversSpecifier and then call delete(specifier:, from container:)
|
|
|
|
*/
|
|
|
|
override func performDefaultImplementation() -> Any? {
|
|
|
|
if let receiversSpecifier = self.receiversSpecifier {
|
|
|
|
if let receiverObjects = receiversSpecifier.objectsByEvaluatingSpecifier {
|
|
|
|
self.delete(specifier:self.keySpecifier, from:receiverObjects)
|
|
|
|
}
|
2018-02-20 09:26:46 +01:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-08-29 07:18:24 +02:00
|
|
|
class NetNewsWireExistsCommand : NSExistsCommand {
|
2018-02-20 09:26:46 +01:00
|
|
|
|
2018-02-11 10:20:30 +01:00
|
|
|
// cocoa default behavior doesn't work here, because of cases where we define an object's property
|
|
|
|
// to be another object type. e.g., 'permalink of the current article' parses as
|
|
|
|
// <property> of <property> of <top level object>
|
|
|
|
// cocoa would send the top level object (the app) a doesExist message for a nested property, and
|
|
|
|
// it errors out because it doesn't know how to handle that
|
|
|
|
// What we do instead is simply see if the defaultImplementation errors, and if it does, the object
|
|
|
|
// must not exist. Otherwise, we return the result of the defaultImplementation
|
|
|
|
// The wrinkle is that it is possible that the direct object is a list, so we need to
|
|
|
|
// handle that case as well
|
|
|
|
|
|
|
|
override func performDefaultImplementation() -> Any? {
|
|
|
|
guard let result = super.performDefaultImplementation() else { return NSNumber(booleanLiteral:false) }
|
|
|
|
return result
|
|
|
|
}
|
|
|
|
}
|
2018-02-08 09:11:52 +01:00
|
|
|
|