From 4e355eb2b9b8138eff019f308a4a49ba6abddd58 Mon Sep 17 00:00:00 2001
From: nexalapp <108235158+nexalapp@users.noreply.github.com>
Date: Thu, 15 Oct 2026 14:47:09 -0600
Subject: [PATCH] themeSwitch: fix theme picking, snippets switching off, and
NSFW blur on the player (2.1.3)
---
.../snippets/global/blur-nsfw-images.css | 16 +-
plugins/themeSwitch/themeSwitch.yml | 2 +-
plugins/themeSwitch/themeSwitchMain.js | 177 +++++++++++++-----
3 files changed, 143 insertions(+), 52 deletions(-)
diff --git a/plugins/themeSwitch/assets/snippets/global/blur-nsfw-images.css b/plugins/themeSwitch/assets/snippets/global/blur-nsfw-images.css
index a309ebc1..34489199 100644
--- a/plugins/themeSwitch/assets/snippets/global/blur-nsfw-images.css
+++ b/plugins/themeSwitch/assets/snippets/global/blur-nsfw-images.css
@@ -19,10 +19,24 @@ img.performer,
.scene-details .pre,
#scene-tabs-tabpane-scene-file-info-panel span.col-8.text-truncate > a,
.gallery .flexbin img,
-.movie-details .logo {
+.movie-details .logo,
+.group-card-image,
+.studio-card-image,
+.image-image,
+.gallery-image {
filter: blur(12px);
}
+/* Scene player (video.js, used since Stash v0.17) and its scrubber thumbnails.
+ !important: the player's own styles and themes can otherwise win. */
+.video-js video,
+.video-js .vjs-tech,
+.video-js .vjs-poster,
+.video-js .vjs-poster img,
+.scrubber-item {
+ filter: blur(20px) !important;
+}
+
.scene-card-video {
filter: blur(13px);
}
diff --git a/plugins/themeSwitch/themeSwitch.yml b/plugins/themeSwitch/themeSwitch.yml
index 1e27f4b5..fdfbdcb5 100644
--- a/plugins/themeSwitch/themeSwitch.yml
+++ b/plugins/themeSwitch/themeSwitch.yml
@@ -1,7 +1,7 @@
name: Theme Switch
# requires: CommunityScriptsUILibrary
description: Theme and CSS script manager located in main menu bar top right.
-version: 2.1.2
+version: 2.1.3
url: https://discourse.stashapp.cc/t/theme-switch/1414
ui:
requires:
diff --git a/plugins/themeSwitch/themeSwitchMain.js b/plugins/themeSwitch/themeSwitchMain.js
index 4759c0c6..f6f3233a 100644
--- a/plugins/themeSwitch/themeSwitchMain.js
+++ b/plugins/themeSwitch/themeSwitchMain.js
@@ -12,7 +12,7 @@
const svgChevUP =
'';
const svgBTN =
- '';
+ '';
let menuCreated = false;
@@ -41,6 +41,35 @@
lis.forEach((li) => ul.appendChild(li));
}
+ // Keep the open menu on screen. On phones Stash moves the nav bar to the
+ // bottom, so a menu that always drops downward opens below the viewport.
+ function placeMenu(button, menu) {
+ const gap = 8;
+ const b = button.getBoundingClientRect();
+ const host = menu.offsetParent || button.parentElement;
+ const h = host.getBoundingClientRect();
+ const vw = document.documentElement.clientWidth;
+ const above = b.top > window.innerHeight - b.bottom;
+ const room = above ? b.top - gap * 2 : window.innerHeight - b.bottom - gap * 2;
+
+ menu.style.setProperty("max-width", `${vw - gap * 2}px`);
+ menu.style.setProperty("min-width", `min(20rem, ${vw - gap * 2}px)`);
+ menu.style.setProperty("max-height", `${Math.max(room, 160)}px`, "important");
+ menu.style.setProperty("overflow-y", "auto");
+ if (above) {
+ menu.style.setProperty("top", "auto");
+ menu.style.setProperty("bottom", `${h.bottom - b.top + gap}px`);
+ } else {
+ menu.style.setProperty("top", `${b.bottom - h.top + gap}px`);
+ menu.style.setProperty("bottom", "auto");
+ }
+
+ // Line the menu's right edge up with the button, then clamp it to the screen.
+ const w = menu.getBoundingClientRect().width;
+ const left = Math.min(Math.max(b.right - w, gap), vw - w - gap);
+ menu.style.setProperty("left", `${left - h.left}px`);
+ }
+
function menuOrderResetButton(elementToAppend) {
if (!document.getElementById("themeSwitchPlugin-menuOrderReset")) {
const resetButton = document.createElement("button");
@@ -248,6 +277,10 @@
}
}
+ // Bump with the plugin version so browsers fetch updated snippets instead
+ // of reusing a cached copy (Stash sends no cache headers for plugin assets).
+ const ASSET_VERSION = "2.1.3";
+
function addStyleSheet(key, path) {
console.log(key, path);
const styleSheet = document.createElement("link");
@@ -256,7 +289,7 @@
document.querySelector("base")?.getAttribute("href") ?? "/";
styleSheet.setAttribute(
"href",
- `${serverURL}plugin/themeSwitch/assets${path}`
+ `${serverURL}plugin/themeSwitch/assets${path}?v=${ASSET_VERSION}`
);
styleSheet.setAttribute("rel", "stylesheet");
styleSheet.setAttribute("type", "text/css");
@@ -264,59 +297,94 @@
document.getElementsByTagName("head")[0].appendChild(styleSheet);
}
- async function applyCSS(category, key, path, pluginId, pluginSrc) {
+ // Picking a theme enables exactly that theme's plugin and disables every
+ // other theme plugin in the list, including ones enabled from the Plugins
+ // page, so themes never stack. On page load only path-based themes need
+ // re-attaching; plugin themes load themselves.
+ async function applyCSS(category, key, path, pluginId, pluginSrc, picked) {
if (category === "Themes") {
- // Turn Off old Theme
- let regex = /(themeSwitchPlugin-theme-.*)/;
+ if (!picked) {
+ // Page load: re-attach a path-based theme; plugin themes load themselves.
+ if (path && !document.getElementById(key)) addStyleSheet(key, path);
+ return;
+ }
+ // Turn off the old theme in storage and drop any path-based sheet.
+ const regex = /(themeSwitchPlugin-theme-.*)/;
for (let i = 0; i < localStorage.length; i++) {
- let storageKey = localStorage.key(i);
- let match = storageKey.match(regex);
- if (
- match &&
- storageKey !== key &&
- JSON.parse(localStorage.getItem(storageKey)).active === true
- ) {
- setObject(storageKey, category, false);
- let element = document.getElementById(storageKey);
- if (element && !pluginId) {
- element.remove();
- } else {
- const oldThemePluginId = getDataFromKey(storageKey, "pluginId");
- if (oldThemePluginId) {
- await enablePlugin(oldThemePluginId, false);
- setTimeout(() => {
- location.reload();
- }, 1000);
- }
- }
+ const storageKey = localStorage.key(i);
+ if (storageKey.match(regex) && storageKey !== key) {
+ await setObject(storageKey, category, false);
+ document.getElementById(storageKey)?.remove();
}
}
+ await setObject(key, category, true);
+ if (path && !document.getElementById(key)) addStyleSheet(key, path);
+
+ let installed = false;
+ if (pluginId && !(await isPluginInstalled(pluginId))) {
+ // installPackages runs as a job; wait for the plugin to register.
+ await installPlugin(pluginId, pluginSrc);
+ for (let i = 0; i < 30 && !(await isPluginInstalled(pluginId)); i++) {
+ await new Promise((r) => setTimeout(r, 500));
+ }
+ installed = true;
+ }
- const theme = JSON.parse(localStorage.getItem(key));
-
- if (!theme && key != "themeSwitchPlugin-theme-default") {
- setObject(key, category, true).then(() => {
- addStyleSheet(key, path);
- });
- } else if (!theme && key === "themeSwitchPlugin-theme-default") {
- setObject(key, category, true);
- } else if (theme && key === "themeSwitchPlugin-theme-default") {
- setObject(key, category, true);
- } else if (theme && key !== "themeSwitchPlugin-theme-default") {
- setObject(key, category, true).then(async () => {
- if (pluginId) {
- if (!(await isPluginInstalled(pluginId))) {
- await installPlugin(pluginId, pluginSrc);
- }
- await enablePlugin(pluginId, true);
- setTimeout(() => {
- location.reload();
- }, 1000);
- } else if (path) {
- addStyleSheet(key, path);
+ // Exactly one theme plugin on: the chosen one (none for Default or a
+ // path-based theme).
+ const data = await csLib
+ .callGQL({
+ query: `query Plugins{plugins{id enabled paths{css javascript}}}`,
+ })
+ .catch((err) => console.error(err));
+ const plugins = new Map((data?.plugins ?? []).map((p) => [p.id, p]));
+ const enabledMap = {};
+ let needsReload = installed;
+ for (const theme of window.themeSwitchCSS.Themes) {
+ const plugin = plugins.get(theme.pluginId);
+ if (!plugin) continue;
+ const want = theme.pluginId === pluginId;
+ if (plugin.enabled !== want) enabledMap[theme.pluginId] = want;
+ // A theme's script can't be unloaded or loaded late: switching away
+ // from or to a theme that ships JavaScript still needs a reload.
+ if (plugin.paths?.javascript?.length && plugin.enabled !== want) {
+ needsReload = true;
+ }
+ }
+ if (Object.keys(enabledMap).length > 0) {
+ await csLib
+ .callGQL({
+ query:
+ "mutation SetPluginsEnabled($enabledMap: BoolMap!) { setPluginsEnabled(enabledMap: $enabledMap) }",
+ variables: { enabledMap },
+ })
+ .catch((err) => console.error(err));
+ }
+ if (needsReload) {
+ location.reload();
+ return;
+ }
+ // CSS-only themes switch in place: drop the other themes' stylesheets
+ // and add the chosen one's, with a cache-busting query so a stale copy
+ // is never reused.
+ for (const theme of window.themeSwitchCSS.Themes) {
+ const plugin = plugins.get(theme.pluginId);
+ if (!plugin) continue;
+ const marker = `/plugin/${theme.pluginId}/`;
+ const links = [...document.querySelectorAll("link[rel=stylesheet]")]
+ .filter((l) => l.href.includes(marker));
+ if (theme.pluginId !== pluginId) {
+ links.forEach((l) => l.remove());
+ } else if (!links.length) {
+ for (const href of plugin.paths?.css ?? []) {
+ const link = document.createElement("link");
+ link.rel = "stylesheet";
+ link.type = "text/css";
+ link.href = `${href}${href.includes("?") ? "&" : "?"}t=${Date.now()}`;
+ document.head.appendChild(link);
}
- });
+ }
}
} else {
// CSS Other than themes
@@ -429,6 +497,7 @@
themesDiv.classList.add("show");
themesDiv.style =
"position: absolute; top: 133%; left: 0px; margin: 0px; opacity: 1; pointer-events: auto; width: max-content; min-width: 20rem; left: -575%;";
+ placeMenu(themeSwitchPlugin, themesDiv);
} else if (!isClickInside && !isClickInsideThemesDiv) {
themeSwitchPlugin.setAttribute("aria-expanded", "false");
themesDiv.classList.remove("show");
@@ -547,7 +616,8 @@
themeData.key,
themeData.path,
themeData.pluginId,
- themeData.pluginSrc
+ themeData.pluginSrc,
+ true
);
};
})(themeData),
@@ -701,7 +771,14 @@
selectedTheme = JSON.parse(localStorage.getItem(key));
if (selectedTheme.active === true) {
appliedThemeOtherThanDefault.push("True");
- applyCSS(selectedTheme.category, key, getDataFromKey(key, "path"));
+ // init() runs on every stash:location event (page changes, filter
+ // changes). Only attach what is missing: going through applyCSS
+ // here toggled an already-attached snippet back off.
+ if (selectedTheme.category === "Themes") {
+ applyCSS(selectedTheme.category, key, getDataFromKey(key, "path"));
+ } else if (!document.getElementById(key)) {
+ addStyleSheet(key, getDataFromKey(key, "path"));
+ }
}
}
}