diff --git a/README.md b/README.md index 4ae9b024..dd835171 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ To run Userscripts on macOS you should running macOS 12 or higher, along with Sa **[App Store Link](https://itunes.apple.com/us/app/userscripts/id1463298887)** +**[Development Progress](https://github.com/quoid/userscripts/projects/3)** + ## Usage It's recommend to read this documentation and, if you have time, watch the following video overviews to familiarize yourself with the app and extension. @@ -168,7 +170,7 @@ Userscripts Safari currently supports the following userscript metadata: ## API -Userscripts currently supports the following api methods. All methods are asynchronous unless otherwise noted. **All methods are accessible without regard to `@grant` when `@inject-into` has the `content` value.** Further, most methods do not require their respective prefix when calling. For example, `GM.setValue` can be called as `GM.setValue(...)` or with simply by `setValue(...)`. +Userscripts currently supports the following api methods. All methods are asynchronous unless otherwise noted. **All methods are accessible without regard to `@grant` when `@inject-into` has the `content` value.**. - `GM.addStyle(css)` - `css: String` @@ -195,6 +197,36 @@ Userscripts currently supports the following api methods. All methods are asynch - `tabId: Int` - `tabId` is **optional** and if omitted the tab that called `US.closeTab` will be closed - on success returns a promise resolved with an object indicating success +- `GM.setClipboard(data, type)` + - `data: String` - **required** + - `type: String` - **optional** and defaults to `text/plain` + - [read more here](https://developer.mozilla.org/en-US/docs/Web/API/ClipboardEvent/clipboardData) + - on success returns a promise resolved with a `Bool` indicating success +- `GM_setClipboard(data, type)` + - "synchronous' version of `GM.setClipboard` + - the setClipboard function runs in the background script, requires a promise to send message from content script to background to facilitate writing to the clipboard, thus no real synchronous function available + - returns `undefined` +- `GM.info` && `GM_info` + - is available without needing to add it to `@grant` + - an object containing information about the running userscript + - `scriptHandler: String` - returns `Userscripts` + - `version: String` - the version of Userscripts app + - `scriptMetaStr: String` - the metablock for the currently running script + - `script: Object` - contains data about the currently running script + - `description: String` + - `exclude-match: [String]` + - `excludes: [String]` + - `grant: [String]` + - `includes: [String]` + - `inject-into: String` + - `matches: [String]` + - `name: String` + - `namespace: String` + - `noframes: Bool` + - `require: [String]` + - `resources: [String]` - *currently not implemented* + - `run-at: String` + - `version: String` - *the userscript version value* - `GM.xmlHttpRequest(details)` - `details: Object` - the `details` object accepts the following properties diff --git a/etc/app_ios/src/css/_main.css b/etc/app_ios/src/css/_main.css index d64503c5..e50249d1 100644 --- a/etc/app_ios/src/css/_main.css +++ b/etc/app_ios/src/css/_main.css @@ -98,7 +98,7 @@ a { height: 1.5rem; } -.logo span { +.logo > span { margin-left: 0.5rem; } diff --git a/etc/app_ios/src/index.html b/etc/app_ios/src/index.html index dcb82adf..c444e72b 100644 --- a/etc/app_ios/src/index.html +++ b/etc/app_ios/src/index.html @@ -13,7 +13,7 @@ Userscripts App Icon

You can turn on the Userscripts iOS Safari extension in Settings. Read the docs.

diff --git a/etc/app_ios/src/js/scripts.js b/etc/app_ios/src/js/scripts.js index 9e7f7ede..a2be1252 100644 --- a/etc/app_ios/src/js/scripts.js +++ b/etc/app_ios/src/js/scripts.js @@ -1,7 +1,13 @@ const directory = document.querySelector("#directory"); const button = document.querySelector("#set_directory"); +const version = document.querySelector("#version"); +const build = document.querySelector("#build"); const setDirectory = () => webkit.messageHandlers.controller.postMessage("SET_READ_LOCATION"); function printDirectory(location) { directory.innerText = location; } +function printVersion(v, b) { + version.innerText = v; + build.innerText = b; +} button.addEventListener("click", setDirectory); diff --git a/etc/assets.sketch b/etc/assets.sketch index 7ea34c54..0d440f58 100644 Binary files a/etc/assets.sketch and b/etc/assets.sketch differ diff --git a/extension/Userscripts Extension/Functions.swift b/extension/Userscripts Extension/Functions.swift index cb818718..3b2e80a0 100644 --- a/extension/Userscripts Extension/Functions.swift +++ b/extension/Userscripts Extension/Functions.swift @@ -12,7 +12,7 @@ func dateToMilliseconds(_ date: Date) -> Int { return Int(since1970 * 1000) } -func sanitize(_ str: String) -> String? { +func sanitize(_ str: String) -> String { // removes invalid filename characters from strings var sanitized = str if sanitized.first == "." { @@ -163,11 +163,15 @@ func isVersionNewer(_ oldVersion: String, _ newVersion: String) -> Bool { return false } +func isEncoded(_ str: String) -> Bool { + return str.removingPercentEncoding != str +} + // parser func parse(_ content: String) -> [String: Any]? { // returns structured data from content of file // will fail to parse if metablock or required @name key missing - let pattern = #"(?:(\/\/ ==UserScript==\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)|(\/\* ==UserStyle==\r?\n([\S\s]*?)\r?\n==\/UserStyle== \*\/)([\S\s]*))"# + let pattern = #"(?:(\/\/ ==UserScript==[ \t]*?\r?\n([\S\s]*?)\r?\n\/\/ ==\/UserScript==)([\S\s]*)|(\/\* ==UserStyle==[ \t]*?\r?\n([\S\s]*?)\r?\n==\/UserStyle== \*\/)([\S\s]*))"# // force try b/c pattern is known to be valid regex let regex = try! NSRegularExpression(pattern: pattern, options: []) let range = NSRange(location: 0, length: content.utf16.count) @@ -202,7 +206,7 @@ func parse(_ content: String) -> [String: Any]? { // this pattern checks for specific keys that won't have values let p2 = #"^(?:[ \t]*(?:\/\/)?[ \t]*@)(noframes)[ \t]*$"# // the individual meta string, ie. // @name File Name - let metaString = String(meta) + let metaString = String(meta).trimmingCharacters(in: .whitespaces) // force try b/c pattern is known to be valid regex let re = try! NSRegularExpression(pattern: p, options: []) let re2 = try! NSRegularExpression(pattern: p2, options: []) @@ -234,7 +238,7 @@ func parse(_ content: String) -> [String: Any]? { return [ "code": trimmedCode, "content": content, - "metablock": metablock, + "metablock": String(metablock), "metadata": metadata ] } @@ -448,9 +452,8 @@ func updateManifestRequired(_ optionalFilesArray: [[String: Any]] = []) -> Bool // populate array with entries for manifest var r = [String]() for resource in required { - if let sanitizedResourceName = sanitize(resource) { - r.append(sanitizedResourceName) - } + let sanitizedResourceName = sanitize(resource) + r.append(sanitizedResourceName) } // if there are values, write them to manifest @@ -708,7 +711,7 @@ func getRequiredCode(_ filename: String, _ resources: [String], _ fileType: Stri } // skip urls pointing to files of different types if resourceUrlPath.hasSuffix(fileType) { - guard let resourceFilename = sanitize(resourceUrlString) else {return false} + let resourceFilename = sanitize(resourceUrlString) let fileURL = directory.appendingPathComponent(resourceFilename) // only attempt to get resource if it does not yet exist if FileManager.default.fileExists(atPath: fileURL.path) {continue} @@ -785,7 +788,27 @@ func getRemoteFileContents(_ url: String) -> String? { urlChecked = urlChecked.replacingOccurrences(of: "http:", with: "https:") logText("\(url) is using insecure http, attempt to fetch remote content with https") } - guard let solidURL = URL(string: urlChecked) else {return nil} + // if the url is already encoded, decode it + if isEncoded(urlChecked) { + if let decodedUrl = urlChecked.removingPercentEncoding { + urlChecked = decodedUrl + } else { + err("getRemoteFileContents failed at (1), couldn't decode url, \(url)") + return nil + } + } + // encode all urls strings + if let encodedUrl = urlChecked.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { + urlChecked = encodedUrl + } else { + err("getRemoteFileContents failed at (2), couldn't percent encode url, \(url)") + return nil + } + // convert url string to url + guard let solidURL = URL(string: urlChecked) else { + err("getRemoteFileContents failed at (3), couldn't convert string to url, \(url)") + return nil + } var contents = "" // get remote file contents, synchronously let semaphore = DispatchSemaphore(value: 0) @@ -806,7 +829,7 @@ func getRemoteFileContents(_ url: String) -> String? { // if made it to this point and contents still an empty string, something went wrong with the request if contents.count < 1 { - logText("something went wrong while trying to fetch remote file contents \(url)") + logText("getRemoteFileContents failed at (4), contents empty, \(url)") return nil } logText("getRemoteFileContents for \(url) end") @@ -1092,8 +1115,10 @@ func getMatchedFiles(_ url: String) -> [String] { } // injection -func getCode(_ filenames: [String], _ isTop: Bool)-> [String: [String: [String: Any]]]? { - var allFiles = [String: [String: [String: Any]]]() +// func getCode(_ filenames: [String], _ isTop: Bool)-> [String: [String: [String: Any]]]? { +func getCode(_ filenames: [String], _ isTop: Bool)-> [String: Any]? { + //var allFiles = [String: [String: [String: Any]]]() + var allFiles = [String: Any]() var cssFiles = [String:[String:String]]() var jsFiles = [String: [String: [String: [String: Any]]]]() jsFiles["auto"] = ["document-start": [:], "document-end": [:], "document-idle": [:]] @@ -1141,6 +1166,71 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: [String: [String: // normalize weight var weight = metadata["weight"]?[0] ?? "1" weight = normalizeWeight(weight) + + // get inject-into and run-at values + // if either is missing, use default value + var injectInto = metadata["inject-into"]?[0] ?? "auto" + var runAt = metadata["run-at"]?[0] ?? "document-end" + let injectVals = ["auto", "content", "page"] + let runAtVals = ["context-menu", "document-start", "document-end", "document-idle"] + // if either is invalid use default value + if !injectVals.contains(injectInto) { + injectInto = "auto" + } + if !runAtVals.contains(runAt) { + runAt = "document-end" + } + + // attempt to get all @grant value + var grants = metadata["grant"] ?? [] + // remove duplicates, if any exist + grants = Array(Set(grants)) + + // set GM.info data + let description = metadata["description"]?[0] ?? "" + let excludes = metadata["exclude"] ?? [] + let excludeMatches = metadata["exclude-match"] ?? [] + let includes = metadata["include"] ?? [] + let matches = metadata["match"] ?? [] + let requires = metadata["require"] ?? [] + let version = metadata["version"]?[0] ?? "" + let noframes = metadata["noframes"] != nil ? true : false + var scriptObject:[String: Any] = [ + "description": description, + "excludes": excludes, + "exclude-match": excludeMatches, + "grant": grants, + "includes": includes, + "inject-into": injectInto, + "matches": matches, + "name": name, + "noframes": noframes, + "namespace": "", + "resources": "", + "require": requires, + "run-at": runAt, + "version": version + ] + // certain metadata keys use a different key name then the actual key name + // for compatibility keeping this when applicable, although the rationale is not clear to me + // for unique keys passed to scriptObject, using the same key name that is present in actual userscript + // this key map is used to check for existence of keys in next loop + let keyMap = [ + "exclude": "excludes", + "include": "includes", + "match": "matches", + "resource": "resources", + ] + for metaline in metadata { + let key = keyMap[metaline.key] ?? metaline.key + if !scriptObject.keys.contains(key) { + let value = metaline.value + // metalines without values aren't included in parsed metadata object + // the only exception is @noframes + scriptObject[key] = value.count > 1 ? value : value[0] + } + } + let scriptMetaStr = contents["metablock"] as? String ?? "??" // attempt to get require resource from disk // if required resource is inaccessible, log error and continue @@ -1149,7 +1239,7 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: [String: [String: // if required is ["A", "B", "C"], C gets added above B which is above A, etc.. // the reverse of that is desired for require in required.reversed() { - let sanitizedName = sanitize(require) ?? "" + let sanitizedName = sanitize(require) let requiredFileURL = getRequireLocation().appendingPathComponent(filename).appendingPathComponent(sanitizedName) if let requiredContent = try? String(contentsOf: requiredFileURL, encoding: .utf8) { code = "\(requiredContent)\n\(code)" @@ -1159,28 +1249,15 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: [String: [String: } } - // attempt to get all @grant value - var grants = metadata["grant"] ?? [] - // remove duplicates, if any exist - grants = Array(Set(grants)) - if type == "css" { - cssFiles[filename] = ["code": code, "weight": weight] + cssFiles[filename] = ["code": code, "weight": weight, "name": name] } else if type == "js" { - var injectInto = metadata["inject-into"]?[0] ?? "auto" - var runAt = metadata["run-at"]?[0] ?? "document-end" - - let injectVals = ["auto", "content", "page"] - let runAtVals = ["context-menu", "document-start", "document-end", "document-idle"] - // if inject/runAt values are not valid, use default - if !injectVals.contains(injectInto) { - injectInto = "page" - } - if !runAtVals.contains(runAt) { - runAt = "document-end" - } - - let data = ["code": code, "weight": weight, "grant": grants] as [String : Any] + let data = [ + "code": code, + "weight": weight, + "scriptMetaStr": scriptMetaStr, + "scriptObject": scriptObject + ] as [String : Any] // add file data to appropriate dict if injectInto == "auto" && runAt == "document-start" { auto_docStart[filename] = data @@ -1201,15 +1278,14 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: [String: [String: } else if injectInto == "page" && runAt == "document-idle" { page_docIdle[filename] = data } - if runAt == "context-menu" && injectInto == "auto" { - auto_context_scripts[filename] = ["code": code, "name": name, "grant": grants] + auto_context_scripts[filename] = data } if runAt == "context-menu" && injectInto == "content" { - content_context_scripts[filename] = ["code": code, "name": name, "grant": grants] + content_context_scripts[filename] = data } if runAt == "context-menu" && injectInto == "page" { - page_context_scripts[filename] = ["code": code, "name": name, "grant": grants] + page_context_scripts[filename] = data } } } @@ -1233,7 +1309,13 @@ func getCode(_ filenames: [String], _ isTop: Bool)-> [String: [String: [String: // construct the returned dictionary allFiles["css"] = cssFiles allFiles["js"] = jsFiles - + + // add global values + let scriptHandler = "Userscripts" + let scriptHandlerVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" + allFiles["scriptHandler"] = scriptHandler + allFiles["scriptHandlerVersion"] = scriptHandlerVersion + return allFiles } @@ -1439,7 +1521,8 @@ func getInitData() -> [String: Any]? { var data:[String: Any] = manifest.settings data["blacklist"] = manifest.blacklist data["saveLocation"] = saveLocation.path - data["version"] = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String + data["version"] = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" + data["build"] = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" return data } @@ -1458,12 +1541,14 @@ func saveFile(_ item: [String: Any],_ content: String) -> [String: Any] { } guard let parsed = parse(newContent), - let metadata = parsed["metadata"] as? [String: [String]], - let n = metadata["name"]?[0], - var name = sanitize(n) + let metadata = parsed["metadata"] as? [String: [String]] else { - return ["error": "failed to parse argument in save function"] + return ["error": "failed to parse metadata"] + } + guard let n = metadata["name"]?[0] else { + return ["error": "@name not found in metadata"] } + var name = sanitize(n) // construct new file name let newFilename = "\(name).\(type)" @@ -1759,12 +1844,12 @@ func installUserscript(_ content: String) -> [String: Any]? { guard let parsed = parse(content), let metadata = parsed["metadata"] as? [String: [String]], - let n = metadata["name"]?[0], - let name = sanitize(n) + let n = metadata["name"]?[0] else { err("installUserscript failed at (1)") return nil } + let name = sanitize(n) let filename = "\(name).js" let saved = saveFile(["filename": filename, "type": "js"], content) diff --git a/extension/Userscripts Extension/Resources/background.js b/extension/Userscripts Extension/Resources/background.js index a8bcc9c1..fd1fb056 100644 --- a/extension/Userscripts Extension/Resources/background.js +++ b/extension/Userscripts Extension/Resources/background.js @@ -91,8 +91,12 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { } else if (name === "API_GET_VALUE") { const key = request.filename + "---" + request.key; browser.storage.local.get(key, item => { - if (Object.keys(item).length === 0 && request.defaultValue) { - sendResponse(request.defaultValue); + if (Object.keys(item).length === 0) { + if (request.defaultValue) { + sendResponse(request.defaultValue); + } else { + sendResponse(`undefined--${request.pid}`); + } } else { sendResponse(Object.values(item)[0]); } @@ -134,6 +138,9 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { } })(); return true; + } else if (name === "API_SET_CLIPBOARD") { + const result = setClipboard(request.data, request.type); + sendResponse(result); } else if (name === "API_XHR_CS") { // https://jsonplaceholder.typicode.com/posts // get tab id and respond only to the content script that sent message @@ -143,14 +150,21 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { const user = details.user || null; const password = details.password || null; let body = details.data || null; - if (body && details.binary) body = new Blob([body], {type: "text/plain"}); + if (body && details.binary) { + const len = body.length; + const arr = new Uint8Array(len); + for (let i = 0; i < len; i++) { + arr[i] = body.charCodeAt(i); + } + body = new Blob([arr], {type: "text/plain"}); + } const xhr = new XMLHttpRequest(); // push to global scoped array so it can be aborted xhrs.push({xhr: xhr, xhrId: request.xhrId}); xhr.withCredentials = (details.user && details.password); xhr.timeout = details.timeout || 0; if (details.overrideMimeType) xhr.overrideMimeType(details.overrideMimeType); - xhrAddListeners(xhr, tab, request.xhrId, details); + xhrAddListeners(xhr, tab, request.id, request.xhrId, details); xhr.open(method, details.url, true, user, password); xhr.responseType = details.responseType || ""; if (details.headers) { @@ -182,7 +196,7 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { } }); -function xhrHandleEvent(e, xhr, tab, xhrId) { +function xhrHandleEvent(e, xhr, tab, id, xhrId) { const name = `RESP_API_XHR_BG_${e.type.toUpperCase()}`; const x = { readyState: xhr.readyState, @@ -197,33 +211,54 @@ function xhrHandleEvent(e, xhr, tab, xhrId) { }; // only include responseText when applicable if (["", "text"].includes(xhr.responseType)) x.responseText = xhr.responseText; - browser.tabs.sendMessage(tab, {name: name, xhrId: xhrId, response: x}); + // convert data if response is arraybuffer so sendMessage can pass it + if (xhr.responseType === "arraybuffer") { + const arr = Array.from(new Uint8Array(xhr.response)); + x.response = arr; + } + // convert data if response is blob so sendMessage can pass it + if (xhr.responseType === "blob") { + const reader = new FileReader(); + reader.readAsDataURL(xhr.response); + reader.onloadend = function() { + const base64data = reader.result; + x.response = { + data: base64data, + type: xhr.response.type + }; + browser.tabs.sendMessage(tab, {name: name, id: id, xhrId: xhrId, response: x}); + }; + } + // blob response will execute its own sendMessage call + if (xhr.responseType !== "blob") { + browser.tabs.sendMessage(tab, {name: name, id: id, xhrId: xhrId, response: x}); + } } -function xhrAddListeners(xhr, tab, xhrId, details) { +function xhrAddListeners(xhr, tab, id, xhrId, details) { if (details.onabort) { - xhr.addEventListener("abort", e => xhrHandleEvent(e, xhr, tab, xhrId)); + xhr.addEventListener("abort", e => xhrHandleEvent(e, xhr, tab, id, xhrId)); } if (details.onerror) { - xhr.addEventListener("error", e => xhrHandleEvent(e, xhr, tab, xhrId)); + xhr.addEventListener("error", e => xhrHandleEvent(e, xhr, tab, id, xhrId)); } if (details.onload) { - xhr.addEventListener("load", e => xhrHandleEvent(e, xhr, tab, xhrId)); + xhr.addEventListener("load", e => xhrHandleEvent(e, xhr, tab, id, xhrId)); } if (details.onloadend) { - xhr.addEventListener("loadend", e => xhrHandleEvent(e, xhr, tab, xhrId)); + xhr.addEventListener("loadend", e => xhrHandleEvent(e, xhr, tab, id, xhrId)); } if (details.onloadstart) { - xhr.addEventListener("loadstart", e => xhrHandleEvent(e, xhr, tab, xhrId)); + xhr.addEventListener("loadstart", e => xhrHandleEvent(e, xhr, tab, id, xhrId)); } if (details.onprogress) { - xhr.addEventListener("progress", e => xhrHandleEvent(e, xhr, tab, xhrId)); + xhr.addEventListener("progress", e => xhrHandleEvent(e, xhr, tab, id, xhrId)); } if (details.onreadystatechange) { - xhr.addEventListener("readystatechange", e => xhrHandleEvent(e, xhr, tab, xhrId)); + xhr.addEventListener("readystatechange", e => xhrHandleEvent(e, xhr, tab, id, xhrId)); } if (details.ontimeout) { - xhr.addEventListener("timeout", e => xhrHandleEvent(e, xhr, tab, xhrId)); + xhr.addEventListener("timeout", e => xhrHandleEvent(e, xhr, tab, id, xhrId)); } } @@ -302,6 +337,33 @@ async function getPlatform() { return response.platform; } +function setClipboard(data, type = "text/plain") { + // future enhancement? + // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/write + // https://developer.mozilla.org/en-US/docs/Web/API/Clipboard/writeText + const onCopy = e => { + e.stopImmediatePropagation(); + e.preventDefault(); + e.clipboardData.setData(type, data); + document.removeEventListener("copy", onCopy, true); + }; + + const textarea = document.createElement("textarea"); + textarea.textContent = ""; + document.body.appendChild(textarea); + textarea.select(); + document.addEventListener("copy", onCopy, true); + try { + return document.execCommand("copy"); + } catch (error) { + console.warn("setClipboard failed", error); + document.removeEventListener("copy", onCopy, true); + return false; + } finally { + document.body.removeChild(textarea); + } +} + browser.tabs.onActivated.addListener(setBadgeCount); browser.windows.onFocusChanged.addListener(setBadgeCount); browser.webNavigation.onCompleted.addListener(setBadgeCount); diff --git a/extension/Userscripts Extension/Resources/content.js b/extension/Userscripts Extension/Resources/content.js index abccefc4..70298821 100644 --- a/extension/Userscripts Extension/Resources/content.js +++ b/extension/Userscripts Extension/Resources/content.js @@ -22,47 +22,76 @@ function sortByWeight(o) { return sorted; } -function injectCSS(filename, code) { +function injectCSS(name, code) { // there's no fallback if blocked by CSP // future fix?: https://wicg.github.io/construct-stylesheets/ - console.info(`Injecting ${filename}`); + console.info(`Injecting ${name} %c(css)`, "color: #60f36c"); const tag = document.createElement("style"); tag.textContent = code; document.head.appendChild(tag); } -function injectJS(filename, code, scope, grants) { - console.info(`Injecting ${filename}`); +function injectJS(filename, code, scope, timing, grants, fallback) { // include api methods - let api = ""; const gmVals = []; const usVals = []; const includedFunctions = []; - if (grants.length) api = `const uid = "${uid}";\nconst filename = "${filename}";`; + // when a csp violation occurs, the scope is set to "content", when previously it's "auto" + // if the scope isn't changed back to "auto" pre-injection, the scriptDataKey will be null + // and the fallback attempt will fail + // this will change back to "content" below + scope = fallback ? "auto" : scope; + let scriptDataKey; + if (timing === "context-menu") { + scriptDataKey = data.js["context-menu"][scope][filename]; + } else { + scriptDataKey = data.js[scope][timing][filename]; + } + console.info(`Injecting ${scriptDataKey.scriptObject.name} %c(js)`, "color: #e4f360"); + const scriptData = { + "script": scriptDataKey.scriptObject, + "scriptHandler": data.scriptHandler, + "scriptHandlerVersion": data.scriptHandlerVersion, + "scriptMetaStr": scriptDataKey.scriptMetaStr + }; + // change the scope back so it properly inject on fallback attempt + if (fallback) scope = "content"; + // TODO: use us_info instead of const variables + let api = `const uid = "${uid}";\nconst filename = "${filename}";`; + const us_info = {filename: filename, info: scriptData, uid: uid}; + api += `\nconst us_info = ${JSON.stringify(us_info)}`; + // all scripts get acces to GM.info and GM_info + api += "\nconst GM_info = us_info.info;\n"; + gmVals.push("info: us_info.info"); grants.forEach(grant => { if (grant === "GM.openInTab") { - api += `\n${openInTab}`; - gmVals.push("openInTab: openInTab"); + api += `\n${us_openInTab}`; + gmVals.push("openInTab: us_openInTab"); } else if (grant === "US.closeTab") { - api += `\n${closeTab}`; - usVals.push("closeTab: closeTab"); + api += `\n${us_closeTab}`; + usVals.push("closeTab: us_closeTab"); } else if (grant === "GM.setValue") { - api += `\n${setValue}`; - gmVals.push("setValue: setValue"); + api += `\n${us_setValue}`; + gmVals.push("setValue: us_setValue"); } else if (grant === "GM.getValue") { - api += `\n${getValue}`; - gmVals.push("getValue: getValue"); + api += `\n${us_getValue}`; + gmVals.push("getValue: us_getValue"); } else if (grant === "GM.deleteValue") { - api += `\n${deleteValue}`; - gmVals.push("deleteValue: deleteValue"); + api += `\n${us_deleteValue}`; + gmVals.push("deleteValue: us_deleteValue"); } else if (grant === "GM.listValues") { - api += `\n${listValues}`; - gmVals.push("listValues: listValues"); + api += `\n${us_listValues}`; + gmVals.push("listValues: us_listValues"); } else if (grant === "GM_addStyle") { - api += `\n${addStyleSync}\nconst GM_addStyle = addStyleSync;`; + api += `\n${us_addStyleSync}\nconst GM_addStyle = us_addStyleSync;`; } else if (grant === "GM.addStyle") { - api += `\n${addStyle}\n`; - gmVals.push("addStyle: addStyle"); + api += `\n${us_addStyle}\n`; + gmVals.push("addStyle: us_addStyle"); + } else if (grant === "GM.setClipboard") { + api += `\n${us_setClipboard}`; + gmVals.push("setClipboard: us_setClipboard"); + } else if (grant === "GM_setClipboard") { + api += `\n${us_setClipboardSync}\nconst GM_setClipboard = us_setClipboardSync;`; } else if (grant === "GM_xmlhttpRequest" || grant === "GM.xmlHttpRequest") { if (!includedFunctions.includes(xhr.name)) { api += `\n${xhr}`; @@ -82,39 +111,39 @@ function injectJS(filename, code, scope, grants) { if (scope !== "content") { const tag = document.createElement("script"); tag.textContent = code; - document.body.appendChild(tag); + document.head.appendChild(tag); } else { eval(code); } } -function processJS(filename, code, scope, timing, grants) { +function processJS(filename, code, scope, timing, grants, fallback) { // this is about to get ugly if (timing === "document-start") { if (document.readyState === "loading") { document.addEventListener("readystatechange", function() { if (document.readyState === "interactive") { - injectJS(filename, code, scope, grants); + injectJS(filename, code, scope, timing, grants, fallback); } }); } else { - injectJS(filename, code, scope, grants); + injectJS(filename, code, scope, timing, grants, fallback); } } else if (timing === "document-end") { if (document.readyState !== "loading") { - injectJS(filename, code, scope, grants); + injectJS(filename, code, scope, timing, grants, fallback); } else { document.addEventListener("DOMContentLoaded", function() { - injectJS(filename, code, scope, grants); + injectJS(filename, code, scope, timing, grants, fallback); }); } } else if (timing === "document-idle") { if (document.readyState === "complete") { - injectJS(filename, code, scope, grants); + injectJS(filename, code, scope, timing, grants, fallback); } else { document.addEventListener("readystatechange", function(e) { if (document.readyState === "complete") { - injectJS(filename, code, scope, grants); + injectJS(filename, code, scope, timing, grants, fallback); } }); } @@ -132,12 +161,13 @@ function parseCode(data, fallback = false) { sorted = sortByWeight(codeTypeObject); for (const filename in sorted) { const code = sorted[filename].code; + const name = sorted[filename].name; // css is only injected into the page scope after DOMContentLoaded event if (document.readyState !== "loading") { - injectCSS(filename, code); + injectCSS(name, code); } else { document.addEventListener("DOMContentLoaded", function() { - injectCSS(filename, code); + injectCSS(name, code); }); } } @@ -158,13 +188,13 @@ function parseCode(data, fallback = false) { sorted = sortByWeight(timingObject); for (const filename in sorted) { const code = sorted[filename].code; - const grants = sorted[filename].grant; + const grants = sorted[filename].scriptObject.grant; // when block by csp rules, auto scope script will auto retry injection if (fallback) { console.warn(`Attempting fallback injection for ${filename}`); scope = "content"; } - processJS(filename, code, scope, timing, grants); + processJS(filename, code, scope, timing, grants, fallback); } } }); @@ -208,7 +238,7 @@ async function processJSContextMenuItems() { for (const scope in contextMenuCodeObject) { const scopeObject = contextMenuCodeObject[scope]; for (const filename in scopeObject) { - const name = scopeObject[filename].name; + const name = scopeObject[filename].scriptObject.name; if (document.readyState === "complete") { addContextMenuItem(filename, name); } else { @@ -252,106 +282,133 @@ function addContextMenuItem(filename, name) { } // api - https://developer.chrome.com/docs/extensions/mv3/content_scripts/#host-page-communication -function openInTab(url, openInBackground) { +function us_openInTab(url, openInBackground) { + const pid = Math.random().toString(36).substring(1, 9); return new Promise(resolve => { const callback = e => { - if (e.data.id !== uid || e.data.name !== "RESP_OPEN_TAB") return; + if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_OPEN_TAB") return; resolve(e.data.response); window.removeEventListener("message", callback); }; window.addEventListener("message", callback); const active = (openInBackground === true) ? false : true; - window.postMessage({id: uid, name: "API_OPEN_TAB", url: url, active: active}); + window.postMessage({id: uid, pid: pid, name: "API_OPEN_TAB", url: url, active: active}); }); } -function closeTab(tabId) { +function us_closeTab(tabId) { + const pid = Math.random().toString(36).substring(1, 9); return new Promise(resolve => { const callback = e => { - if (e.data.id !== uid || e.data.name !== "RESP_CLOSE_TAB") return; + if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_CLOSE_TAB") return; resolve(e.data.response); window.removeEventListener("message", callback); }; window.addEventListener("message", callback); - window.postMessage({id: uid, name: "API_CLOSE_TAB", tabId: tabId}); + window.postMessage({id: uid, pid: pid, name: "API_CLOSE_TAB", tabId: tabId}); }); } -function setValue(key, value) { +function us_setValue(key, value) { + const pid = Math.random().toString(36).substring(1, 9); return new Promise(resolve => { const callback = e => { // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - if (e.data.id !== uid || e.data.name !== "RESP_SET_VALUE" || e.data.filename !== filename) return; + if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_SET_VALUE" || e.data.filename !== filename) return; resolve(e.data.response); window.removeEventListener("message", callback); }; window.addEventListener("message", callback); // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - window.postMessage({id: uid, name: "API_SET_VALUE", filename: filename, key: key, value: value}); + window.postMessage({id: uid, pid: pid, name: "API_SET_VALUE", filename: filename, key: key, value: value}); }); } -function getValue(key, defaultValue) { +function us_getValue(key, defaultValue) { + const pid = Math.random().toString(36).substring(1, 9); return new Promise(resolve => { const callback = e => { // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - if (e.data.id !== uid || e.data.name !== "RESP_GET_VALUE" || e.data.filename !== filename) return; - resolve(e.data.response); + if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_GET_VALUE" || e.data.filename !== filename) return; + const response = e.data.response; + resolve(response); window.removeEventListener("message", callback); }; window.addEventListener("message", callback); // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - window.postMessage({id: uid, name: "API_GET_VALUE", filename: filename, key: key, defaultValue: defaultValue}); + window.postMessage({id: uid, pid: pid, name: "API_GET_VALUE", filename: filename, key: key, defaultValue: defaultValue}); }); } -function listValues() { +function us_listValues() { + const pid = Math.random().toString(36).substring(1, 9); return new Promise(resolve => { const callback = e => { // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - if (e.data.id !== uid || e.data.name !== "RESP_LIST_VALUES" || e.data.filename !== filename) return; + if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_LIST_VALUES" || e.data.filename !== filename) return; resolve(e.data.response); window.removeEventListener("message", callback); }; window.addEventListener("message", callback); // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - window.postMessage({id: uid, name: "API_LIST_VALUES", filename: filename}); + window.postMessage({id: uid, pid: pid, name: "API_LIST_VALUES", filename: filename}); }); } -function deleteValue(key) { +function us_deleteValue(key) { + const pid = Math.random().toString(36).substring(1, 9); return new Promise(resolve => { const callback = e => { // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - if (e.data.id !== uid || e.data.name !== "RESP_DELETE_VALUE" || e.data.filename !== filename) return; + if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_DELETE_VALUE" || e.data.filename !== filename) return; resolve(e.data.response); window.removeEventListener("message", callback); }; window.addEventListener("message", callback); // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - window.postMessage({id: uid, name: "API_DELETE_VALUE", filename: filename, key: key}); + window.postMessage({id: uid, pid: pid, name: "API_DELETE_VALUE", filename: filename, key: key}); }); } -function addStyleSync(css) { +function us_addStyleSync(css) { window.postMessage({id: uid, name: "API_ADD_STYLE_SYNC", css: css}); return css; } -function addStyle(css) { +function us_addStyle(css) { + const pid = Math.random().toString(36).substring(1, 9); return new Promise(resolve => { const callback = e => { - // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - if (e.data.id !== uid || e.data.name !== "RESP_ADD_STYLE") return; + if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_ADD_STYLE") return; resolve(e.data.response); window.removeEventListener("message", callback); }; window.addEventListener("message", callback); - // eslint-disable-next-line no-undef -- filename var accessible to the function at runtime - window.postMessage({id: uid, name: "API_ADD_STYLE", css: css}); + window.postMessage({id: uid, pid: pid, name: "API_ADD_STYLE", css: css}); + }); +} + +function us_setClipboard(data, type) { + const pid = Math.random().toString(36).substring(1, 9); + return new Promise(resolve => { + const callback = e => { + if (e.data.pid !== pid || e.data.id !== uid || e.data.name !== "RESP_SET_CLIPBOARD") return; + resolve(e.data.response); + if (!e.data.response) console.error("clipboard write failed"); + window.removeEventListener("message", callback); + }; + window.addEventListener("message", callback); + window.postMessage({id: uid, pid: pid, name: "API_SET_CLIPBOARD", data: data, type: type}); }); } +function us_setClipboardSync(data, type) { + // there's actually no sync method since a promise needs to be sent to bg page + // however make a dummy sync method for compatibility + window.postMessage({id: uid, name: "API_SET_CLIPBOARD", data: data, type: type}); + return undefined; +} + // when xhr is called it sends a message to the content script // and adds it's own event listener to get responses from content script // each xhr has a unique id so it won't respond to different xhr @@ -383,9 +440,15 @@ function xhr(details) { const callback = e => { const name = e.data.name; const response = e.data.response; - if (!name.startsWith("RESP_API_XHR_CS") || e.data.xhrId !== xhrId) return; + // ensure callback is responding to the proper message + if ( + e.data.id !== uid + || e.data.xhrId !== xhrId + || !name + || !name.startsWith("RESP_API_XHR_CS") + ) return; if (name === "RESP_API_XHR_CS") { - // + // ignore } else if (name.includes("ABORT") && details.onabort) { details.onabort(response); } else if (name.includes("ERROR") && details.onerror) { @@ -428,14 +491,16 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { if (fn === filename) { // get code from object and send for injection along with filename & scope const code = contextMenuCodeObject[scope][filename].code; - const grants = contextMenuCodeObject[scope][filename].grant; + const grants = contextMenuCodeObject[scope][filename].scriptObject.grant; // if strict csp already detected change auto scoped scripts to content + let fallback = false; if (cspFallbackAttempted && scope === "auto") { console.warn(`Attempting fallback injection for ${filename}`); scope = "content"; + fallback = true; } scope = cspFallbackAttempted && scope === "auto" ? "content" : scope; - injectJS(filename, code, scope, grants); + injectJS(filename, code, scope, "context-menu", grants, fallback); found = true; break; } @@ -443,14 +508,49 @@ browser.runtime.onMessage.addListener((request, sender, sendResponse) => { if (found) break; } } else if (name.startsWith("RESP_API_XHR_BG_")) { + // only respond to messages on the correct content script + if (request.id !== uid) return; + const resp = request.response; const n = name.replace("_BG_", "_CS_"); - window.postMessage({name: n, response: request.response, xhrId: request.xhrId}); + // arraybuffer responses had their data converted, convert it back to arraybuffer + if (request.response.responseType === "arraybuffer" && resp.response) { + try { + const r = new Uint8Array(resp.response).buffer; + resp.response = r; + } catch (error) { + console.error("error parsing xhr arraybuffer response", error); + } + // blob responses had their data converted, convert it back to blob + } else if (request.response.responseType === "blob" && resp.response && resp.response.data) { + fetch(request.response.response.data) + .then(res => res.blob()) + .then(b => { + resp.response = b; + window.postMessage({name: n, response: resp, id: request.id, xhrId: request.xhrId}); + }); + } + // blob response will execute its own postMessage call + if (request.response.responseType !== "blob") { + window.postMessage({name: n, response: resp, id: request.id, xhrId: request.xhrId}); + } } else if (["USERSCRIPT_INSTALL_00", "USERSCRIPT_INSTALL_01", "USERSCRIPT_INSTALL_02"].includes(name)) { - const content = document.body.innerText; - browser.runtime.sendMessage({name: name, content: content}, response => { - sendResponse(response); - }); - return true; + const types = [ + "text/plain", + "application/ecmascript", + "application/javascript", + "text/ecmascript", + "text/javascript" + ]; + if (!document.contentType || types.indexOf(document.contentType) === -1) { + // only allow installation if contentType is in list above + sendResponse({invalid: true}); + } else { + const content = document.body.innerText; + browser.runtime.sendMessage({name: name, content: content}, response => { + sendResponse(response); + }); + return true; + } } }); @@ -460,43 +560,49 @@ window.addEventListener("message", e => { if (e.data.id !== uid || !e.data.name) return; const id = e.data.id; const name = e.data.name; + const pid = e.data.pid; let message; if (name === "API_OPEN_TAB") { // ignore requests that don't supply a url if (!e.data.url) return; message = {name: "API_OPEN_TAB", url: e.data.url, active: e.data.active}; browser.runtime.sendMessage(message, response => { - window.postMessage({id: id, name: "RESP_OPEN_TAB", response: response}); + window.postMessage({id: id, pid: pid, name: "RESP_OPEN_TAB", response: response}); }); } else if (name === "API_CLOSE_TAB") { browser.runtime.sendMessage({name: "API_CLOSE_TAB", tabId: e.data.tabId}, response => { - window.postMessage({id: id, name: "RESP_CLOSE_TAB", response: response}); + window.postMessage({id: id, pid: pid, name: "RESP_CLOSE_TAB", response: response}); + }); + } else if (name === "API_SET_CLIPBOARD") { + browser.runtime.sendMessage({name: "API_SET_CLIPBOARD", data: e.data.data, type: e.data.type}, response => { + window.postMessage({id: id, pid: pid, name: "RESP_SET_CLIPBOARD", response: response}); }); } else if (name === "API_SET_VALUE") { message = {name: "API_SET_VALUE", filename: e.data.filename, key: e.data.key, value: e.data.value}; browser.runtime.sendMessage(message, response => { - window.postMessage({id: id, name: "RESP_SET_VALUE", filename: e.data.filename, response: response}); + window.postMessage({id: id, pid: pid, name: "RESP_SET_VALUE", filename: e.data.filename, response: response}); }); } else if (name === "API_GET_VALUE") { - message = {name: "API_GET_VALUE", filename: e.data.filename, key: e.data.key, defaultValue: e.data.defaultValue}; + message = {name: "API_GET_VALUE", filename: e.data.filename, pid: pid, key: e.data.key, defaultValue: e.data.defaultValue}; browser.runtime.sendMessage(message, response => { - window.postMessage({id: id, name: "RESP_GET_VALUE", filename: e.data.filename, response: response}); + const resp = response === `undefined--${pid}` ? undefined : response; + window.postMessage({id: id, pid: pid, name: "RESP_GET_VALUE", filename: e.data.filename, response: resp}); }); } else if (name === "API_DELETE_VALUE") { message = {name: "API_DELETE_VALUE", filename: e.data.filename, key: e.data.key}; browser.runtime.sendMessage(message, response => { - window.postMessage({id: id, name: "RESP_DELETE_VALUE", filename: e.data.filename, response: response}); + window.postMessage({id: id, pid: pid, name: "RESP_DELETE_VALUE", filename: e.data.filename, response: response}); }); } else if (name === "API_LIST_VALUES") { message = {name: "API_LIST_VALUES", filename: e.data.filename}; browser.runtime.sendMessage(message, response => { - window.postMessage({id: id, name: "RESP_LIST_VALUES", filename: e.data.filename, response: response}); + window.postMessage({id: id, pid: pid, name: "RESP_LIST_VALUES", filename: e.data.filename, response: response}); }); } else if (name === "API_ADD_STYLE") { try { message = {name: "API_ADD_STYLE", css: e.data.css}; browser.runtime.sendMessage(message, response => { - window.postMessage({id: id, name: "RESP_ADD_STYLE", response: response}); + window.postMessage({id: id, pid: pid, name: "RESP_ADD_STYLE", response: response}); }); } catch (e) { console.log(e); @@ -509,7 +615,7 @@ window.addEventListener("message", e => { console.log(e); } } else if (name === "API_XHR_INJ") { - message = {name: "API_XHR_CS", details: e.data.details, xhrId: e.data.xhrId}; + message = {name: "API_XHR_CS", details: e.data.details, id: id, xhrId: e.data.xhrId}; browser.runtime.sendMessage(message, response => { window.postMessage({id: id, name: "RESP_API_XHR_CS", response: response, xhrId: e.data.xhrId}); }); diff --git a/extension/Userscripts Extension/Resources/manifest.json b/extension/Userscripts Extension/Resources/manifest.json index 10600bc0..65048ebf 100644 --- a/extension/Userscripts Extension/Resources/manifest.json +++ b/extension/Userscripts Extension/Resources/manifest.json @@ -4,7 +4,7 @@ "default_locale": "en", "name": "__MSG_extension_name__", "description": "__MSG_extension_description__", - "version": "4.0.11", + "version": "4.1.0", "icons": { "48": "images/icon-48.png", "96": "images/icon-96.png", @@ -33,6 +33,7 @@ ], "permissions": [ "", + "clipboardWrite", "contextMenus", "menus", "nativeMessaging", diff --git a/extension/Userscripts Extension/Resources/page.html b/extension/Userscripts Extension/Resources/page.html index b875c6a5..9e1ebc8e 100644 --- a/extension/Userscripts Extension/Resources/page.html +++ b/extension/Userscripts Extension/Resources/page.html @@ -1 +1 @@ -Userscripts Safari
\ No newline at end of file +Userscripts Safari
\ No newline at end of file diff --git a/extension/Userscripts Extension/Resources/page.js b/extension/Userscripts Extension/Resources/page.js index aee24dc9..d872fa3e 100644 --- a/extension/Userscripts Extension/Resources/page.js +++ b/extension/Userscripts Extension/Resources/page.js @@ -1982,8 +1982,42 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o /* src/page/Components/Sidebar/SidebarItem.svelte generated by Svelte v3.29.0 */ + function create_if_block$2(ctx) { + let div; + let t; + let div_title_value; + + return { + c() { + div = element("div"); + t = text(/*description*/ ctx[3]); + attr(div, "class", "item__description svelte-3ix3ls"); + + attr(div, "title", div_title_value = /*showTitle*/ ctx[2] + ? /*data*/ ctx[0].description + : null); + }, + m(target, anchor) { + insert(target, div, anchor); + append(div, t); + }, + p(ctx, dirty) { + if (dirty & /*description*/ 8) set_data(t, /*description*/ ctx[3]); + + if (dirty & /*showTitle, data*/ 5 && div_title_value !== (div_title_value = /*showTitle*/ ctx[2] + ? /*data*/ ctx[0].description + : null)) { + attr(div, "title", div_title_value); + } + }, + d(detaching) { + if (detaching) detach(div); + } + }; + } + function create_fragment$6(ctx) { - let div3; + let div2; let div1; let tag; let t0; @@ -1993,13 +2027,10 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o let t2; let toggle; let t3; - let div2; - let t4_value = (/*data*/ ctx[0].description || "No description provided") + ""; - let t4; - let div3_class_value; - let div3_data_filename_value; - let div3_data_last_modified_value; - let div3_data_type_value; + let div2_class_value; + let div2_data_filename_value; + let div2_data_last_modified_value; + let div2_data_type_value; let current; let mounted; let dispose; @@ -2013,9 +2044,11 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o if (is_function(/*toggleClick*/ ctx[1])) /*toggleClick*/ ctx[1].apply(this, arguments); }); + let if_block = /*description*/ ctx[3] && create_if_block$2(ctx); + return { c() { - div3 = element("div"); + div2 = element("div"); div1 = element("div"); create_component(tag.$$.fragment); t0 = space(); @@ -2024,35 +2057,32 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o t2 = space(); create_component(toggle.$$.fragment); t3 = space(); - div2 = element("div"); - t4 = text(t4_value); + if (if_block) if_block.c(); attr(div0, "class", "item__title truncate svelte-3ix3ls"); attr(div1, "class", "item__header svelte-3ix3ls"); - attr(div2, "class", "item__description svelte-3ix3ls"); - attr(div3, "class", div3_class_value = "item " + (/*data*/ ctx[0].class || "") + " svelte-3ix3ls"); - attr(div3, "data-filename", div3_data_filename_value = /*data*/ ctx[0].filename); - attr(div3, "data-last-modified", div3_data_last_modified_value = /*data*/ ctx[0].lastModified); - attr(div3, "data-type", div3_data_type_value = /*data*/ ctx[0].type); - toggle_class(div3, "active", /*data*/ ctx[0].active); - toggle_class(div3, "disabled", /*data*/ ctx[0].disabled); - toggle_class(div3, "temp", /*data*/ ctx[0].temp); + attr(div2, "class", div2_class_value = "item " + (/*data*/ ctx[0].class || "") + " svelte-3ix3ls"); + attr(div2, "data-filename", div2_data_filename_value = /*data*/ ctx[0].filename); + attr(div2, "data-last-modified", div2_data_last_modified_value = /*data*/ ctx[0].lastModified); + attr(div2, "data-type", div2_data_type_value = /*data*/ ctx[0].type); + toggle_class(div2, "active", /*data*/ ctx[0].active); + toggle_class(div2, "disabled", /*data*/ ctx[0].disabled); + toggle_class(div2, "temp", /*data*/ ctx[0].temp); }, m(target, anchor) { - insert(target, div3, anchor); - append(div3, div1); + insert(target, div2, anchor); + append(div2, div1); mount_component(tag, div1, null); append(div1, t0); append(div1, div0); append(div0, t1); append(div1, t2); mount_component(toggle, div1, null); - append(div3, t3); - append(div3, div2); - append(div2, t4); + append(div2, t3); + if (if_block) if_block.m(div2, null); current = true; if (!mounted) { - dispose = listen(div3, "click", /*click_handler*/ ctx[2]); + dispose = listen(div2, "click", /*click_handler*/ ctx[4]); mounted = true; } }, @@ -2065,34 +2095,46 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o const toggle_changes = {}; if (dirty & /*data*/ 1) toggle_changes.checked = !/*data*/ ctx[0].disabled; toggle.$set(toggle_changes); - if ((!current || dirty & /*data*/ 1) && t4_value !== (t4_value = (/*data*/ ctx[0].description || "No description provided") + "")) set_data(t4, t4_value); - if (!current || dirty & /*data*/ 1 && div3_class_value !== (div3_class_value = "item " + (/*data*/ ctx[0].class || "") + " svelte-3ix3ls")) { - attr(div3, "class", div3_class_value); + if (/*description*/ ctx[3]) { + if (if_block) { + if_block.p(ctx, dirty); + } else { + if_block = create_if_block$2(ctx); + if_block.c(); + if_block.m(div2, null); + } + } else if (if_block) { + if_block.d(1); + if_block = null; } - if (!current || dirty & /*data*/ 1 && div3_data_filename_value !== (div3_data_filename_value = /*data*/ ctx[0].filename)) { - attr(div3, "data-filename", div3_data_filename_value); + if (!current || dirty & /*data*/ 1 && div2_class_value !== (div2_class_value = "item " + (/*data*/ ctx[0].class || "") + " svelte-3ix3ls")) { + attr(div2, "class", div2_class_value); } - if (!current || dirty & /*data*/ 1 && div3_data_last_modified_value !== (div3_data_last_modified_value = /*data*/ ctx[0].lastModified)) { - attr(div3, "data-last-modified", div3_data_last_modified_value); + if (!current || dirty & /*data*/ 1 && div2_data_filename_value !== (div2_data_filename_value = /*data*/ ctx[0].filename)) { + attr(div2, "data-filename", div2_data_filename_value); } - if (!current || dirty & /*data*/ 1 && div3_data_type_value !== (div3_data_type_value = /*data*/ ctx[0].type)) { - attr(div3, "data-type", div3_data_type_value); + if (!current || dirty & /*data*/ 1 && div2_data_last_modified_value !== (div2_data_last_modified_value = /*data*/ ctx[0].lastModified)) { + attr(div2, "data-last-modified", div2_data_last_modified_value); + } + + if (!current || dirty & /*data*/ 1 && div2_data_type_value !== (div2_data_type_value = /*data*/ ctx[0].type)) { + attr(div2, "data-type", div2_data_type_value); } if (dirty & /*data, data*/ 1) { - toggle_class(div3, "active", /*data*/ ctx[0].active); + toggle_class(div2, "active", /*data*/ ctx[0].active); } if (dirty & /*data, data*/ 1) { - toggle_class(div3, "disabled", /*data*/ ctx[0].disabled); + toggle_class(div2, "disabled", /*data*/ ctx[0].disabled); } if (dirty & /*data, data*/ 1) { - toggle_class(div3, "temp", /*data*/ ctx[0].temp); + toggle_class(div2, "temp", /*data*/ ctx[0].temp); } }, i(local) { @@ -2107,9 +2149,10 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o current = false; }, d(detaching) { - if (detaching) detach(div3); + if (detaching) detach(div2); destroy_component(tag); destroy_component(toggle); + if (if_block) if_block.d(); mounted = false; dispose(); } @@ -2119,6 +2162,21 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o function instance$6($$self, $$props, $$invalidate) { let { data = {} } = $$props; let { toggleClick } = $$props; + let showTitle = false; + + // if description > 120 characters, truncate and add ellipsis + // also trim for instances where an empty space is the last character + // this prevents ellipsis from being added next to empty space + // when description truncated, add title attr with full description text + function formatDescription(str) { + if (str && str.length > 120) { + $$invalidate(2, showTitle = true); + return str.substring(0, 120).trim() + "..."; + } else { + $$invalidate(2, showTitle = false); + return str; + } + } function click_handler(event) { bubble($$self, event); @@ -2129,7 +2187,15 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o if ("toggleClick" in $$props) $$invalidate(1, toggleClick = $$props.toggleClick); }; - return [data, toggleClick, click_handler]; + let description; + + $$self.$$.update = () => { + if ($$self.$$.dirty & /*data*/ 1) { + $$invalidate(3, description = formatDescription(data.description)); + } + }; + + return [data, toggleClick, showTitle, description, click_handler]; } class SidebarItem extends SvelteComponent { @@ -16688,7 +16754,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o /* src/page/Components/Editor/EditorSearch.svelte generated by Svelte v3.29.0 */ - function create_if_block$2(ctx) { + function create_if_block$3(ctx) { let div; let input; let t0; @@ -16803,7 +16869,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o function create_fragment$7(ctx) { let if_block_anchor; let current; - let if_block = /*active*/ ctx[0] && create_if_block$2(ctx); + let if_block = /*active*/ ctx[0] && create_if_block$3(ctx); return { c() { @@ -16824,7 +16890,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o transition_in(if_block, 1); } } else { - if_block = create_if_block$2(ctx); + if_block = create_if_block$3(ctx); if_block.c(); transition_in(if_block, 1); if_block.m(if_block_anchor.parentNode, if_block_anchor); @@ -16879,11 +16945,14 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o let rangesIndex = 0; // next/prev marks text, store those marks here for later removal - let marks = []; + const marks = []; async function focusInput() { await tick(); inp.focus(); + + // if text entered, highlight it on focus + if (inp.value) inp.setSelectionRange(0, inp.value.length); } function keys(e) { @@ -16909,7 +16978,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o pattern.lastIndex = stream.pos; const match = pattern.exec(stream.string); - if (match && match.index == stream.pos) { + if (match && match.index === stream.pos) { stream.pos += match[0].length || 1; return "searching"; } else if (match) { @@ -16980,7 +17049,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o $$invalidate(6, --rangesIndex); } - let i = rangesIndex - 1; + const i = rangesIndex - 1; cm.setSelection(ranges[i].anchor, ranges[i].head); cm.scrollIntoView( @@ -17008,7 +17077,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o function input_input_handler() { inputValue = this.value; - (((($$invalidate(4, inputValue), $$invalidate(0, active)), $$invalidate(12, instance)), $$invalidate(18, searchOverlay)), $$invalidate(19, marks)); + ((($$invalidate(4, inputValue), $$invalidate(0, active)), $$invalidate(12, instance)), $$invalidate(18, searchOverlay)); } const click_handler = () => next(); @@ -17092,7 +17161,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o /* src/page/Components/Editor/CodeMirror.svelte generated by Svelte v3.29.0 */ - function create_if_block$3(ctx) { + function create_if_block$4(ctx) { let switch_instance; let switch_instance_anchor; let current; @@ -17179,7 +17248,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o let t; let if_block_anchor; let current; - let if_block = instance$7 && create_if_block$3(ctx); + let if_block = instance$7 && create_if_block$4(ctx); return { c() { @@ -17205,7 +17274,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o transition_in(if_block, 1); } } else { - if_block = create_if_block$3(ctx); + if_block = create_if_block$4(ctx); if_block.c(); transition_in(if_block, 1); if_block.m(if_block_anchor.parentNode, if_block_anchor); @@ -17751,7 +17820,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o } // (249:4) {#if showCount} - function create_if_block$4(ctx) { + function create_if_block$5(ctx) { let div; let t0_value = /*list*/ ctx[3].length + ""; let t0; @@ -17850,7 +17919,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o each_1_lookup.set(key, each_blocks[i] = create_each_block(key, child_ctx)); } - let if_block1 = /*showCount*/ ctx[0] && create_if_block$4(ctx); + let if_block1 = /*showCount*/ ctx[0] && create_if_block$5(ctx); return { c() { @@ -17959,7 +18028,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o transition_in(if_block1, 1); } } else { - if_block1 = create_if_block$4(ctx); + if_block1 = create_if_block$5(ctx); if_block1.c(); transition_in(if_block1, 1); if_block1.m(div3, null); @@ -18445,7 +18514,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o } // (279:20) {#if $state.includes("saving")} - function create_if_block$5(ctx) { + function create_if_block$6(ctx) { let t; return { @@ -18508,7 +18577,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o function select_block_type(ctx, dirty) { if (show_if == null || dirty & /*$state*/ 1024) show_if = !!/*$state*/ ctx[10].includes("saving"); - if (show_if) return create_if_block$5; + if (show_if) return create_if_block$6; if (show_if_1 == null || dirty & /*$state*/ 1024) show_if_1 = !!/*$state*/ ctx[10].includes("trashing"); if (show_if_1) return create_if_block_1$1; if (show_if_2 == null || dirty & /*$state*/ 1024) show_if_2 = !!/*$state*/ ctx[10].includes("updating"); @@ -18977,7 +19046,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o /* src/page/Components/Settings.svelte generated by Svelte v3.29.0 */ - function create_if_block$6(ctx) { + function create_if_block$7(ctx) { let html_tag; let html_anchor; @@ -19076,17 +19145,21 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o let t42; let t43_value = /*$settings*/ ctx[3].version + ""; let t43; + let t44; + let t45_value = /*$settings*/ ctx[3].build + ""; + let t45; + let t46; let br0; let br1; - let t44; + let t47; let a0; let br2; let br3; - let t46; + let t49; let a1; - let t48; + let t51; let a2; - let t50; + let t53; let div29_intro; let div29_outro; let div30_intro; @@ -19153,7 +19226,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o }); iconbutton1.$on("click", changeSaveLocation); - let if_block = /*blacklistSaving*/ ctx[1] && create_if_block$6(); + let if_block = /*blacklistSaving*/ ctx[1] && create_if_block$7(); return { c() { @@ -19247,20 +19320,23 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o p = element("p"); t42 = text("Userscripts Safari Version "); t43 = text(t43_value); + t44 = text(" ("); + t45 = text(t45_value); + t46 = text(")"); br0 = element("br"); br1 = element("br"); - t44 = text("You can review the documentation, report bugs and get more information about this extension by visiting "); + t47 = text("You can review the documentation, report bugs and get more information about this extension by visiting "); a0 = element("a"); a0.textContent = "the code repository."; br2 = element("br"); br3 = element("br"); - t46 = text("If you enjoy using this extension, please consider "); + t49 = text("If you enjoy using this extension, please consider "); a1 = element("a"); a1.textContent = "leaving a review"; - t48 = text(" on the App Store or "); + t51 = text(" on the App Store or "); a2 = element("a"); a2.textContent = "supporting the project"; - t50 = text("."); + t53 = text("."); attr(div0, "class", "svelte-1ibrghz"); attr(div1, "class", "modal__title svelte-1ibrghz"); attr(div2, "class", "svelte-1ibrghz"); @@ -19301,7 +19377,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o attr(div27, "class", "modal__title svelte-1ibrghz"); attr(a0, "href", "https://github.com/quoid/userscripts"); attr(a1, "href", "https://apps.apple.com/us/app/userscripts/id1463298887"); - attr(a2, "href", "https://github.com/quoid/userscripts#support-development"); + attr(a2, "href", "https://github.com/quoid/userscripts#support"); attr(p, "class", "svelte-1ibrghz"); attr(div28, "class", "modal__section"); attr(div29, "class", "modal svelte-1ibrghz"); @@ -19385,17 +19461,20 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o append(div28, p); append(p, t42); append(p, t43); + append(p, t44); + append(p, t45); + append(p, t46); append(p, br0); append(p, br1); - append(p, t44); + append(p, t47); append(p, a0); append(p, br2); append(p, br3); - append(p, t46); + append(p, t49); append(p, a1); - append(p, t48); + append(p, t51); append(p, a2); - append(p, t50); + append(p, t53); current = true; if (!mounted) { @@ -19447,7 +19526,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o if (if_block) { if_block.p(ctx, dirty); } else { - if_block = create_if_block$6(); + if_block = create_if_block$7(); if_block.c(); if_block.m(div24, null); } @@ -19465,6 +19544,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o } if ((!current || dirty & /*$settings*/ 8) && t43_value !== (t43_value = /*$settings*/ ctx[3].version + "")) set_data(t43, t43_value); + if ((!current || dirty & /*$settings*/ 8) && t45_value !== (t45_value = /*$settings*/ ctx[3].build + "")) set_data(t45, t45_value); }, i(local) { if (current) return; @@ -19920,7 +20000,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o } // (126:0) {#if $state.includes("settings")} - function create_if_block$7(ctx) { + function create_if_block$8(ctx) { let settings_1; let current; settings_1 = new Settings({}); @@ -19977,7 +20057,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o each_1_lookup.set(key, each_blocks[i] = create_each_block$1(key, child_ctx)); } - let if_block1 = show_if && create_if_block$7(); + let if_block1 = show_if && create_if_block$8(); return { c() { @@ -20069,7 +20149,7 @@ var JSHINT;"undefined"==typeof window&&(window={}),function(){var f=function u(o transition_in(if_block1, 1); } } else { - if_block1 = create_if_block$7(); + if_block1 = create_if_block$8(); if_block1.c(); transition_in(if_block1, 1); if_block1.m(if_block1_anchor.parentNode, if_block1_anchor); diff --git a/extension/Userscripts Extension/Resources/popup.js b/extension/Userscripts Extension/Resources/popup.js index 8ec73dc0..94409f66 100644 --- a/extension/Userscripts Extension/Resources/popup.js +++ b/extension/Userscripts Extension/Resources/popup.js @@ -1298,8 +1298,8 @@ function create_else_block(ctx) { let current; - const default_slot_template = /*#slots*/ ctx[6].default; - const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[5], null); + const default_slot_template = /*#slots*/ ctx[8].default; + const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[7], null); const default_slot_or_fallback = default_slot || fallback_block(); return { @@ -1315,8 +1315,8 @@ }, p(ctx, dirty) { if (default_slot) { - if (default_slot.p && dirty & /*$$scope*/ 32) { - update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[5], dirty, null, null); + if (default_slot.p && dirty & /*$$scope*/ 128) { + update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[7], dirty, null, null); } } }, @@ -1335,14 +1335,16 @@ }; } - // (31:8) {#if loading && showLoaderOnDisabled} + // (33:8) {#if loading && showLoaderOnDisabled} function create_if_block$2(ctx) { let loader; let current; loader = new Loader({ props: { - backgroundColor: "var(--color-bg-primary)" + backgroundColor: "var(--color-bg-primary)", + abortClick: /*abortClick*/ ctx[5], + abort: /*abort*/ ctx[4] } }); @@ -1354,7 +1356,12 @@ mount_component(loader, target, anchor); current = true; }, - p: noop, + p(ctx, dirty) { + const loader_changes = {}; + if (dirty & /*abortClick*/ 32) loader_changes.abortClick = /*abortClick*/ ctx[5]; + if (dirty & /*abort*/ 16) loader_changes.abort = /*abort*/ ctx[4]; + loader.$set(loader_changes); + }, i(local) { if (current) return; transition_in(loader.$$.fragment, local); @@ -1370,7 +1377,7 @@ }; } - // (34:18)
+ // (36:18)
function fallback_block(ctx) { let div; @@ -1478,7 +1485,7 @@ transition_in(if_block); add_render_callback(() => { - if (!div2_transition) div2_transition = create_bidirectional_transition(div2, /*slide*/ ctx[4], {}, true); + if (!div2_transition) div2_transition = create_bidirectional_transition(div2, /*slide*/ ctx[6], {}, true); div2_transition.run(1); }); @@ -1487,7 +1494,7 @@ o(local) { transition_out(iconbutton.$$.fragment, local); transition_out(if_block); - if (!div2_transition) div2_transition = create_bidirectional_transition(div2, /*slide*/ ctx[4], {}, false); + if (!div2_transition) div2_transition = create_bidirectional_transition(div2, /*slide*/ ctx[6], {}, false); div2_transition.run(0); current = false; }, @@ -1506,6 +1513,11 @@ let { headerTitle = "View Header" } = $$props; let { closeClick } = $$props; let { showLoaderOnDisabled = true } = $$props; + let { abort = false } = $$props; + + let { abortClick = () => { + + } } = $$props; function slide(node, params) { return { @@ -1521,10 +1533,22 @@ if ("headerTitle" in $$props) $$invalidate(1, headerTitle = $$props.headerTitle); if ("closeClick" in $$props) $$invalidate(2, closeClick = $$props.closeClick); if ("showLoaderOnDisabled" in $$props) $$invalidate(3, showLoaderOnDisabled = $$props.showLoaderOnDisabled); - if ("$$scope" in $$props) $$invalidate(5, $$scope = $$props.$$scope); + if ("abort" in $$props) $$invalidate(4, abort = $$props.abort); + if ("abortClick" in $$props) $$invalidate(5, abortClick = $$props.abortClick); + if ("$$scope" in $$props) $$invalidate(7, $$scope = $$props.$$scope); }; - return [loading, headerTitle, closeClick, showLoaderOnDisabled, slide, $$scope, slots]; + return [ + loading, + headerTitle, + closeClick, + showLoaderOnDisabled, + abort, + abortClick, + slide, + $$scope, + slots + ]; } class View extends SvelteComponent { @@ -1535,7 +1559,9 @@ loading: 0, headerTitle: 1, closeClick: 2, - showLoaderOnDisabled: 3 + showLoaderOnDisabled: 3, + abort: 4, + abortClick: 5 }); } } @@ -2833,16 +2859,16 @@ function get_each_context$3(ctx, list, i) { const child_ctx = ctx.slice(); - child_ctx[47] = list[i]; + child_ctx[48] = list[i]; return child_ctx; } - // (468:0) {#if !active} + // (487:0) {#if !active} function create_if_block_10(ctx) { return { c: noop, m: noop, d: noop }; } - // (471:0) {#if showInstallPrompt} + // (490:0) {#if showInstallPrompt} function create_if_block_9(ctx) { let div; let t0; @@ -2865,10 +2891,10 @@ append(div, t0); append(div, span); append(span, t1); - /*div_binding*/ ctx[34](div); + /*div_binding*/ ctx[35](div); if (!mounted) { - dispose = listen(span, "click", /*showInstallView*/ ctx[30]); + dispose = listen(span, "click", /*showInstallView*/ ctx[31]); mounted = true; } }, @@ -2877,14 +2903,14 @@ }, d(detaching) { if (detaching) detach(div); - /*div_binding*/ ctx[34](null); + /*div_binding*/ ctx[35](null); mounted = false; dispose(); } }; } - // (476:0) {#if error} + // (495:0) {#if error} function create_if_block_8(ctx) { let div; let t0; @@ -2896,7 +2922,7 @@ props: { icon: iconClear, title: "Clear error" } }); - iconbutton.$on("click", /*click_handler_1*/ ctx[35]); + iconbutton.$on("click", /*click_handler_1*/ ctx[36]); return { c() { @@ -2911,7 +2937,7 @@ append(div, t0); append(div, t1); mount_component(iconbutton, div, null); - /*div_binding_1*/ ctx[36](div); + /*div_binding_1*/ ctx[37](div); current = true; }, p(ctx, dirty) { @@ -2929,19 +2955,19 @@ d(detaching) { if (detaching) detach(div); destroy_component(iconbutton); - /*div_binding_1*/ ctx[36](null); + /*div_binding_1*/ ctx[37](null); } }; } - // (498:8) {:else} + // (517:8) {:else} function create_else_block$3(ctx) { let div; let each_blocks = []; let each_1_lookup = new Map(); let current; - let each_value = /*list*/ ctx[21]; - const get_key = ctx => /*item*/ ctx[47].filename; + let each_value = /*list*/ ctx[22]; + const get_key = ctx => /*item*/ ctx[48].filename; for (let i = 0; i < each_value.length; i += 1) { let child_ctx = get_each_context$3(ctx, each_value, i); @@ -2970,8 +2996,8 @@ current = true; }, p(ctx, dirty) { - if (dirty[0] & /*list, toggleItem*/ 35651584) { - const each_value = /*list*/ ctx[21]; + if (dirty[0] & /*list, toggleItem*/ 71303168) { + const each_value = /*list*/ ctx[22]; group_outros(); each_blocks = update_keyed_each(each_blocks, dirty, get_key, 1, ctx, each_value, each_1_lookup, div, outro_and_destroy_block, create_each_block$3, null, get_each_context$3); check_outros(); @@ -3007,7 +3033,7 @@ }; } - // (496:35) + // (515:35) function create_if_block_7(ctx) { let div; @@ -3029,7 +3055,7 @@ }; } - // (492:28) + // (511:28) function create_if_block_6$1(ctx) { let div; let t0; @@ -3052,7 +3078,7 @@ append(div, span); if (!mounted) { - dispose = listen(span, "click", /*click_handler_2*/ ctx[37]); + dispose = listen(span, "click", /*click_handler_2*/ ctx[38]); mounted = true; } }, @@ -3067,7 +3093,7 @@ }; } - // (490:8) {#if inactive} + // (509:8) {#if inactive} function create_if_block_5$1(ctx) { let div; @@ -3089,11 +3115,17 @@ }; } - // (487:4) {#if loading} + // (506:4) {#if loading} function create_if_block_4$1(ctx) { let loader; let current; - loader = new Loader({}); + + loader = new Loader({ + props: { + abortClick: abortUpdates, + abort: /*abort*/ ctx[21] + } + }); return { c() { @@ -3103,7 +3135,11 @@ mount_component(loader, target, anchor); current = true; }, - p: noop, + p(ctx, dirty) { + const loader_changes = {}; + if (dirty[0] & /*abort*/ 2097152) loader_changes.abort = /*abort*/ ctx[21]; + loader.$set(loader_changes); + }, i(local) { if (current) return; transition_in(loader.$$.fragment, local); @@ -3119,22 +3155,22 @@ }; } - // (500:16) {#each list as item (item.filename)} + // (519:16) {#each list as item (item.filename)} function create_each_block$3(key_1, ctx) { let first; let popupitem; let current; function click_handler_3(...args) { - return /*click_handler_3*/ ctx[38](/*item*/ ctx[47], ...args); + return /*click_handler_3*/ ctx[39](/*item*/ ctx[48], ...args); } popupitem = new PopupItem({ props: { - enabled: !/*item*/ ctx[47].disabled, - name: /*item*/ ctx[47].name, - subframe: /*item*/ ctx[47].subframe, - type: /*item*/ ctx[47].type + enabled: !/*item*/ ctx[48].disabled, + name: /*item*/ ctx[48].name, + subframe: /*item*/ ctx[48].subframe, + type: /*item*/ ctx[48].type } }); @@ -3156,10 +3192,10 @@ p(new_ctx, dirty) { ctx = new_ctx; const popupitem_changes = {}; - if (dirty[0] & /*list*/ 2097152) popupitem_changes.enabled = !/*item*/ ctx[47].disabled; - if (dirty[0] & /*list*/ 2097152) popupitem_changes.name = /*item*/ ctx[47].name; - if (dirty[0] & /*list*/ 2097152) popupitem_changes.subframe = /*item*/ ctx[47].subframe; - if (dirty[0] & /*list*/ 2097152) popupitem_changes.type = /*item*/ ctx[47].type; + if (dirty[0] & /*list*/ 4194304) popupitem_changes.enabled = !/*item*/ ctx[48].disabled; + if (dirty[0] & /*list*/ 4194304) popupitem_changes.name = /*item*/ ctx[48].name; + if (dirty[0] & /*list*/ 4194304) popupitem_changes.subframe = /*item*/ ctx[48].subframe; + if (dirty[0] & /*list*/ 4194304) popupitem_changes.type = /*item*/ ctx[48].type; popupitem.$set(popupitem_changes); }, i(local) { @@ -3178,7 +3214,7 @@ }; } - // (513:0) {#if !inactive && platform === "macos"} + // (532:0) {#if !inactive && platform === "macos"} function create_if_block_3$1(ctx) { let div1; let div0; @@ -3211,7 +3247,7 @@ }; } - // (546:18) + // (567:18) function create_if_block_2$1(ctx) { let view; let current; @@ -3220,7 +3256,7 @@ props: { headerTitle: "All Userscripts", loading: /*disabled*/ ctx[3], - closeClick: /*func_3*/ ctx[43], + closeClick: /*func_3*/ ctx[44], showLoaderOnDisabled: false, $$slots: { default: [create_default_slot_2] }, $$scope: { ctx } @@ -3238,9 +3274,9 @@ p(ctx, dirty) { const view_changes = {}; if (dirty[0] & /*disabled*/ 8) view_changes.loading = /*disabled*/ ctx[3]; - if (dirty[0] & /*showAll*/ 524288) view_changes.closeClick = /*func_3*/ ctx[43]; + if (dirty[0] & /*showAll*/ 524288) view_changes.closeClick = /*func_3*/ ctx[44]; - if (dirty[0] & /*allItems*/ 1048576 | dirty[1] & /*$$scope*/ 524288) { + if (dirty[0] & /*allItems*/ 1048576 | dirty[1] & /*$$scope*/ 1048576) { view_changes.$$scope = { dirty, ctx }; } @@ -3261,7 +3297,7 @@ }; } - // (532:22) + // (553:22) function create_if_block_1$1(ctx) { let view; let current; @@ -3270,7 +3306,7 @@ props: { headerTitle: "Install Userscript", loading: /*disabled*/ ctx[3], - closeClick: /*func_2*/ ctx[42], + closeClick: /*func_2*/ ctx[43], showLoaderOnDisabled: true, $$slots: { default: [create_default_slot_1] }, $$scope: { ctx } @@ -3288,9 +3324,9 @@ p(ctx, dirty) { const view_changes = {}; if (dirty[0] & /*disabled*/ 8) view_changes.loading = /*disabled*/ ctx[3]; - if (dirty[0] & /*showInstall*/ 65536) view_changes.closeClick = /*func_2*/ ctx[42]; + if (dirty[0] & /*showInstall*/ 65536) view_changes.closeClick = /*func_2*/ ctx[43]; - if (dirty[0] & /*installViewUserscript, installViewUserscriptError, showInstall*/ 458752 | dirty[1] & /*$$scope*/ 524288) { + if (dirty[0] & /*installViewUserscript, installViewUserscriptError, showInstall*/ 458752 | dirty[1] & /*$$scope*/ 1048576) { view_changes.$$scope = { dirty, ctx }; } @@ -3311,7 +3347,7 @@ }; } - // (518:0) {#if showUpdates} + // (537:0) {#if showUpdates} function create_if_block$6(ctx) { let view; let current; @@ -3320,8 +3356,10 @@ props: { headerTitle: "Updates", loading: /*disabled*/ ctx[3], - closeClick: /*func*/ ctx[40], + closeClick: /*func*/ ctx[41], showLoaderOnDisabled: true, + abortClick: abortUpdates, + abort: /*showUpdates*/ ctx[5], $$slots: { default: [create_default_slot] }, $$scope: { ctx } } @@ -3338,9 +3376,10 @@ p(ctx, dirty) { const view_changes = {}; if (dirty[0] & /*disabled*/ 8) view_changes.loading = /*disabled*/ ctx[3]; - if (dirty[0] & /*showUpdates*/ 32) view_changes.closeClick = /*func*/ ctx[40]; + if (dirty[0] & /*showUpdates*/ 32) view_changes.closeClick = /*func*/ ctx[41]; + if (dirty[0] & /*showUpdates*/ 32) view_changes.abort = /*showUpdates*/ ctx[5]; - if (dirty[0] & /*updates*/ 64 | dirty[1] & /*$$scope*/ 524288) { + if (dirty[0] & /*updates*/ 64 | dirty[1] & /*$$scope*/ 1048576) { view_changes.$$scope = { dirty, ctx }; } @@ -3361,7 +3400,7 @@ }; } - // (547:4) {showAll = false; refreshView()}} showLoaderOnDisabled={false} > + // (568:4) {showAll = false; refreshView()}} showLoaderOnDisabled={false} > function create_default_slot_2(ctx) { let allitemsview; let current; @@ -3369,7 +3408,7 @@ allitemsview = new AllItemsView({ props: { allItems: /*allItems*/ ctx[20], - allItemsToggleItem: /*toggleItem*/ ctx[25] + allItemsToggleItem: /*toggleItem*/ ctx[26] } }); @@ -3401,7 +3440,7 @@ }; } - // (533:4) showInstall = false} showLoaderOnDisabled={true} > + // (554:4) showInstall = false} showLoaderOnDisabled={true} > function create_default_slot_1(ctx) { let installview; let current; @@ -3410,8 +3449,8 @@ props: { userscript: /*installViewUserscript*/ ctx[17], installError: /*installViewUserscriptError*/ ctx[18], - installCancelClick: /*func_1*/ ctx[41], - installConfirmClick: /*installConfirm*/ ctx[31] + installCancelClick: /*func_1*/ ctx[42], + installConfirmClick: /*installConfirm*/ ctx[32] } }); @@ -3427,7 +3466,7 @@ const installview_changes = {}; if (dirty[0] & /*installViewUserscript*/ 131072) installview_changes.userscript = /*installViewUserscript*/ ctx[17]; if (dirty[0] & /*installViewUserscriptError*/ 262144) installview_changes.installError = /*installViewUserscriptError*/ ctx[18]; - if (dirty[0] & /*showInstall*/ 65536) installview_changes.installCancelClick = /*func_1*/ ctx[41]; + if (dirty[0] & /*showInstall*/ 65536) installview_changes.installCancelClick = /*func_1*/ ctx[42]; installview.$set(installview_changes); }, i(local) { @@ -3445,16 +3484,16 @@ }; } - // (519:4) showUpdates = false} showLoaderOnDisabled={true} > + // (538:4) showUpdates = false} showLoaderOnDisabled={true} abortClick={abortUpdates} abort={showUpdates} > function create_default_slot(ctx) { let updateview; let current; updateview = new UpdateView({ props: { - checkClick: /*checkForUpdates*/ ctx[26], - updateClick: /*updateAll*/ ctx[23], - updateSingleClick: /*updateItem*/ ctx[24], + checkClick: /*checkForUpdates*/ ctx[27], + updateClick: /*updateAll*/ ctx[24], + updateSingleClick: /*updateItem*/ ctx[25], updates: /*updates*/ ctx[6] } }); @@ -3521,7 +3560,7 @@ } }); - iconbutton0.$on("click", /*openSaveLocation*/ ctx[28]); + iconbutton0.$on("click", /*openSaveLocation*/ ctx[29]); iconbutton1 = new IconButton({ props: { @@ -3532,7 +3571,7 @@ } }); - iconbutton1.$on("click", /*click_handler*/ ctx[32]); + iconbutton1.$on("click", /*click_handler*/ ctx[33]); iconbutton2 = new IconButton({ props: { @@ -3542,7 +3581,7 @@ } }); - iconbutton2.$on("click", /*refreshView*/ ctx[27]); + iconbutton2.$on("click", /*refreshView*/ ctx[28]); toggle = new Toggle({ props: { @@ -3552,7 +3591,7 @@ } }); - toggle.$on("click", /*toggleExtension*/ ctx[22]); + toggle.$on("click", /*toggleExtension*/ ctx[23]); let if_block0 = !/*active*/ ctx[1] && create_if_block_10(); let if_block1 = /*showInstallPrompt*/ ctx[15] && create_if_block_9(ctx); let if_block2 = /*error*/ ctx[0] && create_if_block_8(ctx); @@ -3628,7 +3667,7 @@ mount_component(iconbutton2, div0, null); append(div0, t2); mount_component(toggle, div0, null); - /*div0_binding*/ ctx[33](div0); + /*div0_binding*/ ctx[34](div0); insert(target, t3, anchor); if (if_block0) if_block0.m(target, anchor); insert(target, t4, anchor); @@ -3638,7 +3677,7 @@ insert(target, t6, anchor); insert(target, div1, anchor); if_blocks[current_block_type_index].m(div1, null); - /*div1_binding*/ ctx[39](div1); + /*div1_binding*/ ctx[40](div1); insert(target, t7, anchor); if (if_block4) if_block4.m(target, anchor); insert(target, t8, anchor); @@ -3651,7 +3690,7 @@ current = true; if (!mounted) { - dispose = listen(window_1, "resize", /*resize*/ ctx[29]); + dispose = listen(window_1, "resize", /*resize*/ ctx[30]); mounted = true; } }, @@ -3819,7 +3858,7 @@ destroy_component(iconbutton1); destroy_component(iconbutton2); destroy_component(toggle); - /*div0_binding*/ ctx[33](null); + /*div0_binding*/ ctx[34](null); if (detaching) detach(t3); if (if_block0) if_block0.d(detaching); if (detaching) detach(t4); @@ -3829,7 +3868,7 @@ if (detaching) detach(t6); if (detaching) detach(div1); if_blocks[current_block_type_index].d(); - /*div1_binding*/ ctx[39](null); + /*div1_binding*/ ctx[40](null); if (detaching) detach(t7); if (if_block4) if_block4.d(detaching); if (detaching) detach(t8); @@ -3917,6 +3956,17 @@ return true; } + async function abortUpdates() { + // sends message to swift side canceling all URLSession tasks + browser.runtime.sendNativeMessage({ name: "CANCEL_REQUESTS" }); + + // timestamp for checking updates happens right before update fetching + // that means when this function runs the timestamp has already been saved + // reloading the window will essentially skip the update check + // since the subsequent popup load will not check for updates + window.location.reload(); + } + function instance$9($$self, $$props, $$invalidate) { let error = undefined; let active = true; @@ -3941,6 +3991,7 @@ let showAll; let allItems = []; let resizeTimer; + let abort = false; function toggleExtension(e) { e.preventDefault(); // prevent check state from changing on click @@ -4045,6 +4096,7 @@ $$invalidate(5, showUpdates = false); $$invalidate(6, updates = []); $$invalidate(9, inactive = false); + $$invalidate(21, abort = false); initialize(); } @@ -4162,16 +4214,17 @@ let updatesResponse; try { - updatesResponse = await browser.runtime.sendNativeMessage({ name: "POPUP_UPDATES" }); - // save timestamp in ms to extension storage const timestampMs = Date.now(); await browser.storage.local.set({ "lastUpdateCheck": timestampMs }); + $$invalidate(21, abort = true); + updatesResponse = await browser.runtime.sendNativeMessage({ name: "POPUP_UPDATES" }); } catch(error) { - console.log("Error for updates promise: " + error); + console.error("Error for updates promise: " + error); $$invalidate(11, initError = true); $$invalidate(2, loading = false); + $$invalidate(21, abort = false); return; } @@ -4179,10 +4232,13 @@ $$invalidate(0, error = updatesResponse.error); $$invalidate(2, loading = false); $$invalidate(3, disabled = false); + $$invalidate(21, abort = false); return; } else { $$invalidate(6, updates = updatesResponse.updates); } + + $$invalidate(21, abort = false); } // check if current page url is a userscript @@ -4191,6 +4247,9 @@ if (strippedUrl.endsWith(".user.js")) { // if it does, send message to content script + // context script will check the document contentType + // if it's not an applicable type, it'll return {invalid: true} response and no install prompt shown + // if the contentType is applicable, what is mentioned below happens // content script will get dom content, and send it to the bg page // the bg page will send the content to the swift side for parsing // when swift side parses and returns, the bg page will send a response to the content script @@ -4202,7 +4261,7 @@ if (response.error) { console.log("Error checking .user.js url: " + response.error); $$invalidate(0, error = response.error); - } else { + } else if (!response.invalid) { // the response will contain the string to display // ex: {success: "Click to install"} const prompt = response.success; @@ -4369,10 +4428,10 @@ $$self.$$.update = () => { if ($$self.$$.dirty[0] & /*items*/ 16) { - $$invalidate(21, list = items.sort((a, b) => a.name.localeCompare(b.name))); + $$invalidate(22, list = items.sort((a, b) => a.name.localeCompare(b.name))); } - if ($$self.$$.dirty[0] & /*list*/ 2097152) { + if ($$self.$$.dirty[0] & /*list*/ 4194304) { if (list.length > 1 && list.length % 2 === 0) { $$invalidate(8, rowColors = "even"); } else if (list.length > 1 && list.length % 2 !== 0) { @@ -4409,6 +4468,7 @@ installViewUserscriptError, showAll, allItems, + abort, list, toggleExtension, updateAll, diff --git a/extension/Userscripts Extension/SafariWebExtensionHandler.swift b/extension/Userscripts Extension/SafariWebExtensionHandler.swift index 70e2c9b8..58c0079a 100644 --- a/extension/Userscripts Extension/SafariWebExtensionHandler.swift +++ b/extension/Userscripts Extension/SafariWebExtensionHandler.swift @@ -259,6 +259,7 @@ class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling { task.cancel() } } + response.userInfo = [SFExtensionMessageKey: ["success": true]] } else if name == "PAGE_NEW_REMOTE" { #if os(macOS) diff --git a/extension/Userscripts-iOS/Base.lproj/Main.html b/extension/Userscripts-iOS/Base.lproj/Main.html index 15a2b09e..e718ab5a 100644 --- a/extension/Userscripts-iOS/Base.lproj/Main.html +++ b/extension/Userscripts-iOS/Base.lproj/Main.html @@ -1 +1 @@ -
Userscripts App Icon

You can turn on the Userscripts iOS Safari extension in Settings. Read the docs.

CURRENT DIRECTORY:
init
+
Userscripts App Icon

You can turn on the Userscripts iOS Safari extension in Settings. Read the docs.

CURRENT DIRECTORY:
init
diff --git a/extension/Userscripts-iOS/ViewController.swift b/extension/Userscripts-iOS/ViewController.swift index 6a25ba51..423fa91b 100644 --- a/extension/Userscripts-iOS/ViewController.swift +++ b/extension/Userscripts-iOS/ViewController.swift @@ -28,6 +28,8 @@ class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHan } func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "??" + let buildNumber = Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "??" var readLocation:String if let sharedBookmarkData = UserDefaults(suiteName: SharedDefaults.suiteName)?.data(forKey: SharedDefaults.keyName) { if let bookmarkUrl = readBookmark(data: sharedBookmarkData, isSecure: true) { @@ -39,6 +41,7 @@ class ViewController: UIViewController, WKNavigationDelegate, WKScriptMessageHan readLocation = "Select a directory to load userscripts" } webView.evaluateJavaScript("printDirectory('\(readLocation)')") + webView.evaluateJavaScript("printVersion('v\(appVersion)', '(\(buildNumber))')") } func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { diff --git a/extension/Userscripts.xcodeproj/project.pbxproj b/extension/Userscripts.xcodeproj/project.pbxproj index 9157e837..426a0d70 100644 --- a/extension/Userscripts.xcodeproj/project.pbxproj +++ b/extension/Userscripts.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 4A143AAC279DE6FF0029BFD0 /* UserscriptsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A143AAB279DE6FF0029BFD0 /* UserscriptsTests.swift */; }; + 4A143AB2279DEA170029BFD0 /* Functions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4AED6491268CDB58001794BF /* Functions.swift */; }; + 4A143AB3279DEA370029BFD0 /* Shared.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A6E19ED268CC84A00E0270C /* Shared.swift */; }; 4A301B2A270A474400C7E9E1 /* SafariWebExtensionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4A57BA07227235CE008A9763 /* SafariWebExtensionHandler.swift */; }; 4A301B2B270A474F00C7E9E1 /* _locales in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A617268266B30018536B /* _locales */; }; 4A301B2C270A475200C7E9E1 /* images in Resources */ = {isa = PBXBuildFile; fileRef = 4A36A616268266B30018536B /* images */; }; @@ -49,6 +52,13 @@ /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ + 4A143AAD279DE6FF0029BFD0 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 4A57B9E4227235CD008A9763 /* Project object */; + proxyType = 1; + remoteGlobalIDString = 4A57B9EB227235CD008A9763; + remoteInfo = Userscripts; + }; 4A4CF6F9270A38BF00111584 /* PBXContainerItemProxy */ = { isa = PBXContainerItemProxy; containerPortal = 4A57B9E4227235CD008A9763 /* Project object */; @@ -91,6 +101,8 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 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 = ""; }; 4A36A616268266B30018536B /* images */ = {isa = PBXFileReference; lastKnownFileType = folder; path = images; sourceTree = ""; }; 4A36A617268266B30018536B /* _locales */ = {isa = PBXFileReference; lastKnownFileType = folder; path = _locales; sourceTree = ""; }; 4A4CF6DB270A38BD00111584 /* Userscripts-iOS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Userscripts-iOS.app"; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -131,6 +143,13 @@ /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 4A143AA6279DE6FF0029BFD0 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4A4CF6D8270A38BD00111584 /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; @@ -163,6 +182,14 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 4A143AAA279DE6FF0029BFD0 /* UserscriptsTests */ = { + isa = PBXGroup; + children = ( + 4A143AAB279DE6FF0029BFD0 /* UserscriptsTests.swift */, + ); + path = UserscriptsTests; + sourceTree = ""; + }; 4A4CF6DC270A38BD00111584 /* Userscripts-iOS */ = { isa = PBXGroup; children = ( @@ -205,6 +232,7 @@ 4A57BA06227235CE008A9763 /* Userscripts Extension */, 4A4CF6DC270A38BD00111584 /* Userscripts-iOS */, 4A4CF6FB270A38BF00111584 /* Userscripts-iOS Extension */, + 4A143AAA279DE6FF0029BFD0 /* UserscriptsTests */, 4A57BA03227235CE008A9763 /* Frameworks */, 4A57B9ED227235CD008A9763 /* Products */, ); @@ -217,6 +245,7 @@ 4A57B9FF227235CE008A9763 /* Userscripts Extension.appex */, 4A4CF6DB270A38BD00111584 /* Userscripts-iOS.app */, 4A4CF6F7270A38BF00111584 /* Userscripts-iOS Extension.appex */, + 4A143AA9279DE6FF0029BFD0 /* UserscriptsTests.xctest */, ); name = Products; sourceTree = ""; @@ -273,6 +302,24 @@ /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 4A143AA8279DE6FF0029BFD0 /* UserscriptsTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 4A143AB1279DE6FF0029BFD0 /* Build configuration list for PBXNativeTarget "UserscriptsTests" */; + buildPhases = ( + 4A143AA5279DE6FF0029BFD0 /* Sources */, + 4A143AA6279DE6FF0029BFD0 /* Frameworks */, + 4A143AA7279DE6FF0029BFD0 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + 4A143AAE279DE6FF0029BFD0 /* PBXTargetDependency */, + ); + name = UserscriptsTests; + productName = UserscriptsTests; + productReference = 4A143AA9279DE6FF0029BFD0 /* UserscriptsTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 4A4CF6DA270A38BD00111584 /* Userscripts-iOS */ = { isa = PBXNativeTarget; buildConfigurationList = 4A4CF717270A38BF00111584 /* Build configuration list for PBXNativeTarget "Userscripts-iOS" */; @@ -351,10 +398,14 @@ 4A57B9E4227235CD008A9763 /* Project object */ = { isa = PBXProject; attributes = { - LastSwiftUpdateCheck = 1300; + LastSwiftUpdateCheck = 1320; LastUpgradeCheck = 1200; ORGANIZATIONNAME = "Justin Wasack"; TargetAttributes = { + 4A143AA8279DE6FF0029BFD0 = { + CreatedOnToolsVersion = 13.2.1; + TestTargetID = 4A57B9EB227235CD008A9763; + }; 4A4CF6DA270A38BD00111584 = { CreatedOnToolsVersion = 13.0; }; @@ -391,11 +442,19 @@ 4A57B9FE227235CE008A9763 /* Userscripts Extension */, 4A4CF6DA270A38BD00111584 /* Userscripts-iOS */, 4A4CF6F6270A38BF00111584 /* Userscripts-iOS Extension */, + 4A143AA8279DE6FF0029BFD0 /* UserscriptsTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 4A143AA7279DE6FF0029BFD0 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4A4CF6D9270A38BD00111584 /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -450,6 +509,16 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 4A143AA5279DE6FF0029BFD0 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 4A143AB3279DEA370029BFD0 /* Shared.swift in Sources */, + 4A143AB2279DEA170029BFD0 /* Functions.swift in Sources */, + 4A143AAC279DE6FF0029BFD0 /* UserscriptsTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 4A4CF6D7270A38BD00111584 /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; @@ -494,6 +563,11 @@ /* End PBXSourcesBuildPhase section */ /* Begin PBXTargetDependency section */ + 4A143AAE279DE6FF0029BFD0 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 4A57B9EB227235CD008A9763 /* Userscripts */; + targetProxy = 4A143AAD279DE6FF0029BFD0 /* PBXContainerItemProxy */; + }; 4A4CF6FA270A38BF00111584 /* PBXTargetDependency */ = { isa = PBXTargetDependency; target = 4A4CF6F6270A38BF00111584 /* Userscripts-iOS Extension */; @@ -542,6 +616,44 @@ /* End PBXVariantGroup section */ /* Begin XCBuildConfiguration section */ + 4A143AAF279DE6FF0029BFD0 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = J74Q8V8V8N; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.userscripts.UserscriptsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Userscripts.app/Contents/MacOS/Userscripts"; + }; + name = Debug; + }; + 4A143AB0279DE6FF0029BFD0 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = J74Q8V8V8N; + GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 12.1; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.userscripts.UserscriptsTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Userscripts.app/Contents/MacOS/Userscripts"; + }; + name = Release; + }; 4A4CF711270A38BF00111584 /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { @@ -551,7 +663,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = "Userscripts-iOS/Userscripts-iOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 15; + CURRENT_PROJECT_VERSION = 20; DEVELOPMENT_TEAM = J74Q8V8V8N; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Userscripts-iOS/Info.plist"; @@ -567,7 +679,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.1; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -592,7 +704,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = "Userscripts-iOS/Userscripts-iOS.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 15; + CURRENT_PROJECT_VERSION = 20; DEVELOPMENT_TEAM = J74Q8V8V8N; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Userscripts-iOS/Info.plist"; @@ -608,7 +720,7 @@ "$(inherited)", "@executable_path/Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.1; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -631,7 +743,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = "Userscripts-iOS Extension/Userscripts-iOS Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 15; + CURRENT_PROJECT_VERSION = 20; DEVELOPMENT_TEAM = J74Q8V8V8N; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Userscripts-iOS Extension/Info.plist"; @@ -643,7 +755,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.1; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -664,7 +776,7 @@ CLANG_CXX_LANGUAGE_STANDARD = "gnu++17"; CODE_SIGN_ENTITLEMENTS = "Userscripts-iOS Extension/Userscripts-iOS Extension.entitlements"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 15; + CURRENT_PROJECT_VERSION = 20; DEVELOPMENT_TEAM = J74Q8V8V8N; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = "Userscripts-iOS Extension/Info.plist"; @@ -676,7 +788,7 @@ "@executable_path/Frameworks", "@executable_path/../../Frameworks", ); - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.1; OTHER_LDFLAGS = ( "-framework", SafariServices, @@ -817,7 +929,7 @@ CODE_SIGN_ENTITLEMENTS = "Userscripts Extension/Userscripts Extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 43; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_TEAM = J74Q8V8V8N; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Userscripts Extension/Info.plist"; @@ -827,7 +939,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 4.0.11; + MARKETING_VERSION = 4.1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.userscripts.macos.Userscripts-Extension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -841,7 +953,7 @@ CODE_SIGN_ENTITLEMENTS = "Userscripts Extension/Userscripts Extension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 43; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_TEAM = J74Q8V8V8N; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = "Userscripts Extension/Info.plist"; @@ -851,7 +963,7 @@ "@executable_path/../../../../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 4.0.11; + MARKETING_VERSION = 4.1.0; PRODUCT_BUNDLE_IDENTIFIER = "com.userscripts.macos.Userscripts-Extension"; PRODUCT_NAME = "$(TARGET_NAME)"; SKIP_INSTALL = YES; @@ -868,7 +980,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 43; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_TEAM = J74Q8V8V8N; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Userscripts/Info.plist; @@ -877,7 +989,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 4.0.11; + MARKETING_VERSION = 4.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.userscripts.macos; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -893,7 +1005,7 @@ CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 43; + CURRENT_PROJECT_VERSION = 48; DEVELOPMENT_TEAM = J74Q8V8V8N; ENABLE_HARDENED_RUNTIME = YES; INFOPLIST_FILE = Userscripts/Info.plist; @@ -902,7 +1014,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 11.0; - MARKETING_VERSION = 4.0.11; + MARKETING_VERSION = 4.1.0; PRODUCT_BUNDLE_IDENTIFIER = com.userscripts.macos; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_VERSION = 5.0; @@ -912,6 +1024,15 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 4A143AB1279DE6FF0029BFD0 /* Build configuration list for PBXNativeTarget "UserscriptsTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 4A143AAF279DE6FF0029BFD0 /* Debug */, + 4A143AB0279DE6FF0029BFD0 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 4A4CF716270A38BF00111584 /* Build configuration list for PBXNativeTarget "Userscripts-iOS Extension" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/extension/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts Extension.xcscheme b/extension/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts Extension.xcscheme index ce5e3277..b3aefd4e 100644 --- a/extension/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts Extension.xcscheme +++ b/extension/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts Extension.xcscheme @@ -52,6 +52,16 @@ + + + + diff --git a/extension/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts.xcscheme b/extension/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts.xcscheme index 69c44fa7..1f3711f4 100644 --- a/extension/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts.xcscheme +++ b/extension/Userscripts.xcodeproj/xcshareddata/xcschemes/Userscripts.xcscheme @@ -37,6 +37,16 @@ + + + + com.apple.security.files.user-selected.read-write + com.apple.security.network.client + diff --git a/extension/Userscripts/ViewController.swift b/extension/Userscripts/ViewController.swift index 63788c1f..a23be7c9 100644 --- a/extension/Userscripts/ViewController.swift +++ b/extension/Userscripts/ViewController.swift @@ -25,6 +25,7 @@ class ViewController: NSViewController { ) // 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) @@ -49,6 +50,7 @@ class ViewController: NSViewController { } // shared bookmark can be read and directory exists, update url self.saveLocation.stringValue = url.absoluteString + self.saveLocation.toolTip = url.absoluteString } @objc func setExtensionState() { @@ -87,6 +89,7 @@ class ViewController: NSViewController { return } self.saveLocation.stringValue = url.absoluteString + self.saveLocation.toolTip = url.absoluteString } } }) diff --git a/extension/UserscriptsTests/UserscriptsTests.swift b/extension/UserscriptsTests/UserscriptsTests.swift new file mode 100644 index 00000000..44690877 --- /dev/null +++ b/extension/UserscriptsTests/UserscriptsTests.swift @@ -0,0 +1,99 @@ +// +// UserscriptsTests.swift +// UserscriptsTests +// +// Created by Justin Wasack on 1/23/22. +// Copyright © 2022 Justin Wasack. All rights reserved. +// + +import XCTest +@testable import Userscripts_Extension + +class UserscriptsTests: XCTestCase { + + override func setUpWithError() throws { + // Put setup code here. This method is called before the invocation of each test method in the class. + } + + override func tearDownWithError() throws { + // Put teardown code here. This method is called after the invocation of each test method in the class. + } + + + func testExample() throws { + // This is an example of a functional test case. + // Use XCTAssert and related functions to verify your tests produce the correct results. + // Any test you write for XCTest can be annotated as throws and async. + // Mark your test throws to produce an unexpected failure when your test encounters an uncaught error. + // Mark your test async to allow awaiting for asynchronous code to complete. Check the results with assertions afterwards. + } + + func testStringSanitization() throws { + // given + let strs = [ + "String", + "https://something.com/?foo=12", + "I have backslashes \\\\", + ".....ok", + ":Akneh.,><>dkie:lm", + "..解锁B站大会员番剧、", + "解锁B站大会员番剧、B站视频解析下载;全网VIP视频免费破解去广告;全网音乐直接下载;油管、Facebook等国外视频解析下载;网盘搜索引擎破解无限下载等", + "5CLksm3AAbb2F2F2f----___--+87363&^#%o%3O3", + "Example Userscript Name" + ] + + // when + var result = [String]() + for str in strs { + let sanitizedString = sanitize(str) + let unsanitizedString = unsanitize(sanitizedString) + result.append(unsanitizedString) + } + + // then + XCTAssert(result.elementsEqual(strs)) + } + + func testEncodedCheck() throws { + let urls = [ + "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect%20外链跳转.user.js", + "https://raw.githubusercontent.com/Anarios/return-youtube-dislike/main/Extensions/UserScript/Return%20Youtube%20Dislike.user.js", + "https://cdn.frankerfacez.com/static/ffz_injector.user.js", + "http://www.k21p.com/example.user.js", // add http protocol + "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect 外链跳转.user.js" + ] + var result = [String]() + for url in urls { + if isEncoded(url) { + result.append(url) + } + } + // 2 urls already percent encoded + XCTAssert(result.count == 2) + } + + func testGetRemoteFileContents() throws { + let urls = [ + "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect%20外链跳转.user.js", + "https://greasyfork.org/scripts/416338-redirect-外链跳转/code/redirect 外链跳转.user.js", + "https://raw.githubusercontent.com/Anarios/return-youtube-dislike/main/Extensions/UserScript/Return%20Youtube%20Dislike.user.js", + "https://cdn.frankerfacez.com/static/ffz_injector.user.js", + "http://www.k21p.com/example.user.js" // add http protocol + ] + var result = [String]() + for url in urls { + if let contents = getRemoteFileContents(url) { + result.append(contents) + } + } + XCTAssert(result.count == urls.count) + } + + func testPerformanceExample() throws { + // This is an example of a performance test case. + measure { + // Put the code you want to measure the time of here. + } + } + +} diff --git a/src/page/Components/Editor/EditorSearch.svelte b/src/page/Components/Editor/EditorSearch.svelte index 63967057..3b5d1a91 100644 --- a/src/page/Components/Editor/EditorSearch.svelte +++ b/src/page/Components/Editor/EditorSearch.svelte @@ -23,7 +23,7 @@ // the currently selected range let rangesIndex = 0; // next/prev marks text, store those marks here for later removal - let marks = []; + const marks = []; // the search query $: query = inputValue ? inputValue.trim() : ""; @@ -43,6 +43,8 @@ export async function focusInput() { await tick(); inp.focus(); + // if text entered, highlight it on focus + if (inp.value) inp.setSelectionRange(0, inp.value.length); } function keys(e) { @@ -66,7 +68,7 @@ token: function(stream) { pattern.lastIndex = stream.pos; const match = pattern.exec(stream.string); - if (match && match.index == stream.pos) { + if (match && match.index === stream.pos) { stream.pos += match[0].length || 1; return "searching"; } else if (match) { @@ -119,7 +121,7 @@ } else { --rangesIndex; } - let i = rangesIndex - 1; + const i = rangesIndex - 1; cm.setSelection(ranges[i].anchor, ranges[i].head); cm.scrollIntoView({from: ranges[i].anchor, to: ranges[i].head}, 20); // mark currently selected element diff --git a/src/page/Components/Settings.svelte b/src/page/Components/Settings.svelte index 693989b5..a1eb8b98 100644 --- a/src/page/Components/Settings.svelte +++ b/src/page/Components/Settings.svelte @@ -283,7 +283,7 @@
diff --git a/src/page/Components/Sidebar/SidebarItem.svelte b/src/page/Components/Sidebar/SidebarItem.svelte index 0ff49c90..6b5b3066 100644 --- a/src/page/Components/Sidebar/SidebarItem.svelte +++ b/src/page/Components/Sidebar/SidebarItem.svelte @@ -5,6 +5,23 @@ // the data that will populate the item contents in sidebar export let data = {}; export let toggleClick; + let showTitle = false; + + // if description > 120 characters, truncate and add ellipsis + // also trim for instances where an empty space is the last character + // this prevents ellipsis from being added next to empty space + // when description truncated, add title attr with full description text + function formatDescription(str) { + if (str && str.length > 120) { + showTitle = true; + return str.substring(0, 120).trim() + "..."; + } else { + showTitle = false; + return str; + } + } + + $: description = formatDescription(data.description);