Merge remote-tracking branch 'brentsimmons/master'
This commit is contained in:
commit
fe5a11a2cc
|
@ -6,6 +6,39 @@
|
|||
<description>Most recent Evergreen changes with links to updates.</description>
|
||||
<language>en</language>
|
||||
|
||||
<item>
|
||||
<title>Evergreen 1.0d33</title>
|
||||
<description><![CDATA[
|
||||
|
||||
<h4>Send to MarsEdit</h4>
|
||||
<p>If you have <a href="https://red-sweater.com/marsedit/">MarsEdit</a> on your Mac, then you can send an article from Evergreen to MarsEdit via the sharing menu in the toolbar.</p>
|
||||
<p>In MarsEdit you can edit the post and add your commentary before posting to your blog.</p>
|
||||
|
||||
]]></description>
|
||||
<pubDate>Mon, 15 Jan 2018 14:10:00 -0800</pubDate>
|
||||
<enclosure url="https://ranchero.com/downloads/Evergreen1.0d33.zip" sparkle:version="813" sparkle:shortVersionString="1.0d33" length="7274707" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.13</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Evergreen 1.0d32</title>
|
||||
<description><![CDATA[
|
||||
|
||||
<h4>Send to Micro.blog</h4>
|
||||
<p>See the Sharing command in the toolbar for a new feature: send to Micro.blog. This sends the current item to your copy of the Micro.blog Mac app (if you have it). It gets launched if necessary. You can edit the post in Micro.blog before actually posting it to your microblog.</p>
|
||||
<p>This is hugely important, because feed reading isn’t just about reading — it’s also about posting. While Evergreen doesn’t itself include a way to post to the web, other apps do, and Evergreen should make it easy to connect to these other apps.</p>
|
||||
<p>Note: send-to-MarsEdit is next, probably in the next release.</p>
|
||||
|
||||
<h4>Misc.</h4>
|
||||
<p>Improve the promptness and reliability of feed icons and favicons appearing in the timeline (when a folder is selected).</p>
|
||||
<p>Increase the indentation in the source list so that feeds inside folders line up better.</p>
|
||||
|
||||
]]></description>
|
||||
<pubDate>Sun, 14 Jan 2018 12:00:00 -0800</pubDate>
|
||||
<enclosure url="https://ranchero.com/downloads/Evergreen1.0d32.zip" sparkle:version="804" sparkle:shortVersionString="1.0d32" length="7265605" type="application/zip" />
|
||||
<sparkle:minimumSystemVersion>10.13</sparkle:minimumSystemVersion>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<title>Version 1.0d31</title>
|
||||
<description><![CDATA[
|
||||
|
|
|
@ -12,17 +12,10 @@ import Cocoa
|
|||
|
||||
protocol SendToCommand {
|
||||
|
||||
var title: String { get }
|
||||
var image: NSImage? { get }
|
||||
|
||||
func canSendObject(_ object: Any?, selectedText: String?) -> Bool
|
||||
func sendObject(_ object: Any?, selectedText: String?)
|
||||
}
|
||||
|
||||
extension SendToCommand {
|
||||
|
||||
func appExistsOnDisk(_ bundleIdentifier: String) -> Bool {
|
||||
|
||||
if let _ = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleIdentifier) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,16 +6,77 @@
|
|||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Cocoa
|
||||
import RSCore
|
||||
import Data
|
||||
|
||||
final class SendToMarsEditCommand: SendToCommand {
|
||||
|
||||
let title = "MarsEdit"
|
||||
|
||||
var image: NSImage? {
|
||||
return appToUse()?.icon ?? nil
|
||||
}
|
||||
|
||||
private let marsEditApps = [UserApp(bundleID: "com.red-sweater.marsedit4"), UserApp(bundleID: "com.red-sweater.marsedit")]
|
||||
|
||||
func canSendObject(_ object: Any?, selectedText: String?) -> Bool {
|
||||
|
||||
if let _ = appToUse() {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func sendObject(_ object: Any?, selectedText: String?) {
|
||||
|
||||
guard canSendObject(object, selectedText: selectedText) else {
|
||||
return
|
||||
}
|
||||
guard let article = (object as? ArticlePasteboardWriter)?.article else {
|
||||
return
|
||||
}
|
||||
guard let app = appToUse(), app.launchIfNeeded(), app.bringToFront() else {
|
||||
return
|
||||
}
|
||||
|
||||
send(article, to: app)
|
||||
}
|
||||
}
|
||||
|
||||
private extension SendToMarsEditCommand {
|
||||
|
||||
func send(_ article: Article, to app: UserApp) {
|
||||
|
||||
// App has already been launched.
|
||||
|
||||
guard let targetDescriptor = app.targetDescriptor() else {
|
||||
return
|
||||
}
|
||||
|
||||
let body = article.contentHTML ?? article.contentText ?? article.summary
|
||||
let authorName = article.authors?.first?.name
|
||||
|
||||
let sender = SendToBlogEditorApp(targetDesciptor: targetDescriptor, title: article.title, body: body, summary: article.summary, link: article.externalURL, permalink: article.url, subject: nil, creator: authorName, commentsURL: nil, guid: article.uniqueID, sourceName: article.feed?.nameForDisplay, sourceHomeURL: article.feed?.homePageURL, sourceFeedURL: article.feed?.url)
|
||||
let _ = sender.send()
|
||||
}
|
||||
|
||||
func appToUse() -> UserApp? {
|
||||
|
||||
marsEditApps.forEach{ $0.updateStatus() }
|
||||
|
||||
for app in marsEditApps {
|
||||
if app.isRunning {
|
||||
return app
|
||||
}
|
||||
}
|
||||
|
||||
for app in marsEditApps {
|
||||
if app.existsOnDisk {
|
||||
return app
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,23 +8,24 @@
|
|||
|
||||
import Cocoa
|
||||
import Data
|
||||
import RSCore
|
||||
|
||||
// Not undoable.
|
||||
|
||||
final class SendToMicroBlogCommand: SendToCommand {
|
||||
|
||||
private let bundleID = "blog.micro.mac"
|
||||
private var appExists = false
|
||||
let title = "Micro.blog"
|
||||
|
||||
init() {
|
||||
|
||||
self.appExists = appExistsOnDisk(bundleID)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(appDidBecomeActive(_:)), name: NSApplication.didBecomeActiveNotification, object: nil)
|
||||
var image: NSImage? {
|
||||
return microBlogApp.icon
|
||||
}
|
||||
|
||||
private let microBlogApp = UserApp(bundleID: "blog.micro.mac")
|
||||
|
||||
func canSendObject(_ object: Any?, selectedText: String?) -> Bool {
|
||||
|
||||
guard appExists, let article = object as? Article, let _ = article.preferredLink else {
|
||||
microBlogApp.updateStatus()
|
||||
guard microBlogApp.existsOnDisk, let article = (object as? ArticlePasteboardWriter)?.article, let _ = article.preferredLink else {
|
||||
return false
|
||||
}
|
||||
|
||||
|
@ -36,42 +37,60 @@ final class SendToMicroBlogCommand: SendToCommand {
|
|||
guard canSendObject(object, selectedText: selectedText) else {
|
||||
return
|
||||
}
|
||||
guard let article = object as? Article else {
|
||||
guard let article = (object as? ArticlePasteboardWriter)?.article else {
|
||||
return
|
||||
}
|
||||
guard microBlogApp.launchIfNeeded(), microBlogApp.bringToFront() else {
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: get text from contentHTML or contentText if no title and no selectedText.
|
||||
var s = ""
|
||||
if let selectedText = selectedText {
|
||||
s += selectedText
|
||||
if let link = article.preferredLink {
|
||||
s += "\n\n\(link)"
|
||||
}
|
||||
}
|
||||
else if let title = article.title {
|
||||
s += title
|
||||
if let link = article.preferredLink {
|
||||
s = "[" + s + "](" + link + ")"
|
||||
}
|
||||
}
|
||||
else if let link = article.preferredLink {
|
||||
s = link
|
||||
}
|
||||
// TODO: consider selectedText.
|
||||
|
||||
guard let encodedString = s.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
|
||||
let s = article.attributionString + article.linkString
|
||||
|
||||
let urlQueryDictionary = ["text": s]
|
||||
guard let urlQueryString = urlQueryDictionary.urlQueryString() else {
|
||||
return
|
||||
}
|
||||
guard let url = URL(string: "microblog://post?text=" + encodedString) else {
|
||||
guard let url = URL(string: "microblog://post?" + urlQueryString) else {
|
||||
return
|
||||
}
|
||||
|
||||
let _ = try? NSWorkspace.shared.open(url, options: [], configuration: [:])
|
||||
}
|
||||
|
||||
@objc func appDidBecomeActive(_ note: Notification) {
|
||||
|
||||
self.appExists = appExistsOnDisk(bundleID)
|
||||
}
|
||||
}
|
||||
|
||||
private extension Article {
|
||||
|
||||
var attributionString: String {
|
||||
|
||||
// Feed name, or feed name + author name (if author is specified per-article).
|
||||
// Includes trailing space.
|
||||
|
||||
if let feedName = feed?.nameForDisplay, let authorName = authors?.first?.name {
|
||||
return feedName + ", " + authorName + ": "
|
||||
}
|
||||
if let feedName = feed?.nameForDisplay {
|
||||
return feedName + ": "
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var linkString: String {
|
||||
|
||||
// Title + link or just title (if no link) or just link if no title
|
||||
|
||||
if let title = title, let link = preferredLink {
|
||||
return "[" + title + "](" + link + ")"
|
||||
}
|
||||
if let preferredLink = preferredLink {
|
||||
return preferredLink
|
||||
}
|
||||
if let title = title {
|
||||
return title
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -483,6 +483,7 @@
|
|||
846E77161F6EF5D000A165E2 /* Database.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Database.xcodeproj; path = Frameworks/Database/Database.xcodeproj; sourceTree = "<group>"; };
|
||||
846E77301F6EF5D600A165E2 /* Account.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = Account.xcodeproj; path = Frameworks/Account/Account.xcodeproj; sourceTree = "<group>"; };
|
||||
84702AA31FA27AC0006B8943 /* MarkReadOrUnreadCommand.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MarkReadOrUnreadCommand.swift; sourceTree = "<group>"; };
|
||||
847752FE2008879500D93690 /* CoreServices.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = CoreServices.framework; path = System/Library/Frameworks/CoreServices.framework; sourceTree = SDKROOT; };
|
||||
848F6AE41FC29CFA002D422E /* FaviconDownloader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FaviconDownloader.swift; sourceTree = "<group>"; };
|
||||
849A97421ED9EAA9007D329B /* AddFolderWindowController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AddFolderWindowController.swift; sourceTree = "<group>"; };
|
||||
849A97511ED9EAC0007D329B /* AddFeedController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = AddFeedController.swift; path = AddFeed/AddFeedController.swift; sourceTree = "<group>"; };
|
||||
|
@ -1100,6 +1101,7 @@
|
|||
84FB9A2C1EDCD6A4003D53B9 /* Frameworks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
847752FE2008879500D93690 /* CoreServices.framework */,
|
||||
84FB9A2D1EDCD6B8003D53B9 /* Sparkle.framework */,
|
||||
);
|
||||
name = Frameworks;
|
||||
|
|
|
@ -33,6 +33,10 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSUserInterfaceValidations,
|
|||
return image
|
||||
}()
|
||||
|
||||
lazy var sendToCommands: [SendToCommand] = {
|
||||
return [SendToMicroBlogCommand(), SendToMarsEditCommand()]
|
||||
}()
|
||||
|
||||
var unreadCount = 0 {
|
||||
didSet {
|
||||
if unreadCount != oldValue {
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0d31</string>
|
||||
<string>1.0d33</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>522</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
|
|
@ -287,6 +287,7 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
|||
|
||||
let items = selectedArticles.map { ArticlePasteboardWriter(article: $0) }
|
||||
let sharingServicePicker = NSSharingServicePicker(items: items)
|
||||
sharingServicePicker.delegate = self
|
||||
sharingServicePicker.show(relativeTo: view.bounds, of: view, preferredEdge: .minY)
|
||||
}
|
||||
|
||||
|
@ -299,6 +300,30 @@ class MainWindowController : NSWindowController, NSUserInterfaceValidations {
|
|||
}
|
||||
}
|
||||
|
||||
// MARK: - NSSharingServicePickerDelegate
|
||||
|
||||
extension MainWindowController: NSSharingServicePickerDelegate {
|
||||
|
||||
func sharingServicePicker(_ sharingServicePicker: NSSharingServicePicker, sharingServicesForItems items: [Any], proposedSharingServices proposedServices: [NSSharingService]) -> [NSSharingService] {
|
||||
|
||||
let sendToServices = appDelegate.sendToCommands.flatMap { (sendToCommand) -> NSSharingService? in
|
||||
|
||||
guard let object = items.first else {
|
||||
return nil
|
||||
}
|
||||
guard sendToCommand.canSendObject(object, selectedText: nil) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
let image = sendToCommand.image ?? appDelegate.genericFeedImage ?? NSImage()
|
||||
return NSSharingService(title: sendToCommand.title, image: image, alternateImage: nil) {
|
||||
sendToCommand.sendObject(object, selectedText: nil)
|
||||
}
|
||||
}
|
||||
return proposedServices + sendToServices
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - NSToolbarDelegate
|
||||
|
||||
extension NSToolbarItem.Identifier {
|
||||
|
|
|
@ -11,7 +11,7 @@ import Data
|
|||
|
||||
@objc final class ArticlePasteboardWriter: NSObject, NSPasteboardWriting {
|
||||
|
||||
private let article: Article
|
||||
let article: Article
|
||||
static let articleUTI = "com.ranchero.article"
|
||||
static let articleUTIType = NSPasteboard.PasteboardType(rawValue: articleUTI)
|
||||
static let articleUTIInternal = "com.ranchero.evergreen.internal.article"
|
||||
|
|
|
@ -52,6 +52,7 @@ class TimelineViewController: NSViewController, UndoableCommandRunner {
|
|||
private var didRegisterForNotifications = false
|
||||
private let timelineFontSizeKVOKey = "values.{AppDefaults.Key.timelineFontSize}"
|
||||
private var reloadAvailableCellsTimer: Timer?
|
||||
private var fetchAndMergeArticlesTimer: Timer?
|
||||
|
||||
private var articles = ArticleArray() {
|
||||
didSet {
|
||||
|
@ -120,6 +121,7 @@ class TimelineViewController: NSViewController, UndoableCommandRunner {
|
|||
NotificationCenter.default.addObserver(self, selector: #selector(avatarDidBecomeAvailable(_:)), name: .AvatarDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .ImageDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(imageDidBecomeAvailable(_:)), name: .FaviconDidBecomeAvailable, object: nil)
|
||||
NotificationCenter.default.addObserver(self, selector: #selector(accountDidDownloadArticles(_:)), name: .AccountDidDownloadArticles, object: nil)
|
||||
|
||||
NSUserDefaultsController.shared.addObserver(self, forKeyPath: timelineFontSizeKVOKey, options: NSKeyValueObservingOptions(rawValue: 0), context: nil)
|
||||
|
||||
|
@ -369,6 +371,18 @@ class TimelineViewController: NSViewController, UndoableCommandRunner {
|
|||
}
|
||||
}
|
||||
|
||||
@objc func accountDidDownloadArticles(_ note: Notification) {
|
||||
|
||||
guard let feeds = note.userInfo?[Account.UserInfoKey.feeds] as? Set<Feed> else {
|
||||
return
|
||||
}
|
||||
|
||||
let shouldFetchAndMergeArticles = representedObjectsContainsAnyFeed(feeds)
|
||||
if shouldFetchAndMergeArticles {
|
||||
queueFetchAndMergeArticles()
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Reloading Data
|
||||
|
||||
private func cellForRowView(_ rowView: NSView) -> NSView? {
|
||||
|
@ -627,6 +641,15 @@ private extension TimelineViewController {
|
|||
return
|
||||
}
|
||||
|
||||
let fetchedArticles = fetchUnsortedArticles(for: representedObjects)
|
||||
let sortedArticles = Array(fetchedArticles).sortedByDate()
|
||||
if articles != sortedArticles {
|
||||
articles = sortedArticles
|
||||
}
|
||||
}
|
||||
|
||||
func fetchUnsortedArticles(for representedObjects: [Any]) -> Set<Article> {
|
||||
|
||||
var fetchedArticles = Set<Article>()
|
||||
|
||||
for object in representedObjects {
|
||||
|
@ -639,9 +662,43 @@ private extension TimelineViewController {
|
|||
}
|
||||
}
|
||||
|
||||
let sortedArticles = Array(fetchedArticles).sortedByDate()
|
||||
if articles != sortedArticles {
|
||||
articles = sortedArticles
|
||||
return fetchedArticles
|
||||
}
|
||||
|
||||
func fetchAndMergeArticles() {
|
||||
|
||||
let selectedArticleIDs = selectedArticles.articleIDs()
|
||||
|
||||
|
||||
selectArticles(selectedArticleIDs)
|
||||
}
|
||||
|
||||
func selectArticles(_ articleIDs: [String]) {
|
||||
|
||||
// let indexesToSelect = indexesOf(articleIDs)
|
||||
// if indexesToSelect.isEmpty {
|
||||
// tableView.deselectAll(self)
|
||||
// return
|
||||
// }
|
||||
// tableView.selectRowIndexes(indexesToSelect, byExtendingSelection: false)
|
||||
}
|
||||
|
||||
func invalidateFetchAndMergeArticlesTimer() {
|
||||
|
||||
if let timer = fetchAndMergeArticlesTimer {
|
||||
if timer.isValid {
|
||||
timer.invalidate()
|
||||
}
|
||||
fetchAndMergeArticlesTimer = nil
|
||||
}
|
||||
}
|
||||
|
||||
func queueFetchAndMergeArticles() {
|
||||
|
||||
invalidateFetchAndMergeArticlesTimer()
|
||||
fetchAndMergeArticlesTimer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: false) { (timer) in
|
||||
self.fetchAndMergeArticles()
|
||||
self.invalidateFetchAndMergeArticlesTimer()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -666,5 +723,31 @@ private extension TimelineViewController {
|
|||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func representedObjectsContainsAnyFeed(_ feeds: Set<Feed>) -> Bool {
|
||||
|
||||
// Return true if there’s a match or if a folder contains (recursively) one of feeds
|
||||
|
||||
guard let representedObjects = representedObjects else {
|
||||
return false
|
||||
}
|
||||
for representedObject in representedObjects {
|
||||
if let feed = representedObject as? Feed {
|
||||
for oneFeed in feeds {
|
||||
if feed.feedID == oneFeed.feedID || feed.url == oneFeed.url {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
else if let folder = representedObject as? Folder {
|
||||
for oneFeed in feeds {
|
||||
if folder.hasFeed(with: oneFeed.feedID) || folder.hasFeed(withURL: oneFeed.url) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -41,7 +41,7 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
public static let updatedArticles = "updatedArticles" // AccountDidDownloadArticles
|
||||
public static let statuses = "statuses" // StatusesDidChange
|
||||
public static let articles = "articles" // StatusesDidChange
|
||||
public static let feeds = "feeds" // StatusesDidChange
|
||||
public static let feeds = "feeds" // AccountDidDownloadArticles, StatusesDidChange
|
||||
}
|
||||
|
||||
public let accountID: String
|
||||
|
@ -167,10 +167,11 @@ public final class Account: DisplayNameProvider, UnreadCountProvider, Container,
|
|||
if let updatedArticles = updatedArticles, !updatedArticles.isEmpty {
|
||||
userInfo[UserInfoKey.updatedArticles] = updatedArticles
|
||||
}
|
||||
userInfo[UserInfoKey.feeds] = Set([feed])
|
||||
|
||||
completion()
|
||||
|
||||
NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo.isEmpty ? nil : userInfo)
|
||||
NotificationCenter.default.post(name: .AccountDidDownloadArticles, object: self, userInfo: userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
842E45CC1ED623C7000A8B52 /* UniqueIdentifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842E45CB1ED623C7000A8B52 /* UniqueIdentifier.swift */; };
|
||||
8432B1861DACA0E90057D6DF /* NSResponder-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8432B1851DACA0E90057D6DF /* NSResponder-Extensions.swift */; };
|
||||
8432B1881DACA2060057D6DF /* NSWindow-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8432B1871DACA2060057D6DF /* NSWindow-Extensions.swift */; };
|
||||
8434D15C200BD6F400D6281E /* UserApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8434D15B200BD6F400D6281E /* UserApp.swift */; };
|
||||
84411E731FE5FFC3004B527F /* NSImage+RSCore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84411E721FE5FFC3004B527F /* NSImage+RSCore.swift */; };
|
||||
844B5B571FE9D36000C7C76A /* Keyboard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844B5B561FE9D36000C7C76A /* Keyboard.swift */; };
|
||||
844C915B1B65753E0051FC1B /* RSPlist.h in Headers */ = {isa = PBXBuildFile; fileRef = 844C91591B65753E0051FC1B /* RSPlist.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
|
@ -102,6 +103,10 @@
|
|||
84B99C9A1FAE650100ECDEDB /* OPMLRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */; };
|
||||
84B99C9B1FAE650100ECDEDB /* OPMLRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */; };
|
||||
84BB45431D6909C700B48537 /* NSMutableDictionary-Extensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84BB45421D6909C700B48537 /* NSMutableDictionary-Extensions.swift */; };
|
||||
84C632A0200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C6329E200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
84C632A1200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C6329F200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m */; };
|
||||
84C632A4200D356E007BEEAA /* SendToBlogEditorApp.h in Headers */ = {isa = PBXBuildFile; fileRef = 84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */; settings = {ATTRIBUTES = (Public, ); }; };
|
||||
84C632A5200D356E007BEEAA /* SendToBlogEditorApp.m in Sources */ = {isa = PBXBuildFile; fileRef = 84C632A3200D356E007BEEAA /* SendToBlogEditorApp.m */; };
|
||||
84C687301FBAA30800345C9E /* LogWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 84C6872F1FBAA30800345C9E /* LogWindow.xib */; };
|
||||
84C687321FBAA3DF00345C9E /* LogWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C687311FBAA3DF00345C9E /* LogWindowController.swift */; };
|
||||
84C687351FBC025600345C9E /* Log.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84C687341FBC025600345C9E /* Log.swift */; };
|
||||
|
@ -191,6 +196,7 @@
|
|||
842E45CB1ED623C7000A8B52 /* UniqueIdentifier.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = UniqueIdentifier.swift; sourceTree = "<group>"; };
|
||||
8432B1851DACA0E90057D6DF /* NSResponder-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSResponder-Extensions.swift"; sourceTree = "<group>"; };
|
||||
8432B1871DACA2060057D6DF /* NSWindow-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSWindow-Extensions.swift"; sourceTree = "<group>"; };
|
||||
8434D15B200BD6F400D6281E /* UserApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = UserApp.swift; path = AppKit/UserApp.swift; sourceTree = "<group>"; };
|
||||
84411E721FE5FFC3004B527F /* NSImage+RSCore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "NSImage+RSCore.swift"; path = "Images/NSImage+RSCore.swift"; sourceTree = "<group>"; };
|
||||
844B5B561FE9D36000C7C76A /* Keyboard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Keyboard.swift; path = RSCore/Keyboard.swift; sourceTree = "<group>"; };
|
||||
844C91591B65753E0051FC1B /* RSPlist.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = RSPlist.h; path = RSCore/RSPlist.h; sourceTree = "<group>"; };
|
||||
|
@ -218,6 +224,10 @@
|
|||
84B99C931FAE64D400ECDEDB /* DisplayNameProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = DisplayNameProvider.swift; path = RSCore/DisplayNameProvider.swift; sourceTree = "<group>"; };
|
||||
84B99C991FAE650100ECDEDB /* OPMLRepresentable.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = OPMLRepresentable.swift; path = RSCore/OPMLRepresentable.swift; sourceTree = "<group>"; };
|
||||
84BB45421D6909C700B48537 /* NSMutableDictionary-Extensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = "NSMutableDictionary-Extensions.swift"; sourceTree = "<group>"; };
|
||||
84C6329E200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = "NSAppleEventDescriptor+RSCore.h"; path = "AppKit/NSAppleEventDescriptor+RSCore.h"; sourceTree = "<group>"; };
|
||||
84C6329F200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = "NSAppleEventDescriptor+RSCore.m"; path = "AppKit/NSAppleEventDescriptor+RSCore.m"; sourceTree = "<group>"; };
|
||||
84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; name = SendToBlogEditorApp.h; path = AppKit/SendToBlogEditorApp.h; sourceTree = "<group>"; };
|
||||
84C632A3200D356E007BEEAA /* SendToBlogEditorApp.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; name = SendToBlogEditorApp.m; path = AppKit/SendToBlogEditorApp.m; sourceTree = "<group>"; };
|
||||
84C6872F1FBAA30800345C9E /* LogWindow.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = LogWindow.xib; path = AppKit/LogWindow.xib; sourceTree = "<group>"; };
|
||||
84C687311FBAA3DF00345C9E /* LogWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = LogWindowController.swift; path = AppKit/LogWindowController.swift; sourceTree = "<group>"; };
|
||||
84C687341FBC025600345C9E /* Log.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Log.swift; sourceTree = "<group>"; };
|
||||
|
@ -434,6 +444,10 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
84CFF5511AC3CF4700CEA6C8 /* NSColor+RSCore.h */,
|
||||
84C6329E200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h */,
|
||||
84C6329F200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m */,
|
||||
84C632A2200D356E007BEEAA /* SendToBlogEditorApp.h */,
|
||||
84C632A3200D356E007BEEAA /* SendToBlogEditorApp.m */,
|
||||
84CFF5521AC3CF4700CEA6C8 /* NSColor+RSCore.m */,
|
||||
8415CB881BF84D24007B1E98 /* NSEvent+RSCore.h */,
|
||||
8415CB891BF84D24007B1E98 /* NSEvent+RSCore.m */,
|
||||
|
@ -460,6 +474,7 @@
|
|||
84C687311FBAA3DF00345C9E /* LogWindowController.swift */,
|
||||
84C687341FBC025600345C9E /* Log.swift */,
|
||||
84C687371FBC028900345C9E /* LogItem.swift */,
|
||||
8434D15B200BD6F400D6281E /* UserApp.swift */,
|
||||
842DD7F91E1499FA00E061EB /* Views */,
|
||||
);
|
||||
name = AppKit;
|
||||
|
@ -544,6 +559,7 @@
|
|||
84CFF4FA1AC3C69700CEA6C8 /* RSCore.h in Headers */,
|
||||
844F91D51D90D86100820C48 /* RSTransparentContainerView.h in Headers */,
|
||||
84CFF53F1AC3CD0100CEA6C8 /* NSMutableSet+RSCore.h in Headers */,
|
||||
84C632A0200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.h in Headers */,
|
||||
84CFF5121AC3C6D800CEA6C8 /* RSBlocks.h in Headers */,
|
||||
84CFF56D1AC3D20A00CEA6C8 /* NSImage+RSCore.h in Headers */,
|
||||
84CFF5471AC3CD8000CEA6C8 /* NSTimer+RSCore.h in Headers */,
|
||||
|
@ -555,6 +571,7 @@
|
|||
84CFF52F1AC3CB1900CEA6C8 /* NSDate+RSCore.h in Headers */,
|
||||
8414CBAB1C95F8F700333C12 /* RSGeometry.h in Headers */,
|
||||
845DE0F31B80477100D1571B /* NSSet+RSCore.h in Headers */,
|
||||
84C632A4200D356E007BEEAA /* SendToBlogEditorApp.h in Headers */,
|
||||
844C915B1B65753E0051FC1B /* RSPlist.h in Headers */,
|
||||
8453F7DE1BDF337800B1C8ED /* RSMacroProcessor.h in Headers */,
|
||||
8415CB8A1BF84D24007B1E98 /* NSEvent+RSCore.h in Headers */,
|
||||
|
@ -748,6 +765,7 @@
|
|||
84CFF5171AC3C73000CEA6C8 /* RSConstants.m in Sources */,
|
||||
8432B1881DACA2060057D6DF /* NSWindow-Extensions.swift in Sources */,
|
||||
8402047E1FBCE77900D94C1A /* BatchUpdate.swift in Sources */,
|
||||
84C632A5200D356E007BEEAA /* SendToBlogEditorApp.m in Sources */,
|
||||
84CFF5541AC3CF4700CEA6C8 /* NSColor+RSCore.m in Sources */,
|
||||
84536F671BB856D4001E1639 /* NSFileManager+RSCore.m in Sources */,
|
||||
8415CB8B1BF84D24007B1E98 /* NSEvent+RSCore.m in Sources */,
|
||||
|
@ -769,6 +787,7 @@
|
|||
84C687351FBC025600345C9E /* Log.swift in Sources */,
|
||||
84CFF5301AC3CB1900CEA6C8 /* NSDate+RSCore.m in Sources */,
|
||||
84CFF5281AC3C9A200CEA6C8 /* NSArray+RSCore.m in Sources */,
|
||||
84C632A1200D30F1007BEEAA /* NSAppleEventDescriptor+RSCore.m in Sources */,
|
||||
84E72E171FBD647500B873C1 /* InspectorView.swift in Sources */,
|
||||
84CFF5591AC3CF9100CEA6C8 /* NSView+RSCore.m in Sources */,
|
||||
84CFF56A1AC3D1B000CEA6C8 /* RSScaling.m in Sources */,
|
||||
|
@ -794,6 +813,7 @@
|
|||
842E45CC1ED623C7000A8B52 /* UniqueIdentifier.swift in Sources */,
|
||||
84A8358A1D4EC7B80004C598 /* PlistProviderProtocol.swift in Sources */,
|
||||
849A339E1AC90A0A0015BA09 /* NSTableView+RSCore.m in Sources */,
|
||||
8434D15C200BD6F400D6281E /* UserApp.swift in Sources */,
|
||||
84CFF51B1AC3C77500CEA6C8 /* RSPlatform.m in Sources */,
|
||||
845A291F1FC8BC49007B49E3 /* BinaryDiskCache.swift in Sources */,
|
||||
84CFF52C1AC3CA9700CEA6C8 /* NSData+RSCore.m in Sources */,
|
||||
|
@ -931,7 +951,6 @@
|
|||
GCC_WARN_ABOUT_MISSING_NEWLINE = YES;
|
||||
GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES;
|
||||
GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;
|
||||
GCC_WARN_SHADOW = YES;
|
||||
GCC_WARN_SIGN_COMPARE = YES;
|
||||
|
@ -1003,7 +1022,6 @@
|
|||
GCC_WARN_ABOUT_MISSING_NEWLINE = YES;
|
||||
GCC_WARN_ABOUT_MISSING_PROTOTYPES = YES;
|
||||
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
|
||||
GCC_WARN_FOUR_CHARACTER_CONSTANTS = YES;
|
||||
GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES;
|
||||
GCC_WARN_SHADOW = YES;
|
||||
GCC_WARN_SIGN_COMPARE = YES;
|
||||
|
|
|
@ -0,0 +1,19 @@
|
|||
//
|
||||
// NSAppleEventDescriptor+RSCore.h
|
||||
// RSCore
|
||||
//
|
||||
// Created by Brent Simmons on 1/15/18.
|
||||
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Cocoa;
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface NSAppleEventDescriptor (RSCore)
|
||||
|
||||
+ (NSAppleEventDescriptor * _Nullable)descriptorWithRunningApplication:(NSRunningApplication *)runningApplication;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
|
@ -0,0 +1,23 @@
|
|||
//
|
||||
// NSAppleEventDescriptor+RSCore.m
|
||||
// RSCore
|
||||
//
|
||||
// Created by Brent Simmons on 1/15/18.
|
||||
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "NSAppleEventDescriptor+RSCore.h"
|
||||
|
||||
@implementation NSAppleEventDescriptor (RSCore)
|
||||
|
||||
+ (NSAppleEventDescriptor * _Nullable)descriptorWithRunningApplication:(NSRunningApplication *)runningApplication {
|
||||
|
||||
pid_t processIdentifier = runningApplication.processIdentifier;
|
||||
if (processIdentifier == -1) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
return [NSAppleEventDescriptor descriptorWithProcessIdentifier:processIdentifier];
|
||||
}
|
||||
|
||||
@end
|
|
@ -0,0 +1,28 @@
|
|||
//
|
||||
// SendToBlogEditorApp.h
|
||||
// RSCore
|
||||
//
|
||||
// Created by Brent Simmons on 1/15/18.
|
||||
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
@import Cocoa;
|
||||
|
||||
// This is for sending articles to MarsEdit and other apps that implement the send-to-blog-editor Apple events API:
|
||||
// http://ranchero.com/netnewswire/developers/externalinterface
|
||||
//
|
||||
// The first parameter is a target descriptor. The easiest way to get this is probably UserApp.targetDescriptor or +[NSAppleEventDescriptor descriptorWithRunningApplication:].
|
||||
// This does not care of launching the app in the first place. See UserApp.swift.
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
@interface SendToBlogEditorApp : NSObject
|
||||
|
||||
- (instancetype)initWithTargetDesciptor:(NSAppleEventDescriptor *)targetDescriptor title:(NSString * _Nullable)title body:(NSString * _Nullable)body summary:(NSString * _Nullable)summary link:(NSString * _Nullable)link permalink:(NSString * _Nullable)permalink subject:(NSString * _Nullable)subject creator:(NSString * _Nullable)creator commentsURL:(NSString * _Nullable)commentsURL guid:(NSString * _Nullable)guid sourceName:(NSString * _Nullable)sourceName sourceHomeURL:(NSString * _Nullable)sourceHomeURL sourceFeedURL:(NSString * _Nullable)sourceFeedURL;
|
||||
|
||||
- (OSStatus)send; // Actually send the Apple event.
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
//
|
||||
// SendToBlogEditorApp.m
|
||||
// RSCore
|
||||
//
|
||||
// Created by Brent Simmons on 1/15/18.
|
||||
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
#import "SendToBlogEditorApp.h"
|
||||
|
||||
@interface SendToBlogEditorApp()
|
||||
|
||||
@property (nonatomic, readonly) NSAppleEventDescriptor *targetDescriptor;
|
||||
|
||||
@property (nonatomic, nullable, readonly) NSString *title;
|
||||
@property (nonatomic, nullable, readonly) NSString *body;
|
||||
@property (nonatomic, nullable, readonly) NSString *summary;
|
||||
@property (nonatomic, nullable, readonly) NSString *link;
|
||||
@property (nonatomic, nullable, readonly) NSString *permalink;
|
||||
@property (nonatomic, nullable, readonly) NSString *subject;
|
||||
@property (nonatomic, nullable, readonly) NSString *creator;
|
||||
@property (nonatomic, nullable, readonly) NSString *commentsURL;
|
||||
@property (nonatomic, nullable, readonly) NSString *guid;
|
||||
@property (nonatomic, nullable, readonly) NSString *sourceName;
|
||||
@property (nonatomic, nullable, readonly) NSString *sourceHomeURL;
|
||||
@property (nonatomic, nullable, readonly) NSString *sourceFeedURL;
|
||||
|
||||
@end
|
||||
|
||||
@implementation SendToBlogEditorApp
|
||||
|
||||
- (instancetype)initWithTargetDesciptor:(NSAppleEventDescriptor *)targetDescriptor title:(NSString * _Nullable)title body:(NSString * _Nullable)body summary:(NSString * _Nullable)summary link:(NSString * _Nullable)link permalink:(NSString * _Nullable)permalink subject:(NSString * _Nullable)subject creator:(NSString * _Nullable)creator commentsURL:(NSString * _Nullable)commentsURL guid:(NSString * _Nullable)guid sourceName:(NSString * _Nullable)sourceName sourceHomeURL:(NSString * _Nullable)sourceHomeURL sourceFeedURL:(NSString * _Nullable)sourceFeedURL {
|
||||
|
||||
self = [super init];
|
||||
if (!self) {
|
||||
return nil;
|
||||
}
|
||||
|
||||
_targetDescriptor = targetDescriptor;
|
||||
_title = title;
|
||||
_body = body;
|
||||
_summary = summary;
|
||||
_link = link;
|
||||
_permalink = permalink;
|
||||
_subject = subject;
|
||||
_creator = creator;
|
||||
_commentsURL = commentsURL;
|
||||
_guid = guid;
|
||||
_sourceName = sourceName;
|
||||
_sourceHomeURL = sourceHomeURL;
|
||||
_sourceFeedURL = sourceFeedURL;
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
const AEKeyword EditDataItemAppleEventClass = 'EBlg';
|
||||
const AEKeyword EditDataItemAppleEventID = 'oitm';
|
||||
const AEKeyword DataItemTitle = 'titl';
|
||||
const AEKeyword DataItemDescription = 'desc';
|
||||
const AEKeyword DataItemSummary = 'summ';
|
||||
const AEKeyword DataItemLink = 'link';
|
||||
const AEKeyword DataItemPermalink = 'plnk';
|
||||
const AEKeyword DataItemSubject = 'subj';
|
||||
const AEKeyword DataItemCreator = 'crtr';
|
||||
const AEKeyword DataItemCommentsURL = 'curl';
|
||||
const AEKeyword DataItemGUID = 'guid';
|
||||
const AEKeyword DataItemSourceName = 'snam';
|
||||
const AEKeyword DataItemSourceHomeURL = 'hurl';
|
||||
const AEKeyword DataItemSourceFeedURL = 'furl';
|
||||
|
||||
- (OSStatus)send {
|
||||
|
||||
NSAppleEventDescriptor *appleEvent = [NSAppleEventDescriptor appleEventWithEventClass:EditDataItemAppleEventClass eventID:EditDataItemAppleEventID targetDescriptor:self.targetDescriptor returnID:kAutoGenerateReturnID transactionID:kAnyTransactionID];
|
||||
|
||||
[appleEvent setParamDescriptor:[self paramDescriptor] forKeyword:keyDirectObject];
|
||||
|
||||
return AESendMessage((const AppleEvent *)[appleEvent aeDesc], NULL, kAENoReply | kAECanSwitchLayer | kAEAlwaysInteract, kAEDefaultTimeout);
|
||||
}
|
||||
|
||||
#pragma mark - Private
|
||||
|
||||
- (NSAppleEventDescriptor *)paramDescriptor {
|
||||
|
||||
NSAppleEventDescriptor *descriptor = [NSAppleEventDescriptor recordDescriptor];
|
||||
|
||||
[self addToDescriptor:descriptor value:self.title keyword:DataItemTitle];
|
||||
[self addToDescriptor:descriptor value:self.body keyword:DataItemDescription];
|
||||
[self addToDescriptor:descriptor value:self.summary keyword:DataItemSummary];
|
||||
[self addToDescriptor:descriptor value:self.link keyword:DataItemLink];
|
||||
[self addToDescriptor:descriptor value:self.permalink keyword:DataItemPermalink];
|
||||
[self addToDescriptor:descriptor value:self.subject keyword:DataItemSubject];
|
||||
[self addToDescriptor:descriptor value:self.creator keyword:DataItemCreator];
|
||||
[self addToDescriptor:descriptor value:self.commentsURL keyword:DataItemCommentsURL];
|
||||
[self addToDescriptor:descriptor value:self.guid keyword:DataItemGUID];
|
||||
[self addToDescriptor:descriptor value:self.sourceName keyword:DataItemSourceName];
|
||||
[self addToDescriptor:descriptor value:self.sourceHomeURL keyword:DataItemSourceHomeURL];
|
||||
[self addToDescriptor:descriptor value:self.sourceFeedURL keyword:DataItemSourceFeedURL];
|
||||
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
- (void)addToDescriptor:(NSAppleEventDescriptor *)descriptor value:(NSString *)value keyword:(AEKeyword)keyword {
|
||||
|
||||
if (!value) {
|
||||
return;
|
||||
}
|
||||
|
||||
NSAppleEventDescriptor *stringDescriptor = [NSAppleEventDescriptor descriptorWithString:value];
|
||||
[descriptor setDescriptor:stringDescriptor forKeyword:keyword];
|
||||
}
|
||||
|
||||
|
||||
@end
|
|
@ -0,0 +1,135 @@
|
|||
//
|
||||
// UserApp.swift
|
||||
// RSCore
|
||||
//
|
||||
// Created by Brent Simmons on 1/14/18.
|
||||
// Copyright © 2018 Ranchero Software, LLC. All rights reserved.
|
||||
//
|
||||
|
||||
import Cocoa
|
||||
|
||||
// Represents an app (the type of app mostly found in /Applications.)
|
||||
// The app may or may not be running. It may or may not exist.
|
||||
|
||||
public final class UserApp {
|
||||
|
||||
public let bundleID: String
|
||||
public var icon: NSImage? = nil
|
||||
public var existsOnDisk = false
|
||||
public var path: String? = nil
|
||||
public var runningApplication: NSRunningApplication? = nil
|
||||
|
||||
public var isRunning: Bool {
|
||||
|
||||
updateStatus()
|
||||
if let runningApplication = runningApplication {
|
||||
return !runningApplication.isTerminated
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
public init(bundleID: String) {
|
||||
|
||||
self.bundleID = bundleID
|
||||
updateStatus()
|
||||
}
|
||||
|
||||
public func updateStatus() {
|
||||
|
||||
if let runningApplication = runningApplication, runningApplication.isTerminated {
|
||||
self.runningApplication = nil
|
||||
}
|
||||
|
||||
let runningApplications = NSRunningApplication.runningApplications(withBundleIdentifier: bundleID)
|
||||
for app in runningApplications {
|
||||
if let runningApplication = runningApplication {
|
||||
if app == runningApplication {
|
||||
break
|
||||
}
|
||||
}
|
||||
else {
|
||||
if !app.isTerminated {
|
||||
runningApplication = app
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let runningApplication = runningApplication {
|
||||
existsOnDisk = true
|
||||
icon = runningApplication.icon
|
||||
if let bundleURL = runningApplication.bundleURL {
|
||||
path = bundleURL.path
|
||||
}
|
||||
else {
|
||||
path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID)
|
||||
}
|
||||
if icon == nil, let path = path {
|
||||
icon = NSWorkspace.shared.icon(forFile: path)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
path = NSWorkspace.shared.absolutePathForApplication(withBundleIdentifier: bundleID)
|
||||
if let path = path {
|
||||
if icon == nil {
|
||||
icon = NSWorkspace.shared.icon(forFile: path)
|
||||
}
|
||||
existsOnDisk = true
|
||||
}
|
||||
else {
|
||||
existsOnDisk = false
|
||||
icon = nil
|
||||
}
|
||||
}
|
||||
|
||||
public func launchIfNeeded() -> Bool {
|
||||
|
||||
// Return true if already running.
|
||||
// Return true if not running and successfully gets launched.
|
||||
|
||||
updateStatus()
|
||||
if isRunning {
|
||||
return true
|
||||
}
|
||||
|
||||
guard existsOnDisk, let path = path else {
|
||||
return false
|
||||
}
|
||||
|
||||
let url = URL(fileURLWithPath: path)
|
||||
if let app = try? NSWorkspace.shared.launchApplication(at: url, options: [.withErrorPresentation], configuration: [:]) {
|
||||
runningApplication = app
|
||||
if app.isFinishedLaunching {
|
||||
return true
|
||||
}
|
||||
Thread.sleep(forTimeInterval: 1.0) // Give the app time to launch. This is ugly.
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
public func bringToFront() -> Bool {
|
||||
|
||||
// Activates the app, ignoring other apps.
|
||||
// Does not automatically launch the app first.
|
||||
|
||||
updateStatus()
|
||||
return runningApplication?.activate(options: [.activateIgnoringOtherApps]) ?? false
|
||||
}
|
||||
|
||||
public func targetDescriptor() -> NSAppleEventDescriptor? {
|
||||
|
||||
// Requires that the app has previously been launched.
|
||||
|
||||
updateStatus()
|
||||
guard let runningApplication = runningApplication, !runningApplication.isTerminated else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return NSAppleEventDescriptor(runningApplication: runningApplication)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -52,6 +52,9 @@
|
|||
|
||||
#import <RSCore/RSGeometry.h>
|
||||
|
||||
#import <RSCore/NSAppleEventDescriptor+RSCore.h>
|
||||
#import <RSCore/SendToBlogEditorApp.h>
|
||||
|
||||
#endif
|
||||
|
||||
|
||||
|
|
|
@ -7,6 +7,9 @@
|
|||
objects = {
|
||||
|
||||
/* Begin PBXBuildFile section */
|
||||
8409DB2C200AE4D700CE879E /* Dictionary+RSWeb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8409DB2B200AE4D700CE879E /* Dictionary+RSWeb.swift */; };
|
||||
8409DB2E200AE74400CE879E /* DictionaryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8409DB2D200AE74400CE879E /* DictionaryTests.swift */; };
|
||||
8409DB30200AE81400CE879E /* String+RSWeb.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8409DB2F200AE81400CE879E /* String+RSWeb.swift */; };
|
||||
84245C5A1FDC690A0074AFBB /* WebServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C591FDC690A0074AFBB /* WebServiceProvider.swift */; };
|
||||
84245C5B1FDC690A0074AFBB /* WebServiceProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C591FDC690A0074AFBB /* WebServiceProvider.swift */; };
|
||||
84245C5D1FDC697A0074AFBB /* Credentials.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C5C1FDC697A0074AFBB /* Credentials.swift */; };
|
||||
|
@ -15,6 +18,7 @@
|
|||
84245C611FDC69F20074AFBB /* APICall.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C5F1FDC69F20074AFBB /* APICall.swift */; };
|
||||
84245C6F1FDDCD8C0074AFBB /* HTTPResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C6E1FDDCD8C0074AFBB /* HTTPResult.swift */; };
|
||||
84245C701FDDCD8C0074AFBB /* HTTPResult.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84245C6E1FDDCD8C0074AFBB /* HTTPResult.swift */; };
|
||||
84261183200AE918004D89DD /* StringTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 84261182200AE918004D89DD /* StringTests.swift */; };
|
||||
842ED2E71E12FB8A000CF738 /* HTTPRequestHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842ED2E61E12FB8A000CF738 /* HTTPRequestHeader.swift */; };
|
||||
842ED2E81E12FB8A000CF738 /* HTTPRequestHeader.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842ED2E61E12FB8A000CF738 /* HTTPRequestHeader.swift */; };
|
||||
842ED2EA1E12FB91000CF738 /* HTTPMethod.swift in Sources */ = {isa = PBXBuildFile; fileRef = 842ED2E91E12FB91000CF738 /* HTTPMethod.swift */; };
|
||||
|
@ -61,10 +65,14 @@
|
|||
/* End PBXContainerItemProxy section */
|
||||
|
||||
/* Begin PBXFileReference section */
|
||||
8409DB2B200AE4D700CE879E /* Dictionary+RSWeb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "Dictionary+RSWeb.swift"; path = "RSWeb/Dictionary+RSWeb.swift"; sourceTree = "<group>"; };
|
||||
8409DB2D200AE74400CE879E /* DictionaryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DictionaryTests.swift; sourceTree = "<group>"; };
|
||||
8409DB2F200AE81400CE879E /* String+RSWeb.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = "String+RSWeb.swift"; path = "RSWeb/String+RSWeb.swift"; sourceTree = "<group>"; };
|
||||
84245C591FDC690A0074AFBB /* WebServiceProvider.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WebServiceProvider.swift; sourceTree = "<group>"; };
|
||||
84245C5C1FDC697A0074AFBB /* Credentials.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = Credentials.swift; path = RSWeb/Credentials.swift; sourceTree = "<group>"; };
|
||||
84245C5F1FDC69F20074AFBB /* APICall.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = APICall.swift; sourceTree = "<group>"; };
|
||||
84245C6E1FDDCD8C0074AFBB /* HTTPResult.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = HTTPResult.swift; path = RSWeb/HTTPResult.swift; sourceTree = "<group>"; };
|
||||
84261182200AE918004D89DD /* StringTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StringTests.swift; sourceTree = "<group>"; };
|
||||
842ED2E61E12FB8A000CF738 /* HTTPRequestHeader.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPRequestHeader.swift; path = RSWeb/HTTPRequestHeader.swift; sourceTree = "<group>"; };
|
||||
842ED2E91E12FB91000CF738 /* HTTPMethod.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPMethod.swift; path = RSWeb/HTTPMethod.swift; sourceTree = "<group>"; };
|
||||
842ED2EC1E12FB97000CF738 /* HTTPResponseCode.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = HTTPResponseCode.swift; path = RSWeb/HTTPResponseCode.swift; sourceTree = "<group>"; };
|
||||
|
@ -143,6 +151,8 @@
|
|||
842ED2D41E11FE8B000CF738 /* Constants */,
|
||||
849C09231E0CAD67006B03FA /* Downloading */,
|
||||
842ED30A1E12FBD8000CF738 /* URL+RSWeb.swift */,
|
||||
8409DB2B200AE4D700CE879E /* Dictionary+RSWeb.swift */,
|
||||
8409DB2F200AE81400CE879E /* String+RSWeb.swift */,
|
||||
842ED3131E12FBE7000CF738 /* MimeType.swift */,
|
||||
842ED3101E12FBE1000CF738 /* MacWebBrowser.swift */,
|
||||
84245C5C1FDC697A0074AFBB /* Credentials.swift */,
|
||||
|
@ -177,6 +187,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
849C08C41E0CAC86006B03FA /* RSWebTests.swift */,
|
||||
8409DB2D200AE74400CE879E /* DictionaryTests.swift */,
|
||||
84261182200AE918004D89DD /* StringTests.swift */,
|
||||
849C08C61E0CAC86006B03FA /* Info.plist */,
|
||||
);
|
||||
path = RSWebTests;
|
||||
|
@ -367,6 +379,7 @@
|
|||
842ED3081E12FBD2000CF738 /* URLRequest+RSWeb.swift in Sources */,
|
||||
842ED3051E12FBCC000CF738 /* NSMutableURLRequest+RSWeb.swift in Sources */,
|
||||
842ED2E71E12FB8A000CF738 /* HTTPRequestHeader.swift in Sources */,
|
||||
8409DB30200AE81400CE879E /* String+RSWeb.swift in Sources */,
|
||||
842ED3111E12FBE1000CF738 /* MacWebBrowser.swift in Sources */,
|
||||
842ED3141E12FBE7000CF738 /* MimeType.swift in Sources */,
|
||||
84245C5D1FDC697A0074AFBB /* Credentials.swift in Sources */,
|
||||
|
@ -376,6 +389,7 @@
|
|||
842ED2F91E12FBB5000CF738 /* DownloadProgress.swift in Sources */,
|
||||
842ED2EA1E12FB91000CF738 /* HTTPMethod.swift in Sources */,
|
||||
842ED3021E12FBC7000CF738 /* HTTPConditionalGetInfo.swift in Sources */,
|
||||
8409DB2C200AE4D700CE879E /* Dictionary+RSWeb.swift in Sources */,
|
||||
842ED2ED1E12FB97000CF738 /* HTTPResponseCode.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -385,6 +399,8 @@
|
|||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
849C08C51E0CAC86006B03FA /* RSWebTests.swift in Sources */,
|
||||
84261183200AE918004D89DD /* StringTests.swift in Sources */,
|
||||
8409DB2E200AE74400CE879E /* DictionaryTests.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
//
|
||||
// Dictionary+RSWeb.swift
|
||||
// RSWeb
|
||||
//
|
||||
// Created by Brent Simmons on 1/13/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension Dictionary {
|
||||
|
||||
public func urlQueryString() -> String? {
|
||||
|
||||
// Turn a dictionary into string like foo=bar¶m2=some%20thing
|
||||
// Return nil if empty dictionary.
|
||||
|
||||
if isEmpty {
|
||||
return nil
|
||||
}
|
||||
|
||||
var s = ""
|
||||
var numberAdded = 0
|
||||
for (key, value) in self {
|
||||
|
||||
guard let key = key as? String, let value = value as? String else {
|
||||
continue
|
||||
}
|
||||
guard let encodedKey = key.encodedForURLQuery(), let encodedValue = value.encodedForURLQuery() else {
|
||||
continue
|
||||
}
|
||||
|
||||
if numberAdded > 0 {
|
||||
s += "&"
|
||||
}
|
||||
s += "\(encodedKey)=\(encodedValue)"
|
||||
numberAdded += 1
|
||||
}
|
||||
|
||||
if numberAdded < 1 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
//
|
||||
// String+RSWeb.swift
|
||||
// RSWeb
|
||||
//
|
||||
// Created by Brent Simmons on 1/13/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
public extension String {
|
||||
|
||||
public func encodedForURLQuery() -> String? {
|
||||
|
||||
guard let encodedString = addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {
|
||||
return nil
|
||||
}
|
||||
return encodedString.replacingOccurrences(of: "&", with: "%38")
|
||||
}
|
||||
}
|
|
@ -0,0 +1,45 @@
|
|||
//
|
||||
// DictionaryTests.swift
|
||||
// RSWebTests
|
||||
//
|
||||
// Created by Brent Simmons on 1/13/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class DictionaryTests: XCTestCase {
|
||||
|
||||
func testSimpleQueryString() {
|
||||
|
||||
let d = ["foo": "bar", "param1": "This is a value."]
|
||||
let s = d.urlQueryString()
|
||||
|
||||
XCTAssertTrue(s == "foo=bar¶m1=This%20is%20a%20value." || s == "param1=This%20is%20a%20value.&foo=bar")
|
||||
}
|
||||
|
||||
func testQueryStringWithAmpersand() {
|
||||
|
||||
let d = ["fo&o": "bar", "param1": "This is a&value."]
|
||||
let s = d.urlQueryString()
|
||||
|
||||
XCTAssertTrue(s == "fo%38o=bar¶m1=This%20is%20a%38value." || s == "param1=This%20is%20a%38value.&fo%38o=bar")
|
||||
}
|
||||
|
||||
func testQueryStringWithAccentedCharacters() {
|
||||
|
||||
let d = ["fée": "bør"]
|
||||
let s = d.urlQueryString()
|
||||
|
||||
XCTAssertTrue(s == "f%C3%A9e=b%C3%B8r")
|
||||
}
|
||||
|
||||
func testQueryStringWithEmoji() {
|
||||
|
||||
let d = ["🌴e": "bar🎩🌴"]
|
||||
let s = d.urlQueryString()
|
||||
|
||||
XCTAssertTrue(s == "%F0%9F%8C%B4e=bar%F0%9F%8E%A9%F0%9F%8C%B4")
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
//
|
||||
// StringTests.swift
|
||||
// RSWebTests
|
||||
//
|
||||
// Created by Brent Simmons on 1/13/18.
|
||||
// Copyright © 2018 Ranchero Software. All rights reserved.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
|
||||
class StringTests: XCTestCase {
|
||||
|
||||
func testURLQueryEncoding() {
|
||||
|
||||
var s = "foo".encodedForURLQuery()
|
||||
XCTAssertEqual(s, "foo")
|
||||
|
||||
s = "foo bar".encodedForURLQuery()
|
||||
XCTAssertEqual(s, "foo%20bar")
|
||||
|
||||
s = "foo bar &well".encodedForURLQuery()
|
||||
XCTAssertEqual(s, "foo%20bar%20%38well")
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue