Major folder and project tree restructuring.

This commit is contained in:
Brent Simmons
2019-04-13 16:18:54 -07:00
parent 778068a81a
commit 70312aa75c
253 changed files with 296 additions and 2346 deletions

View File

@@ -0,0 +1,142 @@
//
// Account+Scriptability.swift
// NetNewsWire
//
// Created by Olof Hellman on 1/9/18.
// Copyright © 2018 Olof Hellman. All rights reserved.
//
import AppKit
import Account
import Articles
import RSCore
@objc(ScriptableAccount)
class ScriptableAccount: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let account:Account
init (_ account:Account) {
self.account = account
}
@objc(objectSpecifier)
override var objectSpecifier: NSScriptObjectSpecifier? {
let myContainer = NSApplication.shared
let scriptObjectSpecifier = myContainer.makeFormUniqueIDScriptObjectSpecifier(forObject:self)
return (scriptObjectSpecifier)
}
// MARK: --- ScriptingObject protocol ---
var scriptingKey: String {
return "accounts"
}
// MARK: --- UniqueIdScriptingObject protocol ---
// I am not sure if account should prefer to be specified by name or by ID
// but in either case it seems like the accountID would be used as the keydata, so I chose ID
@objc(uniqueId)
var scriptingUniqueId:Any {
return account.accountID
}
// MARK: --- ScriptingObjectContainer protocol ---
var scriptingClassDescription: NSScriptClassDescription {
return self.classDescription as! NSScriptClassDescription
}
func deleteElement(_ element:ScriptingObject) {
if let scriptableFolder = element as? ScriptableFolder {
BatchUpdate.shared.perform {
account.deleteFolder(scriptableFolder.folder)
}
} else if let scriptableFeed = element as? ScriptableFeed {
BatchUpdate.shared.perform {
account.deleteFeed(scriptableFeed.feed)
}
}
}
@objc(isLocationRequiredToCreateForKey:)
func isLocationRequiredToCreate(forKey key:String) -> Bool {
return false;
}
// MARK: --- Scriptable elements ---
@objc(feeds)
var feeds:NSArray {
return account.topLevelFeeds.map { ScriptableFeed($0, container:self) } as NSArray
}
@objc(valueInFeedsWithUniqueID:)
func valueInFeeds(withUniqueID id:String) -> ScriptableFeed? {
let feeds = Array(account.topLevelFeeds)
guard let feed = feeds.first(where:{$0.feedID == id}) else { return nil }
return ScriptableFeed(feed, container:self)
}
@objc(valueInFeedsWithName:)
func valueInFeeds(withName name:String) -> ScriptableFeed? {
let feeds = Array(account.topLevelFeeds)
guard let feed = feeds.first(where:{$0.name == name}) else { return nil }
return ScriptableFeed(feed, container:self)
}
@objc(folders)
var folders:NSArray {
let foldersSet = account.folders ?? Set<Folder>()
let folders = Array(foldersSet)
return folders.map { ScriptableFolder($0, container:self) } as NSArray
}
@objc(valueInFoldersWithUniqueID:)
func valueInFolders(withUniqueID id:NSNumber) -> ScriptableFolder? {
let folderId = id.intValue
let foldersSet = account.folders ?? Set<Folder>()
let folders = Array(foldersSet)
guard let folder = folders.first(where:{$0.folderID == folderId}) else { return nil }
return ScriptableFolder(folder, container:self)
}
// MARK: --- Scriptable properties ---
@objc(contents)
var contents:NSArray {
var contentsArray:[AnyObject] = []
for feed in account.topLevelFeeds {
contentsArray.append(ScriptableFeed(feed, container: self))
}
if let folders = account.folders {
for folder in folders {
contentsArray.append(ScriptableFolder(folder, container:self))
}
}
return contentsArray as NSArray
}
@objc(opmlRepresentation)
var opmlRepresentation:String {
return self.account.OPMLString(indentLevel:0)
}
@objc(accountType)
var accountType:OSType {
var osType:String = ""
switch self.account.type {
case .onMyMac:
osType = "Locl"
case .feedly:
osType = "Fdly"
case .feedbin:
osType = "Fdbn"
case .feedWrangler:
osType = "FWrg"
case .newsBlur:
osType = "NBlr"
}
return osType.fourCharCode()
}
}

