From 2c408e539649c1afb0693e6c80d04a907caa3d06 Mon Sep 17 00:00:00 2001 From: nexalapp <108235158+nexalapp@users.noreply.github.com> Date: Fri, 2 Oct 2026 18:26:37 -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 | 145 ++++++++++++------ 3 files changed, 112 insertions(+), 51 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..f134ef32 100644 --- a/plugins/themeSwitch/themeSwitchMain.js +++ b/plugins/themeSwitch/themeSwitchMain.js @@ -248,6 +248,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 +260,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 +268,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 @@ -547,7 +586,8 @@ themeData.key, themeData.path, themeData.pluginId, - themeData.pluginSrc + themeData.pluginSrc, + true ); }; })(themeData), @@ -701,7 +741,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")); + } } } }