diff --git a/src/background/main.js b/src/background/main.js index 1aa9971b..823976aa 100644 --- a/src/background/main.js +++ b/src/background/main.js @@ -1,3 +1,4 @@ +import {openExtensionPage} from "../shared/utils.js"; // import * as settingsStorage from "../shared/settings.js"; // first sorts files by run-at value, then by weight value @@ -458,3 +459,16 @@ browser.runtime.onMessage.addListener(handleMessage); browser.tabs.onActivated.addListener(setBadgeCount); browser.windows.onFocusChanged.addListener(setBadgeCount); browser.webNavigation.onCompleted.addListener(setBadgeCount); + +// handle native app messages +const port = browser.runtime.connectNative(); +port.onMessage.addListener(message => { + // console.info(message); // DEBUG + if (message.name === "SAVE_LOCATION_CHANGED") { + openExtensionPage(); + if (message?.userInfo?.returnApp === true) browser.runtime.sendNativeMessage({name: "OPEN_APP"}); + } + // if (message.name === "OPEN_EXTENSION_PAGE") { + // openExtensionPage(); + // } +}); diff --git a/src/page/App.svelte b/src/page/App.svelte index dc40a37b..62e46a66 100644 --- a/src/page/App.svelte +++ b/src/page/App.svelte @@ -56,6 +56,15 @@ items.set(files); state.remove("items-loading"); }); + + // handle native app messages + const port = browser.runtime.connectNative(); + port.onMessage.addListener(message => { + // console.info(message); // DEBUG + if (message.name === "SAVE_LOCATION_CHANGED") { + window.location.reload(); + } + }); diff --git a/src/page/Components/Settings.svelte b/src/page/Components/Settings.svelte index db099910..75e64086 100644 --- a/src/page/Components/Settings.svelte +++ b/src/page/Components/Settings.svelte @@ -55,15 +55,7 @@ // called when the user clicks the icon next to the save location link async function changeSaveLocation() { - const m = "Changing the save location requires all instances of the extension to be closed and the host application to be opened. This will be automatically attempted.\n\nDo you wish to continue?"; - if (!window.confirm(m)) return; - window.open("userscriptsurlscheme://changesavelocation"); - // close all open extension pages - const url = window.location.href; - const close = []; - const tabs = await browser.tabs.query({}); - tabs.forEach(tab => tab.url === url && close.push(tab.id)); - if (close.length > 0) browser.tabs.remove(close); + browser.runtime.sendNativeMessage({name: "CHANGE_SAVE_LOCATION"}); } diff --git a/src/popup/App.svelte b/src/popup/App.svelte index c8850b4a..085394cd 100644 --- a/src/popup/App.svelte +++ b/src/popup/App.svelte @@ -12,9 +12,9 @@ import iconUpdate from "../shared/img/icon-update.svg?raw"; import iconClear from "../shared/img/icon-clear.svg?raw"; import iconRefresh from "../shared/img/icon-refresh.svg?raw"; + import {extensionPaths, openExtensionPage} from "../shared/utils.js"; import * as settingsStorage from "../shared/settings.js"; - const extensionPageUrl = browser.runtime.getURL("dist/entry-page.html"); let errorNotification; let active = true; let loading = true; @@ -149,19 +149,6 @@ initialize(); } - async function openExtensionPage() { - const tabs = await browser.tabs.query({}); - for (let i = 0; i < tabs.length; i++) { - if (tabs[i].url === extensionPageUrl) { - await browser.windows.update(tabs[i].windowId, {focused: true}); - await browser.tabs.update(tabs[i].id, {active: true}); - window.close(); - return; - } - } - await browser.tabs.create({url: extensionPageUrl}); - } - async function shouldCheckForUpdates() { // if there's no network connectivity, do not check for updates if (!window || !window.navigator || !window.navigator.onLine) { @@ -281,7 +268,9 @@ disabled = false; return; } - if (url === extensionPageUrl) { + // strip fragments and query params + const strippedUrl = url.split(/[?#]/)[0]; + if (strippedUrl === browser.runtime.getURL(extensionPaths.page)) { // disable popup on extension page inactive = true; loading = false; @@ -343,8 +332,6 @@ } // check if current page url is a userscript - // strip fragments and query params - const strippedUrl = url.split(/[?#]/)[0]; if (strippedUrl.endsWith(".user.js")) { // if it does, send message to content script // context script will check the document contentType @@ -464,6 +451,15 @@ // run resize again for good measure resize(); }); + + // handle native app messages + const port = browser.runtime.connectNative(); + port.onMessage.addListener(message => { + // console.info(message); // DEBUG + if (message.name === "SAVE_LOCATION_CHANGED") { + window.location.reload(); + } + }); diff --git a/src/shared/dev.js b/src/shared/dev.js index 7f3b6a44..5a2c0eae 100644 --- a/src/shared/dev.js +++ b/src/shared/dev.js @@ -367,7 +367,12 @@ const _browser = { }); } setTimeout(() => responseCallback(response), _browser.delay); - } + }, + connectNative: () => ({ + onMessage: { + addListener: () => console.info("connectNative - addListener") + } + }) }, tabs: { getCurrent(/* responseCallback */) { diff --git a/src/shared/utils.js b/src/shared/utils.js index 3d38f81c..9f73ff56 100644 --- a/src/shared/utils.js +++ b/src/shared/utils.js @@ -176,3 +176,21 @@ export const validMetaKeys = new Set([ "version", "weight" ]); + +export const extensionPaths = { + page: "/dist/entry-page.html", + popup: "/dist/entry-popup.html" +}; + +export async function openExtensionPage() { + const extensionPageUrl = browser.runtime.getURL(extensionPaths.page); + const tabs = await browser.tabs.query({}); + for (let i = 0; i < tabs.length; i++) { + if (tabs[i].url === extensionPageUrl) { + await browser.windows.update(tabs[i].windowId, {focused: true}); + await browser.tabs.update(tabs[i].id, {active: true}); + return; + } + } + await browser.tabs.create({url: extensionPageUrl}); +} diff --git a/xcode/Shared.swift b/xcode/Shared.swift index 94604ed8..14aa2c9a 100644 --- a/xcode/Shared.swift +++ b/xcode/Shared.swift @@ -2,6 +2,8 @@ import Foundation import SafariServices import os +let extensionIdentifier = "com.userscripts.macos.Userscripts-Extension" + struct SharedDefaults { // https://developer.apple.com/documentation/bundleresources/entitlements/com_apple_security_application-groups #if os(iOS) diff --git a/xcode/Userscripts Extension/SafariWebExtensionHandler.swift b/xcode/Userscripts Extension/SafariWebExtensionHandler.swift index 1e79cce8..e2820b00 100644 --- a/xcode/Userscripts Extension/SafariWebExtensionHandler.swift +++ b/xcode/Userscripts Extension/SafariWebExtensionHandler.swift @@ -17,10 +17,16 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { // these if/else if statement are formatted so that they can be neatly collapsed in Xcode // typically the "else if" would be on the same line as the preceding statements close bracket // ie. } else if { - if name == "REQ_PLATFORM" { + if name == "OPEN_APP" { + if let url = URL(string: "userscriptsurlscheme://") { + NSWorkspace.shared.open(url) + } + } + else if name == "REQ_PLATFORM" { let platform = getPlatform() response.userInfo = [SFExtensionMessageKey: ["platform": platform]] - } else if name == "REQ_USERSCRIPTS" { + } + else if name == "REQ_USERSCRIPTS" { if let url = message?["url"] as? String, let isTop = message?["isTop"] as? Bool { if let matches = getInjectionFilenames(url), @@ -162,6 +168,13 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { } #endif } + else if name == "CHANGE_SAVE_LOCATION" { + #if os(macOS) + if let url = URL(string: "userscriptsurlscheme://changesavelocation") { + NSWorkspace.shared.open(url) + } + #endif + } else if name == "USERSCRIPT_INSTALL_00" { if let content = message?["content"] as? String, diff --git a/xcode/Userscripts.xcodeproj/project.pbxproj b/xcode/Userscripts.xcodeproj/project.pbxproj index 3a6e6b37..0865ec4c 100644 --- a/xcode/Userscripts.xcodeproj/project.pbxproj +++ b/xcode/Userscripts.xcodeproj/project.pbxproj @@ -7,6 +7,8 @@ objects = { /* Begin PBXBuildFile section */ + 0336619F294DF7C900CFE179 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0336619E294DF7C900CFE179 /* Functions.swift */; }; + 033661A529510B7900CFE179 /* View.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 033661A329510B7900CFE179 /* View.storyboard */; }; 03E20D06291E170000B4F692 /* dist in Resources */ = {isa = PBXBuildFile; fileRef = 03E20D05291E170000B4F692 /* dist */; }; 03E20D07291E170000B4F692 /* dist in Resources */ = {isa = PBXBuildFile; fileRef = 03E20D05291E170000B4F692 /* dist */; }; 4A143AAC279DE6FF0029BFD0 /* UserscriptsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A143AAB279DE6FF0029BFD0 /* UserscriptsTests.swift */; }; @@ -95,6 +97,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 0336619E294DF7C900CFE179 /* Functions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Functions.swift; sourceTree = ""; }; + 033661A429510B7900CFE179 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/View.storyboard; sourceTree = ""; }; 03E20D05291E170000B4F692 /* dist */ = {isa = PBXFileReference; lastKnownFileType = folder; path = dist; sourceTree = ""; }; 4A143AA9279DE6FF0029BFD0 /* UserscriptsTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = UserscriptsTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4A143AAB279DE6FF0029BFD0 /* UserscriptsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserscriptsTests.swift; sourceTree = ""; }; @@ -247,8 +251,10 @@ 4A57B9F1227235CD008A9763 /* AppDelegate.swift */, 4A57B9F3227235CD008A9763 /* Main.storyboard */, 4A57B9F6227235CD008A9763 /* ViewController.swift */, + 033661A329510B7900CFE179 /* View.storyboard */, 4A57B9F8227235CE008A9763 /* Assets.xcassets */, 4A57B9FA227235CE008A9763 /* Info.plist */, + 0336619E294DF7C900CFE179 /* Functions.swift */, ); path = Userscripts; sourceTree = ""; @@ -470,6 +476,7 @@ buildActionMask = 2147483647; files = ( 4A57B9F9227235CE008A9763 /* Assets.xcassets in Resources */, + 033661A529510B7900CFE179 /* View.storyboard in Resources */, 4A57B9F5227235CD008A9763 /* Main.storyboard in Resources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -527,6 +534,7 @@ 4A6E19EE268CC84A00E0270C /* Shared.swift in Sources */, 4A57B9F7227235CD008A9763 /* ViewController.swift in Sources */, 4A57B9F2227235CD008A9763 /* AppDelegate.swift in Sources */, + 0336619F294DF7C900CFE179 /* Functions.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -561,6 +569,14 @@ /* End PBXTargetDependency section */ /* Begin PBXVariantGroup section */ + 033661A329510B7900CFE179 /* View.storyboard */ = { + isa = PBXVariantGroup; + children = ( + 033661A429510B7900CFE179 /* Base */, + ); + name = View.storyboard; + sourceTree = ""; + }; 4A4CF6E0270A38BD00111584 /* Main.html */ = { isa = PBXVariantGroup; children = ( diff --git a/xcode/Userscripts/AppDelegate.swift b/xcode/Userscripts/AppDelegate.swift index fcb7fca5..23e8a127 100644 --- a/xcode/Userscripts/AppDelegate.swift +++ b/xcode/Userscripts/AppDelegate.swift @@ -3,8 +3,40 @@ import Cocoa @NSApplicationMain class AppDelegate: NSObject, NSApplicationDelegate { + private var window: NSWindow! + private var windowForego = false + private var windowLoaded = false + + func application(_ application: NSApplication, open urls: [URL]) { + // if open panel is already open, stop processing the URL scheme + if NSApplication.shared.keyWindow?.accessibilityIdentifier() == "open-panel" { return } + for url in urls { + if url.host == "changesavelocation" { + // avoid opening the panel repeatedly and playing unnecessary warning sounds + if NSApplication.shared.keyWindow?.identifier?.rawValue == "changeSaveLocation" { continue } + if windowLoaded { + let viewController = window.contentViewController as? ViewController + viewController?.changeSaveLocation(nil) + } else { + windowForego = true + schemeChangeSaveLocation() + } + } + } + } + func applicationDidFinishLaunching(_ aNotification: Notification) { - // Insert code here to initialize your application + if windowForego { return } + let storyboard = NSStoryboard(name: "View", bundle: Bundle.main) + let windowController = storyboard.instantiateInitialController() as! NSWindowController +// let viewController = windowController.contentViewController as! ViewController + window = windowController.window + window.setIsVisible(true) + windowLoaded = true + } + + func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool { + return true } func applicationWillTerminate(_ aNotification: Notification) { diff --git a/xcode/Userscripts/Base.lproj/Main.storyboard b/xcode/Userscripts/Base.lproj/Main.storyboard index 559aff50..ed7ecf33 100644 --- a/xcode/Userscripts/Base.lproj/Main.storyboard +++ b/xcode/Userscripts/Base.lproj/Main.storyboard @@ -1,9 +1,8 @@ - + - - + @@ -43,177 +42,5 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/xcode/Userscripts/Base.lproj/View.storyboard b/xcode/Userscripts/Base.lproj/View.storyboard new file mode 100644 index 00000000..aeedc83c --- /dev/null +++ b/xcode/Userscripts/Base.lproj/View.storyboard @@ -0,0 +1,218 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/xcode/Userscripts/Functions.swift b/xcode/Userscripts/Functions.swift new file mode 100644 index 00000000..b22226ee --- /dev/null +++ b/xcode/Userscripts/Functions.swift @@ -0,0 +1,95 @@ +import AppKit +import SafariServices + +func getSaveLocationURL() -> URL { + var url: URL + // default url + if #available(macOS 13.0, *) { + url = getDocumentsDirectory().appending(path: "scripts") + } else { + url = getDocumentsDirectory().appendingPathComponent("scripts") + } + // if not in safari extension environment, replace with extension path + if let bundleIdentifier = Bundle.main.bundleIdentifier, bundleIdentifier != extensionIdentifier { + let s = url.absoluteString.replacingOccurrences(of: bundleIdentifier, with: extensionIdentifier) + // avoid being encode again, decode first + if let decodePath = s.removingPercentEncoding { + url = URL(fileURLWithPath: decodePath, isDirectory: true) + } + } + // bookmark url + if let data = UserDefaults(suiteName: SharedDefaults.suiteName)?.data(forKey: SharedDefaults.keyName) { + if let bookmarkURL = readBookmark(data: data, isSecure: false) { + if directoryExists(path: bookmarkURL.path) { + url = bookmarkURL + } else { + UserDefaults(suiteName: SharedDefaults.suiteName)?.removeObject(forKey: SharedDefaults.keyName) + NSLog("removed shared bookmark because it's directory is non-existent, permanently deleted or in trash") + } + } + } + return url +} + +func setSaveLocationURL(url: URL) -> Bool { + guard FileManager.default.isWritableFile(atPath: url.path) else { + let alert = NSAlert() + alert.messageText = "Can not write to path. Choose a different path." + alert.runModal() + return false + } + guard saveBookmark(url: url, isShared: true, keyName: SharedDefaults.keyName, isSecure: false) else { + err("couldn't save new location from host app") + return false + } + return true +} + +func sendExtensionMessage(name: String, userInfo: [String : Any]? = nil, completion: ((Error?) -> Void)? = nil) { + SFSafariApplication.dispatchMessage( + withName: name, + toExtensionWithIdentifier: extensionIdentifier, + userInfo: userInfo + ) { error in // always be called + if error != nil { + debugPrint("Message attempted. Error info: \(String.init(describing: error))") + } + if let userHandle = completion { userHandle(error) } + } +} + +func changeSaveLocationPanel(directoryURL: URL? = nil) -> NSOpenPanel { + let panel = NSOpenPanel() + panel.allowsMultipleSelection = false + panel.canChooseDirectories = true + panel.canCreateDirectories = true + panel.canChooseFiles = false + if directoryURL != nil { + panel.directoryURL = directoryURL + } + panel.title = "Change Save Location - Userscripts" + panel.identifier = NSUserInterfaceItemIdentifier("changeSaveLocation") + return panel +} + +func schemeChangeSaveLocation() { + let saveLocationURL = getSaveLocationURL() + let panel = changeSaveLocationPanel(directoryURL: saveLocationURL) + // shows the path selection panel + let response = panel.runModal() + // check if clicked open button and there is a valid result + guard response == .OK, let url: URL = panel.urls.first else { return } + // check if path has indeed changed + if url.absoluteString == saveLocationURL.absoluteString { return } + // try set new save location path to bookmark + guard setSaveLocationURL(url: url) else { return } + // use semaphore to ensure the async func executed before app exits + let semaphore = DispatchSemaphore(value: 0) + // notify browser extension of relevant updates + sendExtensionMessage( + name: "SAVE_LOCATION_CHANGED", + userInfo: ["saveLocation": url.absoluteString.removingPercentEncoding ?? url.absoluteString], + completion: { _ in semaphore.signal() } + ) + semaphore.wait() +} diff --git a/xcode/Userscripts/ViewController.swift b/xcode/Userscripts/ViewController.swift index a23be7c9..89292951 100644 --- a/xcode/Userscripts/ViewController.swift +++ b/xcode/Userscripts/ViewController.swift @@ -10,45 +10,20 @@ class ViewController: NSViewController { let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" - let hostID = Bundle.main.bundleIdentifier! - let foo = Bundle.main.bundleIdentifier - let extensionID = "com.userscripts.macos.Userscripts-Extension" - let documentsDirectory = getDocumentsDirectory().appendingPathComponent("scripts").absoluteString + let extensionID = extensionIdentifier override func viewDidLoad() { super.viewDidLoad() - let location = documentsDirectory.replacingOccurrences(of: hostID, with: extensionID) self.appName.stringValue = "Userscripts Safari Version \(appVersion) (\(buildNumber))" setExtensionState() NotificationCenter.default.addObserver( - self, selector: #selector(setExtensionState), name: NSApplication.didBecomeActiveNotification, object: nil + self, + selector: #selector(setExtensionState), + name: NSApplication.didBecomeActiveNotification, + object: nil ) - // set the save location url to default location - self.saveLocation.stringValue = location - self.saveLocation.toolTip = location - // check if bookmark data exists - guard - let sharedBookmark = UserDefaults(suiteName: SharedDefaults.suiteName)?.data(forKey: SharedDefaults.keyName) - else { - // bookmark data doesn't exist, no need to update url - return - } - // at this point it's known bookmark data does exist, try to read it - guard let url = readBookmark(data: sharedBookmark, isSecure: false) else { - // bookmark data does exist, but it can not be read, log an error - err("shared bookmark data exists, but it can not be read") - return - } - // shared bookmark data does exist and it can be read, check if the directory where it leads to exists - guard directoryExists(path: url.path) else { - // sharedBookmark removed, or in trash - // renamed directories retain association - // moved directories retain association - UserDefaults(suiteName: SharedDefaults.suiteName)?.removeObject(forKey: SharedDefaults.keyName) - NSLog("removed shared bookmark because it's directory is non-existent, permanently deleted or in trash") - return - } - // shared bookmark can be read and directory exists, update url + // set the save location url display + let url = getSaveLocationURL() self.saveLocation.stringValue = url.absoluteString self.saveLocation.toolTip = url.absoluteString } @@ -67,31 +42,28 @@ class ViewController: NSViewController { } } - @IBAction func changeSaveLocation(_ sender: NSButton) { + @IBAction func changeSaveLocation(_ sender: AnyObject?) { guard let window = self.view.window else { return } - let panel = NSOpenPanel() - panel.allowsMultipleSelection = false - panel.canChooseDirectories = true - panel.canCreateDirectories = true - panel.canChooseFiles = false + let saveLocationURL = getSaveLocationURL() + let panel = changeSaveLocationPanel(directoryURL: saveLocationURL) panel.beginSheetModal(for: window, completionHandler: { response in - if let url: URL = panel.urls.first { - // check it is a writeable path - let canWrite = FileManager.default.isWritableFile(atPath: url.path) - if !canWrite { - // display error message - let alert = NSAlert() - alert.messageText = "Can not write to path. Choose a different path." - alert.runModal() - } else { - if !saveBookmark(url: url, isShared: true, keyName: SharedDefaults.keyName, isSecure: false) { - err("couldn't save new location from host app") - return - } - self.saveLocation.stringValue = url.absoluteString - self.saveLocation.toolTip = url.absoluteString - } - } + // check if clicked open button and there is a valid result + guard response == .OK, let url: URL = panel.urls.first else { return } + // check if path has indeed changed + if url.absoluteString == saveLocationURL.absoluteString { return } + // try set new save location path to bookmark + guard setSaveLocationURL(url: url) else { return } + // update user interface text display + self.saveLocation.stringValue = url.absoluteString + self.saveLocation.toolTip = url.absoluteString + // notify browser extension of relevant updates + sendExtensionMessage( + name: "SAVE_LOCATION_CHANGED", + userInfo: [ + "saveLocation": url.absoluteString.removingPercentEncoding ?? url.absoluteString, + "returnApp": true + ] + ) }) }