View File

@@ -0,0 +1,151 @@
//
// AppDelegate+Scriptability.swift
// NetNewsWire
//
// Created by Olof Hellman on 2/7/18.
// Copyright © 2018 Olof Hellman. All rights reserved.
//
/*
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
import Articles
protocol AppDelegateAppleEvents {
func installAppleEventHandlers()
func getURL(_ event: NSAppleEventDescriptor, _ withReplyEvent: NSAppleEventDescriptor)
}
protocol ScriptingAppDelegate {
var scriptingCurrentArticle: Article? {get}
var scriptingSelectedArticles: [Article] {get}
var scriptingMainWindowController:ScriptingMainWindowController? {get}
}
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) {
guard let urlString = event.paramDescriptor(forKeyword: keyDirectObject)?.stringValue else {
return
}
let normalizedURLString = urlString.rs_normalizedURL()
if !normalizedURLString.rs_stringMayBeURL() {
return
}
DispatchQueue.main.async {
self.addFeed(normalizedURLString)
}
}
}
class NetNewsWireCreateElementCommand : NSCreateCommand {
override func performDefaultImplementation() -> Any? {
let classDescription = self.createClassDescription
if (classDescription.className == "feed") {
return ScriptableFeed.handleCreateElement(command:self)
} 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.
*/
class NetNewsWireDeleteCommand : NSDeleteCommand {
/*
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)
}
}
return nil
}
}
class NetNewsWireExistsCommand : NSExistsCommand {
// 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
}
}

View File

@@ -0,0 +1,134 @@
//
// Article+Scriptability.swift
// NetNewsWire
//
// Created by Olof Hellman on 1/23/18.
// Copyright © 2018 Olof Hellman. All rights reserved.
//
import Foundation
import Account
import Articles
@objc(ScriptableArticle)
class ScriptableArticle: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let article:Article
let container:ScriptingObjectContainer
init (_ article:Article, container:ScriptingObjectContainer) {
self.article = article
self.container = container
}
@objc(objectSpecifier)
override var objectSpecifier: NSScriptObjectSpecifier? {
let scriptObjectSpecifier = self.container.makeFormUniqueIDScriptObjectSpecifier(forObject:self)
return (scriptObjectSpecifier)
}
// MARK: --- ScriptingObject protocol ---
var scriptingKey: String {
return "articles"
}
// MARK: --- UniqueIdScriptingObject protocol ---
// articles have id in the NetNewsWire database and id in the feed
// article.uniqueID here is the feed unique id
@objc(uniqueId)
var scriptingUniqueId:Any {
return article.uniqueID
}
// MARK: --- ScriptingObjectContainer protocol ---
var scriptingClassDescription: NSScriptClassDescription {
return self.classDescription as! NSScriptClassDescription
}
func deleteElement(_ element:ScriptingObject) {
print ("delete event not handled")
}
// MARK: --- Scriptable properties ---
@objc(url)
var url:String? {
return article.url ?? article.externalURL
}
@objc(permalink)
var permalink:String? {
return article.url
}
@objc(externalUrl)
var externalUrl:String? {
return article.externalURL
}
@objc(title)
var title:String {
return article.title ?? ""
}
@objc(contents)
var contents:String {
return article.contentText ?? ""
}
@objc(html)
var html:String {
return article.contentHTML ?? ""
}
@objc(summary)
var summary:String {
return article.summary ?? ""
}
@objc(datePublished)
var datePublished:Date? {
return article.datePublished
}
@objc(dateModified)
var dateModified:Date? {
return article.dateModified
}
@objc(dateArrived)
var dateArrived:Date {
return article.status.dateArrived
}
@objc(read)
var read:Bool {
return article.status.boolStatus(forKey:.read)
}
@objc(starred)
var starred:Bool {
return article.status.boolStatus(forKey:.starred)
}
@objc(deleted)
var deleted:Bool {
return article.status.boolStatus(forKey:.userDeleted)
}
@objc(imageURL)
var imageURL:String {
return article.imageURL ?? ""
}
@objc(authors)
var authors:NSArray {
let articleAuthors = article.authors ?? []
return articleAuthors.map { ScriptableAuthor($0, container:self) } as NSArray
}
}

View File

@@ -0,0 +1,67 @@
//
// Author+Scriptability.swift
// NetNewsWire
//
// Created by Olof Hellman on 1/19/18.
// Copyright © 2018 Olof Hellman. All rights reserved.
//
import Foundation
import Account
import Articles
@objc(ScriptableAuthor)
class ScriptableAuthor: NSObject, UniqueIdScriptingObject {
let author:Author
let container:ScriptingObjectContainer
init (_ author:Author, container:ScriptingObjectContainer) {
self.author = author
self.container = container
}
@objc(objectSpecifier)
override var objectSpecifier: NSScriptObjectSpecifier? {
let scriptObjectSpecifier = self.container.makeFormUniqueIDScriptObjectSpecifier(forObject:self)
return (scriptObjectSpecifier)
}
// MARK: --- ScriptingObject protocol ---
var scriptingKey: String {
return "authors"
}
// MARK: --- UniqueIdScriptingObject protocol ---
// I am not sure if account should prefer to be specified by name or by ID
// but in either case it seems like the accountID would be used as the keydata, so I chose ID
@objc(uniqueId)
var scriptingUniqueId:Any {
return author.authorID
}
// MARK: --- Scriptable properties ---
@objc(url)
var url:String {
return self.author.url ?? ""
}
@objc(name)
var name:String {
return self.author.name ?? ""
}
@objc(avatarURL)
var avatarURL:String {
return self.author.avatarURL ?? ""
}
@objc(emailAddress)
var emailAddress:String {
return self.author.emailAddress ?? ""
}
}

View File

@@ -0,0 +1,199 @@
//
// Feed+Scriptability.swift
// NetNewsWire
//
// Created by Olof Hellman on 1/10/18.
// Copyright © 2018 Olof Hellman. All rights reserved.
//
import Foundation
import RSParser
import Account
import Articles
@objc(ScriptableFeed)
class ScriptableFeed: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer{
let feed:Feed
let container:ScriptingObjectContainer
init (_ feed:Feed, container:ScriptingObjectContainer) {
self.feed = feed
self.container = container
}
@objc(objectSpecifier)
override var objectSpecifier: NSScriptObjectSpecifier? {
let scriptObjectSpecifier = self.container.makeFormUniqueIDScriptObjectSpecifier(forObject:self)
return (scriptObjectSpecifier)
}
@objc(scriptingSpecifierDescriptor)
func scriptingSpecifierDescriptor() -> NSScriptObjectSpecifier {
return (self.objectSpecifier ?? NSScriptObjectSpecifier() )
}
// MARK: --- ScriptingObject protocol ---
var scriptingKey: String {
return "feeds"
}
// MARK: --- UniqueIdScriptingObject protocol ---
// I am not sure if account should prefer to be specified by name or by ID
// but in either case it seems like the accountID would be used as the keydata, so I chose ID
@objc(uniqueId)
var scriptingUniqueId:Any {
return feed.feedID
}
// MARK: --- ScriptingObjectContainer protocol ---
var scriptingClassDescription: NSScriptClassDescription {
return self.classDescription as! NSScriptClassDescription
}
func deleteElement(_ element:ScriptingObject) {
}
// MARK: --- handle NSCreateCommand ---
class func parsedFeedForURL(_ urlString:String, _ completionHandler: @escaping (_ parsedFeed: ParsedFeed?) -> Void) {
guard let url = URL(string: urlString) else {
completionHandler(nil)
return
}
InitialFeedDownloader.download(url) { (parsedFeed) in
completionHandler(parsedFeed)
}
}
class func urlForNewFeed(arguments:[String:Any]) -> String? {
var url:String?
if let withDataParam = arguments["ObjectData"] {
if let objectDataDescriptor = withDataParam as? NSAppleEventDescriptor {
url = objectDataDescriptor.stringValue
}
} else if let withPropsParam = arguments["ObjectProperties"] as? [String:Any] {
url = withPropsParam["url"] as? String
}
return url
}
class func scriptableFeed(_ feed:Feed, account:Account, folder:Folder?) -> ScriptableFeed {
let scriptableAccount = ScriptableAccount(account)
if let folder = folder {
let scriptableFolder = ScriptableFolder(folder, container:scriptableAccount)
return ScriptableFeed(feed, container:scriptableFolder)
} else {
return ScriptableFeed(feed, container:scriptableAccount)
}
}
class func handleCreateElement(command:NSCreateCommand) -> Any? {
guard command.isCreateCommand(forClass:"Feed") else { return nil }
guard let arguments = command.arguments else {return nil}
let titleFromArgs = command.property(forKey:"name") as? String
let (account, folder) = command.accountAndFolderForNewChild()
guard let url = self.urlForNewFeed(arguments:arguments) else {return nil}
if let existingFeed = account.existingFeed(withURL:url) {
return self.scriptableFeed(existingFeed, account:account, folder:folder)
}
// at this point, we need to download the feed and parse it.
// RS Parser does the callback for the download on the main thread (which it probably shouldn't?)
// because we can't wait here (on the main thread, maybe) for the callback, we have to return from this function
// Generally, returning from an AppleEvent handler function means that handling the appleEvent is over,
// but we don't yet have the result of the event yet, so we prevent the AppleEvent from returning by calling
// suspendExecution(). When we get the callback, we can supply the event result and call resumeExecution()
command.suspendExecution()
self.parsedFeedForURL(url, { (parsedFeedOptional) in
if let parsedFeed = parsedFeedOptional {
let titleFromFeed = parsedFeed.title
guard let feed = account.createFeed(with: titleFromFeed, editedName: titleFromArgs, url: url) else {
command.resumeExecution(withResult:nil)
return
}
account.update(feed, with:parsedFeed, {})
// add the feed, putting it in a folder if needed
account.addFeed(feed, to:folder)
NotificationCenter.default.post(name: .UserDidAddFeed, object: self, userInfo: [UserInfoKey.feed: feed])
let scriptableFeed = self.scriptableFeed(feed, account:account, folder:folder)
command.resumeExecution(withResult:scriptableFeed.objectSpecifier)
} else {
command.resumeExecution(withResult:nil)
}
})
return nil
}
// MARK: --- Scriptable properties ---
@objc(url)
var url:String {
return self.feed.url
}
@objc(name)
var name:String {
return self.feed.name ?? ""
}
@objc(homePageURL)
var homePageURL:String {
return self.feed.homePageURL ?? ""
}
@objc(iconURL)
var iconURL:String {
return self.feed.iconURL ?? ""
}
@objc(faviconURL)
var faviconURL:String {
return self.feed.faviconURL ?? ""
}
@objc(opmlRepresentation)
var opmlRepresentation:String {
return self.feed.OPMLString(indentLevel:0)
}
// MARK: --- scriptable elements ---
@objc(authors)
var authors:NSArray {
let feedAuthors = feed.authors ?? []
return feedAuthors.map { ScriptableAuthor($0, container:self) } as NSArray
}
@objc(valueInAuthorsWithUniqueID:)
func valueInAuthors(withUniqueID id:String) -> ScriptableAuthor? {
guard let author = feed.authors?.first(where:{$0.authorID == id}) else { return nil }
return ScriptableAuthor(author, container:self)
}
@objc(articles)
var articles:NSArray {
let feedArticles = feed.fetchArticles()
// the articles are a set, use the sorting algorithm from the viewer
let sortedArticles = feedArticles.sorted(by:{
return $0.logicalDatePublished > $1.logicalDatePublished
})
return sortedArticles.map { ScriptableArticle($0, container:self) } as NSArray
}
@objc(valueInArticlesWithUniqueID:)
func valueInArticles(withUniqueID id:String) -> ScriptableArticle? {
let articles = feed.fetchArticles()
guard let article = articles.first(where:{$0.uniqueID == id}) else { return nil }
return ScriptableArticle(article, container:self)
}
}

View File

@@ -0,0 +1,111 @@
//
// Folder+Scriptability.swift
// NetNewsWire
//
// Created by Olof Hellman on 1/10/18.
// Copyright © 2018 Olof Hellman. All rights reserved.
//
import Foundation
import Account
import Articles
import RSCore
@objc(ScriptableFolder)
class ScriptableFolder: NSObject, UniqueIdScriptingObject, ScriptingObjectContainer {
let folder:Folder
let container:ScriptingObjectContainer
init (_ folder:Folder, container:ScriptingObjectContainer) {
self.folder = folder
self.container = container
}
@objc(objectSpecifier)
override var objectSpecifier: NSScriptObjectSpecifier? {
let scriptObjectSpecifier = self.container.makeFormUniqueIDScriptObjectSpecifier(forObject:self)
return (scriptObjectSpecifier)
}
// MARK: --- ScriptingObject protocol ---
var scriptingKey: String {
return "folders"
}
// MARK: --- UniqueIdScriptingObject protocol ---
// I am not sure if account should prefer to be specified by name or by ID
// but in either case it seems like the accountID would be used as the keydata, so I chose ID
@objc(uniqueId)
var scriptingUniqueId:Any {
return folder.folderID
}
// MARK: --- ScriptingObjectContainer protocol ---
var scriptingClassDescription: NSScriptClassDescription {
return self.classDescription as! NSScriptClassDescription
}
func deleteElement(_ element:ScriptingObject) {
if let scriptableFolder = element as? ScriptableFolder {
BatchUpdate.shared.perform {
folder.deleteFolder(scriptableFolder.folder)
}
} else if let scriptableFeed = element as? ScriptableFeed {
BatchUpdate.shared.perform {
folder.deleteFeed(scriptableFeed.feed)
}
}
}
// MARK: --- handle NSCreateCommand ---
/*
handle an AppleScript like
make new folder in account X with properties {name:"new folder name"}
or
tell account X to make new folder at end with properties {name:"new folder name"}
*/
class func handleCreateElement(command:NSCreateCommand) -> Any? {
guard command.isCreateCommand(forClass:"fold") else { return nil }
let name = command.property(forKey:"name") as? String ?? ""
// some combination of the tell target and the location specifier ("in" or "at")
// identifies where the new folder should be created
let (account, folder) = command.accountAndFolderForNewChild()
guard folder == nil else {
print("support for folders within folders is NYI");
return nil
}
let scriptableAccount = ScriptableAccount(account)
if let newFolder = account.ensureFolder(with:name) {
let scriptableFolder = ScriptableFolder(newFolder, container:scriptableAccount)
return(scriptableFolder.objectSpecifier)
}
return nil
}
// MARK: --- Scriptable elements ---
@objc(feeds)
var feeds:NSArray {
let feeds = Array(folder.topLevelFeeds)
return feeds.map { ScriptableFeed($0, container:self) } as NSArray
}
// MARK: --- Scriptable properties ---
@objc(name)
var name:String {
return self.folder.name ?? ""
}
@objc(opmlRepresentation)
var opmlRepresentation:String {
return self.folder.OPMLString(indentLevel:0)
}
}

View File

@@ -0,0 +1,16 @@
//
// MainWindowController+Scriptability.swift
// NetNewsWire
//
// Created by Olof Hellman on 2/7/18.
// Copyright © 2018 Olof Hellman. All rights reserved.
//
import Foundation
import Articles
protocol ScriptingMainWindowController {
var scriptingCurrentArticle: Article? { get }
var scriptingSelectedArticles: [Article] { get }
}

View File

@@ -0,0 +1,99 @@
//
// NSApplication+Scriptability.swift
// NetNewsWire
//
// Created by Olof Hellman on 1/8/18.
// Copyright © 2018 Olof Hellman. All rights reserved.
//
import AppKit
import Account
import Articles
extension NSApplication : ScriptingObjectContainer {
// MARK: --- ScriptingObjectContainer protocol ---
var scriptingClassDescription: NSScriptClassDescription {
return NSApplication.shared.classDescription as! NSScriptClassDescription
}
func deleteElement(_ element:ScriptingObject) {
print ("delete event not handled")
}
var scriptingKey: String {
return "application"
}
@objc(currentArticle)
func currentArticle() -> ScriptableArticle? {
var scriptableArticle: ScriptableArticle?
if let currentArticle = appDelegate.scriptingCurrentArticle {
if let feed = currentArticle.feed {
let scriptableFeed = ScriptableFeed(feed, container:self)
scriptableArticle = ScriptableArticle(currentArticle, container:scriptableFeed)
}
}
return scriptableArticle
}
@objc(selectedArticles)
func selectedArticles() -> NSArray {
let articles = appDelegate.scriptingSelectedArticles
let scriptableArticles:[ScriptableArticle] = articles.compactMap { article in
if let feed = article.feed {
let scriptableFeed = ScriptableFeed(feed, container:self)
return ScriptableArticle(article, container:scriptableFeed)
} else {
return nil
}
}
return scriptableArticles as NSArray
}
// MARK: --- scriptable elements ---
@objc(accounts)
func accounts() -> NSArray {
let accounts = AccountManager.shared.accounts
return accounts.map { ScriptableAccount($0) } as NSArray
}
@objc(valueInAccountsWithUniqueID:)
func valueInAccounts(withUniqueID id:String) -> ScriptableAccount? {
let accounts = AccountManager.shared.accounts
guard let account = accounts.first(where:{$0.accountID == id}) else { return nil }
return ScriptableAccount(account)
}
/*
accessing feeds from the application object skips the 'account' containment hierarchy
this allows a script like 'articles of feed "The Shape of Everything"' as a shorthand
for 'articles of feed "The Shape of Everything" of account "On My Mac"'
*/
func allFeeds() -> [Feed] {
let accounts = AccountManager.shared.accounts
let emptyFeeds:[Feed] = []
return accounts.reduce(emptyFeeds) { (result, nthAccount) -> [Feed] in
let accountFeeds = Array(nthAccount.topLevelFeeds)
return result + accountFeeds
}
}
@objc(feeds)
func feeds() -> NSArray {
let feeds = self.allFeeds()
return feeds.map { ScriptableFeed($0, container:self) } as NSArray
}
@objc(valueInFeedsWithUniqueID:)
func valueInFeeds(withUniqueID id:String) -> ScriptableFeed? {
let feeds = self.allFeeds()
guard let feed = feeds.first(where:{$0.feedID == id}) else { return nil }
return ScriptableFeed(feed, container:self)
}
}

