diff --git a/.github/workflows/deployment.yaml b/.github/workflows/deployment.yaml
index 86855b3f..f9b2e529 100644
--- a/.github/workflows/deployment.yaml
+++ b/.github/workflows/deployment.yaml
@@ -9,7 +9,7 @@ jobs:
strategy:
matrix: # https://docs.github.com/en/actions/using-jobs/using-a-matrix-for-your-jobs
platform: [mac, ios]
- runs-on: macos-15 # https://github.com/actions/runner-images
+ runs-on: macos-26 # https://github.com/actions/runner-images
steps:
- uses: actions/checkout@v4 # https://github.com/actions/checkout
with:
@@ -37,9 +37,10 @@ jobs:
if: ${{ github.event.release.prerelease }}
- run: npm run build:${{ matrix.platform }}-safari-15
if: ${{ ! github.event.release.prerelease }}
- - name: Set xcode version
- run: sudo xcode-select -s "/Applications/Xcode_26_beta.app" # https://github.com/actions/runner-images/blob/main/images/macos/macos-15-arm64-Readme.md#xcode
- - run: xcodebuild -downloadPlatform iOS # Temporary fix for Xcode_26_beta
+ # - name: Set xcode version
+ # run: sudo xcode-select -s "/Applications/Xcode_26.1.1.app" # https://github.com/actions/runner-images/blob/main/images/macos/macos-26-arm64-Readme.md#xcode
+ # - run: xcodebuild -downloadPlatform iOS # Temporary fix for Xcode_26_beta
+ # if: matrix.platform == 'ios'
- name: Run fastlane
id: fastlane
working-directory: ./fastlane
diff --git a/README.md b/README.md
index d0601bfc..09cdc78a 100644
--- a/README.md
+++ b/README.md
@@ -54,9 +54,9 @@ After installing the iOS App, you need two main steps to make the extension work
> [!NOTE]
>
-> The App cannot detect whether you have enabled the extension in Safari, therefore, the App prompt will not change after you enable the extension. Currently the App interface is only used to set or change the userscripts directory.
+> The iOS App cannot detect whether you have enabled the extension in Safari, therefore, the App prompt will not change after you enable the extension. Currently the App interface is only used to set or change the userscripts directory.
>
-> You could select an iCloud folder for syncing scripts between macOS and iOS, but please note that there may be delays in synchronization, and you may encounter files be evictioned due to iCloud optimization, please refer to [#424](https://github.com/quoid/userscripts/issues/424).
+> You could select an iCloud folder for syncing scripts between macOS and iOS, but please note that there may be delays in synchronization, and you may encounter files be evictioned due to iCloud optimization, please refer to [#424](https://github.com/quoid/userscripts/issues/424). (**Since macOS 15 / iOS 18, please set "keep downloaded" for the folder to avoid eviction**)
There are two main ways to install a user script from the iOS version:
@@ -386,6 +386,6 @@ Userscripts does not collect any data from its users nor monitor activities or a
## License
-Copyright (c) 2018-2024 Justin Wasack
+Copyright © 2018-2025 Justin Wasack
Licensed under the [GNU General Public License v3.0](/LICENSE) license for all open source applications. A commercial license is required for all other applications.
diff --git a/docs/contributing.md b/docs/contributing.md
index 7d09d9d4..3cb92e9d 100644
--- a/docs/contributing.md
+++ b/docs/contributing.md
@@ -117,4 +117,4 @@ If you are not a member of the project, first you need to fork the master branch
# About
-[Userscripts](https://github.com/quoid/userscripts) @ 2018-2023
+[Userscripts](https://github.com/quoid/userscripts)
diff --git a/docs/dev.md b/docs/dev.md
index 2bfa7681..262aac05 100644
--- a/docs/dev.md
+++ b/docs/dev.md
@@ -79,4 +79,4 @@ Note that all existing `.xcconfig` files already include `.dev.xcconfig` files o
# About
-[Userscripts](https://github.com/quoid/userscripts) @ 2018-2024
+[Userscripts](https://github.com/quoid/userscripts)
diff --git a/docs/structure.md b/docs/structure.md
index d493c3b7..1a0cd320 100644
--- a/docs/structure.md
+++ b/docs/structure.md
@@ -38,4 +38,4 @@ Build the project from here to extension bundled app
# About
-[Userscripts](https://github.com/quoid/userscripts) @ 2018-2023
+[Userscripts](https://github.com/quoid/userscripts)
diff --git a/fastlane/Gemfile.lock b/fastlane/Gemfile.lock
index 989cb342..c6013a88 100644
--- a/fastlane/Gemfile.lock
+++ b/fastlane/Gemfile.lock
@@ -1,39 +1,40 @@
GEM
remote: https://rubygems.org/
specs:
- CFPropertyList (3.0.7)
- base64
- nkf
- rexml
- addressable (2.8.7)
- public_suffix (>= 2.0.2, < 7.0)
+ CFPropertyList (3.0.8)
+ abbrev (0.1.2)
+ addressable (2.8.8)
+ public_suffix (>= 2.0.2, < 8.0)
artifactory (3.0.17)
atomos (0.1.3)
- aws-eventstream (1.3.2)
- aws-partitions (1.1106.0)
- aws-sdk-core (3.224.0)
+ aws-eventstream (1.4.0)
+ aws-partitions (1.1188.0)
+ aws-sdk-core (3.239.2)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9)
base64
+ bigdecimal
jmespath (~> 1, >= 1.6.1)
logger
- aws-sdk-kms (1.101.0)
- aws-sdk-core (~> 3, >= 3.216.0)
+ aws-sdk-kms (1.118.0)
+ aws-sdk-core (~> 3, >= 3.239.1)
aws-sigv4 (~> 1.5)
- aws-sdk-s3 (1.186.1)
- aws-sdk-core (~> 3, >= 3.216.0)
+ aws-sdk-s3 (1.205.0)
+ aws-sdk-core (~> 3, >= 3.234.0)
aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5)
- aws-sigv4 (1.11.0)
+ aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
babosa (1.0.4)
base64 (0.2.0)
+ bigdecimal (3.3.1)
claide (1.1.0)
colored (1.2)
colored2 (3.1.2)
commander (4.6.0)
highline (~> 2.0.0)
+ csv (3.3.5)
declarative (0.0.20)
digest-crc (0.7.0)
rake (>= 12.0.0, < 14.0.0)
@@ -53,14 +54,14 @@ GEM
faraday-rack (~> 1.0)
faraday-retry (~> 1.0)
ruby2_keywords (>= 0.0.4)
- faraday-cookie_jar (0.0.7)
+ faraday-cookie_jar (0.0.8)
faraday (>= 0.8.0)
- http-cookie (~> 1.0.0)
+ http-cookie (>= 1.0.0)
faraday-em_http (1.0.0)
- faraday-em_synchrony (1.0.0)
+ faraday-em_synchrony (1.0.1)
faraday-excon (1.1.0)
faraday-httpclient (1.0.1)
- faraday-multipart (1.1.0)
+ faraday-multipart (1.1.1)
multipart-post (~> 2.0)
faraday-net_http (1.0.2)
faraday-net_http_persistent (1.2.0)
@@ -70,15 +71,18 @@ GEM
faraday_middleware (1.2.1)
faraday (~> 1.0)
fastimage (2.4.0)
- fastlane (2.227.2)
+ fastlane (2.229.1)
CFPropertyList (>= 2.3, < 4.0.0)
+ abbrev (~> 0.1.2)
addressable (>= 2.8, < 3.0.0)
artifactory (~> 3.0)
aws-sdk-s3 (~> 1.0)
babosa (>= 1.0.3, < 2.0.0)
+ base64 (~> 0.2.0)
bundler (>= 1.12.0, < 3.0.0)
colored (~> 1.2)
commander (~> 4.6)
+ csv (~> 3.3)
dotenv (>= 2.1.1, < 3.0.0)
emoji_regex (>= 0.1, < 4.0)
excon (>= 0.71.0, < 1.0.0)
@@ -98,7 +102,9 @@ GEM
jwt (>= 2.1.0, < 3)
mini_magick (>= 4.9.4, < 5.0.0)
multipart-post (>= 2.0.0, < 3.0.0)
+ mutex_m (~> 0.3.0)
naturally (~> 2.2)
+ nkf (~> 0.2.0)
optparse (>= 0.1.1, < 1.0.0)
plist (>= 3.1.0, < 4.0.0)
rubyzip (>= 2.0.0, < 3.0.0)
@@ -157,37 +163,37 @@ GEM
httpclient (2.9.0)
mutex_m
jmespath (1.6.2)
- json (2.12.0)
- jwt (2.10.1)
+ json (2.16.0)
+ jwt (2.10.2)
base64
logger (1.7.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
- multi_json (1.15.0)
+ multi_json (1.17.0)
multipart-post (2.4.1)
mutex_m (0.3.0)
nanaimo (0.4.0)
- naturally (2.2.1)
+ naturally (2.3.0)
nkf (0.2.0)
- optparse (0.6.0)
+ optparse (0.8.0)
os (1.1.4)
plist (3.7.2)
- public_suffix (6.0.2)
- rake (13.2.1)
+ public_suffix (7.0.0)
+ rake (13.3.1)
representable (3.2.0)
declarative (< 0.1.0)
trailblazer-option (>= 0.1.1, < 0.2.0)
uber (< 0.2.0)
retriable (3.1.2)
- rexml (3.4.1)
+ rexml (3.4.4)
rouge (3.28.0)
ruby2_keywords (0.0.5)
rubyzip (2.4.1)
security (0.1.5)
- signet (0.20.0)
+ signet (0.21.0)
addressable (~> 2.8)
faraday (>= 0.17.5, < 3.a)
- jwt (>= 1.5, < 3.0)
+ jwt (>= 1.5, < 4.0)
multi_json (~> 1.10)
simctl (1.6.10)
CFPropertyList
@@ -217,11 +223,11 @@ GEM
xcpretty (~> 0.2, >= 0.0.7)
PLATFORMS
- arm64-darwin-23
+ arm64-darwin-25
ruby
DEPENDENCIES
fastlane
BUNDLED WITH
- 2.6.9
+ 2.7.2
diff --git a/public/ext/shared/_locales/en/messages.json b/public/ext/shared/_locales/en/messages.json
index a39bfc24..c17c6cad 100644
--- a/public/ext/shared/_locales/en/messages.json
+++ b/public/ext/shared/_locales/en/messages.json
@@ -85,6 +85,24 @@
"settings_global_active_desc": {
"message": "Toggle on/off script injection for the pages you visit"
},
+ "settings_theme_mode": {
+ "message": "Theme mode"
+ },
+ "settings_theme_mode_desc": {
+ "message": "Toggle theme mode"
+ },
+ "settings_theme_mode_auto": {
+ "message": "Auto (System)"
+ },
+ "settings_theme_mode_dark": {
+ "message": "Dark"
+ },
+ "settings_theme_mode_light": {
+ "message": "Light"
+ },
+ "settings_theme_mode_custom": {
+ "message": "Custom"
+ },
"settings_global_exclude_match": {
"message": "Global Exclude Match patterns"
},
diff --git a/scripts/dev-safari.js b/scripts/dev-safari.js
index 34943850..c9236b9f 100644
--- a/scripts/dev-safari.js
+++ b/scripts/dev-safari.js
@@ -186,6 +186,7 @@ async function serverCheck(url) {
/** run development server */
const server = await createServer({
...sharedConfig,
+ publicDir: "public/ext/vendor/",
plugins: [svelte()],
server: {
host: true,
diff --git a/src/ext/action-popup/App.svelte b/src/ext/action-popup/App.svelte
index 5740e7b5..6d593530 100644
--- a/src/ext/action-popup/App.svelte
+++ b/src/ext/action-popup/App.svelte
@@ -242,6 +242,12 @@
}
platform = pltfm.platform;
+ /* Override theme mode */
+ const theme = await settingsStorage.get("theme_mode");
+ if (theme && theme !== "auto") {
+ document.body.style.setProperty("color-scheme", theme);
+ }
+
// display native error if there is
const errorNative = await settingsStorage.get("error_native");
if (errorNative.error) {
diff --git a/src/ext/content-scripts/entry-script-market.js b/src/ext/content-scripts/entry-script-market.js
index 67022e47..45ba17d6 100644
--- a/src/ext/content-scripts/entry-script-market.js
+++ b/src/ext/content-scripts/entry-script-market.js
@@ -9,10 +9,10 @@ async function injection() {
);
}
for (const link of links) {
+ if (link["href"]) url = link["href"];
link.addEventListener(
"click",
(e) => {
- url = link["href"];
e.stopImmediatePropagation();
e.preventDefault();
browser.runtime.sendMessage({ name: "WEB_USERJS_POPUP" });
@@ -23,14 +23,27 @@ async function injection() {
}
async function listeners() {
- browser.runtime.onMessage.addListener(async (message) => {
+ /**
+ * handle messages from background, popup, etc...
+ * @type {import("webextension-polyfill").Runtime.OnMessageListener}
+ */
+ const handleMessage = async (message) => {
if (import.meta.env.MODE === "development") {
console.debug(message, url);
}
if (message === "TAB_CLICK_USERJS") {
- const response = url;
- url = undefined; // respond only once of click event
- return response;
+ return url;
+ }
+ };
+ /** Dynamically remove listeners to avoid memory leaks */
+ if (document.visibilityState === "visible") {
+ browser.runtime.onMessage.addListener(handleMessage);
+ }
+ document.addEventListener("visibilitychange", () => {
+ if (document.hidden) {
+ browser.runtime.onMessage.removeListener(handleMessage);
+ } else {
+ browser.runtime.onMessage.addListener(handleMessage);
}
});
}
diff --git a/src/ext/content-scripts/entry-userscripts.js b/src/ext/content-scripts/entry-userscripts.js
index 5c0f79e7..362bfacb 100644
--- a/src/ext/content-scripts/entry-userscripts.js
+++ b/src/ext/content-scripts/entry-userscripts.js
@@ -1,5 +1,5 @@
import USAPI from "./api.js";
-import { getColor } from "@shared/colors.js";
+import { colors } from "@shared/colors.js";
// code received from background page will be stored in this variable
// code referenced again when strict CSPs block initial injection attempt
@@ -25,19 +25,23 @@ function triageJS(userscript) {
if (document.readyState !== "loading") {
injectJS(userscript);
} else {
- document.addEventListener("DOMContentLoaded", () => {
- injectJS(userscript);
- });
+ document.addEventListener(
+ "DOMContentLoaded",
+ () => injectJS(userscript),
+ { once: true },
+ );
}
} else if (runAt === "document-idle") {
if (document.readyState === "complete") {
injectJS(userscript);
} else {
- document.addEventListener("readystatechange", () => {
+ const handle = () => {
if (document.readyState === "complete") {
injectJS(userscript);
+ document.removeEventListener("readystatechange", handle);
}
- });
+ };
+ document.addEventListener("readystatechange", handle);
}
}
}
@@ -63,13 +67,13 @@ ${userscript.code}
}
const world = injectInto === "content" ? "content" : "page";
if (window.self === window.top) {
- console.info(`Injecting: ${name} %c(js/${world})`, getColor("yellow"));
+ console.info(`Injecting: ${name} %c(js/${world})`, colors.yellow);
} else {
console.info(
`Injecting: ${name} %c(js/${world})%c - %cframe(${label})(${window.location})`,
- getColor("yellow"),
- getColor(),
- getColor("blue"),
+ colors.yellow,
+ colors.inherit,
+ colors.blue,
);
}
if (world === "page") {
@@ -95,13 +99,13 @@ ${userscript.code}
function injectCSS(name, code) {
if (window.self === window.top) {
- console.info(`Injecting ${name} %c(css)`, getColor("green"));
+ console.info(`Injecting ${name} %c(css)`, colors.green);
} else {
console.info(
`Injecting ${name} %c(css)%c - %cframe(${label})(${window.location})`,
- getColor("green"),
- getColor(),
- getColor("blue"),
+ colors.green,
+ colors.inherit,
+ colors.blue,
);
}
// Safari lacks full support for tabs.insertCSS
@@ -233,30 +237,45 @@ async function injection() {
}
function listeners() {
- // listens for messages from background, popup, etc...
- browser.runtime.onMessage.addListener((request) => {
- const name = request.name;
+ /** listen for CSP violations */
+ document.addEventListener("securitypolicyviolation", cspFallback, {
+ once: true,
+ });
+ /**
+ * listens for messages from background, popup, etc...
+ * @type {import("webextension-polyfill").Runtime.OnMessageListener}
+ */
+ const handleMessage = (message) => {
+ const name = message.name;
if (name === "CONTEXT_RUN") {
// from bg script when context-menu item is clicked
// double check to ensure context-menu scripts only run in top windows
if (window !== window.top) return;
-
// loop through context-menu scripts saved to data object and find match
// if no match found, nothing will execute and error will log
- const filename = request.menuItemId;
+ const filename = message.menuItemId;
for (let i = 0; i < data.files.menu.length; i++) {
const item = data.files.menu[i];
if (item.scriptObject.filename === filename) {
- console.info(`Injecting ${filename} %c(js)`, getColor("yellow"));
+ console.info(`Injecting ${filename} %c(js)`, colors.yellow);
injectJS(item);
return;
}
}
console.error(`Couldn't find ${filename} code!`);
}
+ };
+ /** Dynamically remove listeners to avoid memory leaks */
+ if (document.visibilityState === "visible") {
+ browser.runtime.onMessage.addListener(handleMessage);
+ }
+ document.addEventListener("visibilitychange", () => {
+ if (document.hidden) {
+ browser.runtime.onMessage.removeListener(handleMessage);
+ } else {
+ browser.runtime.onMessage.addListener(handleMessage);
+ }
});
- // listen for CSP violations
- document.addEventListener("securitypolicyviolation", cspFallback);
}
async function initialize() {
diff --git a/src/ext/extension-page/App.svelte b/src/ext/extension-page/App.svelte
index 60574f73..1c4ce4d3 100644
--- a/src/ext/extension-page/App.svelte
+++ b/src/ext/extension-page/App.svelte
@@ -19,6 +19,15 @@
}
});
+ /* Override theme mode */
+ $: if ($settings["theme_mode"]) {
+ if ($settings["theme_mode"] === "auto") {
+ document.body.style.removeProperty("color-scheme");
+ } else {
+ document.body.style.setProperty("color-scheme", $settings["theme_mode"]);
+ }
+ }
+
// disables default cmd+s (save) and cmd+f (find) behavior
function preventKeyCommands(e) {
if (e.metaKey && (e.code === "KeyS" || e.code === "KeyF")) {
@@ -58,6 +67,33 @@
window.location.reload();
}
});
+
+ let splitterActive = false;
+ let sidebarHidden = false;
+ let sidebarWidth = "23svw";
+ const sidebarMinWidth = "20rem";
+ const editorMinWidth = "20rem";
+
+ function sidebarSwitch() {
+ if (!sidebarHidden && !$items.find((i) => i.active)) {
+ log.add("Please select or create a new item first.", "info", true);
+ return;
+ }
+ sidebarHidden = !sidebarHidden;
+ }
+
+ function handleSplitterDrag(event) {
+ if (event.type === "mouseup") {
+ splitterActive = false;
+ } else if (event.type === "mousedown") {
+ splitterActive = true;
+ } else if (event.type === "mousemove") {
+ const vw = window.innerWidth;
+ const sw = (event.x / vw) * 100;
+ sidebarWidth = `max(min(${sw}svw, 100svw - ${editorMinWidth}), ${sidebarMinWidth})`;
+ }
+ event.preventDefault();
+ }
@@ -72,10 +108,30 @@
{/if}
{/if}
-
-
-
+
+ {#if !sidebarHidden}
+
+
+ {/if}
+
+{#if splitterActive}
+
+{/if}
{#each $notifications as item (item.id)}
notifications.remove(item.id)} {item} />
@@ -120,6 +176,34 @@
overflow: hidden;
}
+ .resize-mask {
+ background-color: transparent;
+ cursor: col-resize;
+ position: fixed;
+ inset: 0;
+ z-index: 20;
+ }
+
+ .splitter {
+ --splitter-width: 6px;
+ --splitter-offset: calc(var(--splitter-width) / 2);
+ --sidebar-fixed-width: max(var(--sidebar-min-width), var(--sidebar-width));
+
+ background-color: transparent;
+ cursor: col-resize;
+ position: fixed;
+ left: calc(var(--sidebar-fixed-width) - var(--splitter-offset));
+ width: var(--splitter-width);
+ height: 100svh;
+ z-index: 21;
+ transition: background-color 0.2s ease-in-out;
+
+ &:hover,
+ &.dragging {
+ background-color: var(--color-blue);
+ }
+ }
+
ul {
bottom: 1rem;
left: 50%;
diff --git a/src/ext/extension-page/Components/Editor/CodeMirror.svelte b/src/ext/extension-page/Components/Editor/CodeMirror.svelte
index f097189b..b2cbacda 100644
--- a/src/ext/extension-page/Components/Editor/CodeMirror.svelte
+++ b/src/ext/extension-page/Components/Editor/CodeMirror.svelte
@@ -150,11 +150,6 @@
"Cmd-S": () => saveHandler(),
"Cmd-F": () => activateSearch(),
Esc: () => (searchActive = false),
- Tab: (cm) => {
- // convert tabs to spaces and add invisible elements
- const s = Array(cm.getOption("indentUnit") + 1).join(" ");
- cm.replaceSelection(s);
- },
},
});
diff --git a/src/ext/extension-page/Components/Editor/Editor.svelte b/src/ext/extension-page/Components/Editor/Editor.svelte
index 0bdd3507..e0b7c719 100644
--- a/src/ext/extension-page/Components/Editor/Editor.svelte
+++ b/src/ext/extension-page/Components/Editor/Editor.svelte
@@ -13,6 +13,10 @@
import iconDownload from "@shared/img/icon-download.svg?raw";
import iconTrash from "@shared/img/icon-trash.svg?raw";
import iconSync from "@shared/img/icon-sync.svg?raw";
+ import iconSidebar from "@shared/img/icon-sidebar.svg?raw";
+
+ export let sidebarHidden;
+ export let sidebarSwitch;
// the data the populates editor elements
let canUpdate;
@@ -167,13 +171,22 @@
No Item Selected
{/if}