Five problems in Theme Switch, all reproducible on Stash v0.31:
1. **First pick… of a plugin theme does nothing.** When a theme has never been picked, `applyCSS` takes the `!theme` branch and calls `addStyleSheet(key, path)`. Plugin themes have no `path`, so it requests `plugin/themeSwitch/assetsundefined` and never installs or enables the plugin. The radio shows the theme as selected but the page stays default; picking it a second time works.
2. **Themes stack.** Switching only disables the plugin Theme Switch remembers enabling (in localStorage). Theme plugins enabled from Settings → Plugins, or in another browser, stay on, so several themes load at once and fight.
3. **Snippets switch themselves off.** `init()` runs on every `stash:location` event (any page change, including changing a filter or sort, and once more on load) and re-applies active snippets through `applyCSS`, which treats an already-attached snippet as a click and removes it, saving `active: false`. With "Blur NSFW images" on, every filter change and every refresh toggled the blur.
4. **"Blur NSFW images" doesn't cover the scene player.** The snippet targets JW Player classes (`.jw-video`, `.jw-preview`) from before Stash switched to video.js, so the video, its poster and the scrubber thumbnails stay sharp. Group (formerly movie), studio, image and gallery images were also missing; the "Un-blur on mouse over" snippet already listed them.
5. **Updated snippets never reach the browser.** Snippets are added after page load and Stash sends no cache headers for plugin assets, so browsers keep the old CSS even after a hard reload.
This change:
- On a pick, installs the theme if needed (waiting for the install job to register it), then enables exactly that theme's plugin and disables every other theme plugin in the list in one `setPluginsEnabled` call. Picking Default disables them all.
- CSS-only themes switch in place: the other themes' stylesheets are removed and the chosen one is added (cache-busted), with no page reload. Switching to or from a theme that ships JavaScript still reloads, since a script can't be unloaded.
- On navigation, `init()` only attaches what is missing: path-based themes and active snippets. It never toggles anything and makes no GraphQL calls.
- Removes path-based theme stylesheets when switching away from them.
- Adds `.video-js video`, `.video-js .vjs-tech`, `.video-js .vjs-poster`, `.video-js .vjs-poster img`, `.scrubber-item`, `.group-card-image`, `.studio-card-image`, `.image-image` and `.gallery-image` to the blur snippet. The old JW Player selectors are kept. The player rules are `!important` so player styles and themes can't undo them.
- Snippet URLs carry `?v=<plugin version>`, so an update reaches users without clearing their cache.
**Since this PR was opened**
- **The menu opens off screen on phones.** Stash moves the nav bar to the bottom on phones, so the menu, which always drops downward, opened below the viewport and nothing appeared. It now opens toward the side of the button with more room, is clamped to the screen width, and scrolls if it is taller than the space.
- **The icon ignores the theme.** Its white parts were a fixed `#fafafa`, so on light themes the icon was invisible and on others it didn't match the nav icons beside it. Those parts now use `currentColor`, so the icon follows the nav colour; the coloured dots are unchanged. The icon's inline `<style>` (`.s0`–`.s4`) also leaked page-wide; the fills are now attributes.
### Testing
Tested on Stash v0.31.1 (Docker) in Chrome:
- With no Theme Switch history and an unrelated theme plugin enabled from Settings → Plugins, one pick of a theme enabled it and disabled the other; switching between themes always left exactly one enabled, with the page and the server agreeing; CSS-only themes switched without a reload, themes with JavaScript reloaded once; Default disabled them all.
- With "Blur NSFW images" on, it stayed on across sort and filter changes, in-page searches and repeated refreshes (localStorage stayed `active: true`, stylesheet attached once).
- On a scene page, the video, its poster and the scrubber thumbnails are blurred.
- At 390×844 with the nav bar at the bottom, the menu opens above the button and stays on screen; at 1440×900 it opens below. The icon matches the nav icons in each of the community themes.
- `node validate.js --ci` passes.
### AI disclosure
This change was written with Claude (Anthropic) in Claude Code. I reviewed the code and tested it on my own Stash install before submitting.
🤖 Generated with [Claude Code](https://claude.com/claude-code)