View File

@@ -0,0 +1,63 @@
//
// NSScriptCommand+NetNewsWire.swift
// NetNewsWire
//
// Created by Olof Hellman on 3/4/18.
// Copyright © 2018 Olof Hellman. All rights reserved.
//
import Foundation
import Account
extension NSScriptCommand {
func property(forKey key:String) -> Any? {
if let evaluatedArguments = self.evaluatedArguments {
if let props = evaluatedArguments["KeyDictionary"] as? [String: Any] {
return props[key]
}
}
return nil
}
func isCreateCommand(forClass whatClass:String) -> Bool {
guard let arguments = self.arguments else {return false}
guard let newObjectClass = arguments["ObjectClass"] as? Int else {return false}
guard (newObjectClass.fourCharCode() == whatClass.fourCharCode()) else {return false}
return true
}
func accountAndFolderForNewChild() -> (Account, Folder?) {
let appleEvent = self.appleEvent
var account = AccountManager.shared.localAccount
var folder:Folder? = nil
if let appleEvent = appleEvent {
var descriptorToConsider:NSAppleEventDescriptor?
if let insertionLocationDescriptor = appleEvent.paramDescriptor(forKeyword:keyAEInsertHere) {
print("insertionLocation : \(insertionLocationDescriptor)")
// insertion location can be a typeObjectSpecifier, e.g. 'in account "Acct"'
// or a typeInsertionLocation, e.g. 'at end of folder "
if (insertionLocationDescriptor.descriptorType == "insl".fourCharCode()) {
descriptorToConsider = insertionLocationDescriptor.forKeyword("kobj".fourCharCode())
} else if ( insertionLocationDescriptor.descriptorType == "obj ".fourCharCode()) {
descriptorToConsider = insertionLocationDescriptor
}
} else if let subjectDescriptor = appleEvent.attributeDescriptor(forKeyword:"subj".fourCharCode()) {
descriptorToConsider = subjectDescriptor
}
if let descriptorToConsider = descriptorToConsider {
guard let newContainerSpecifier = NSScriptObjectSpecifier(descriptor:descriptorToConsider) else {return (account, folder)}
let newContainer = newContainerSpecifier.objectsByEvaluatingSpecifier
if let scriptableAccount = newContainer as? ScriptableAccount {
account = scriptableAccount.account
} else if let scriptableFolder = newContainer as? ScriptableFolder {
if let folderAccount = scriptableFolder.folder.account {
folder = scriptableFolder.folder
account = folderAccount
}
}
}
}
return (account, folder)
}
}

View File

@@ -0,0 +1,22 @@
//
// ScriptingObject.swift
// NetNewsWire
//
// Created by Olof Hellman on 1/10/18.
// Copyright © 2018 Olof Hellman. All rights reserved.
//
import Foundation
protocol ScriptingObject {
var objectSpecifier: NSScriptObjectSpecifier? { get }
var scriptingKey: String { get }
}
protocol NamedScriptingObject: ScriptingObject {
var name:String { get }
}
protocol UniqueIdScriptingObject: ScriptingObject {
var scriptingUniqueId:Any { get }
}

View File

@@ -0,0 +1,39 @@
//
// ScriptingObjectContainer.swift
// Account
//
// Created by Olof Hellman on 1/9/18.
// Copyright © 2018 Olof Hellman. All rights reserved.
//
import AppKit
import Account
protocol ScriptingObjectContainer: ScriptingObject {
var scriptingClassDescription:NSScriptClassDescription { get }
func deleteElement(_ element:ScriptingObject)
}
extension ScriptingObjectContainer {
func makeFormNameScriptObjectSpecifier(forObject object:NamedScriptingObject) -> NSScriptObjectSpecifier? {
let containerClassDescription = self.scriptingClassDescription
let containerScriptObjectSpecifier = self.objectSpecifier
let scriptingKey = object.scriptingKey
let name = object.name
let specifier = NSNameSpecifier(containerClassDescription:containerClassDescription,
containerSpecifier:containerScriptObjectSpecifier, key:scriptingKey, name:name)
return specifier
}
func makeFormUniqueIDScriptObjectSpecifier(forObject object:UniqueIdScriptingObject) -> NSScriptObjectSpecifier? {
let containerClassDescription = self.scriptingClassDescription
let containerScriptObjectSpecifier = self.objectSpecifier
let scriptingKey = object.scriptingKey
let uniqueId = object.scriptingUniqueId
let specifier = NSUniqueIDSpecifier(containerClassDescription:containerClassDescription,
containerSpecifier:containerScriptObjectSpecifier, key:scriptingKey, uniqueID: uniqueId)
return specifier
}
}