Add performer-poster-backdrop plugin (#648)

This commit is contained in:
Mike 2025-12-23 17:28:39 -05:00 committed by GitHub
parent 9374193341
commit e7aea56b16
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 337 additions and 0 deletions

View File

@ -0,0 +1,89 @@
# Performer Poster Backdrop (Stash UI Plugin)
Adds a blurred poster-style backdrop behind performer headers using the performers poster image.
![Performer Poster Backdrop example](https://raw.githubusercontent.com/worryaboutstuff/performer-poster-backdrop/main/assets/performer-poster-backdrop-example.png)
## Features
- Applies only to **Performer pages**
- Uses the performers **poster image** as a background layer
- Adjustable:
- Opacity
- Blur strength
- Vertical image alignment
- Supports **per-performer Y-offset overrides**
- Blank settings automatically fall back to defaults
## Installation
1. Copy the following files into your Stash UI plugins directory:
- `performer-poster-backdrop.yml`
- `performer-poster-backdrop.js`
- `performer-poster-backdrop.css`
2. In Stash, go to **Settings → Plugins**
3. Find **Performer Poster Backdrop**
4. Adjust settings if desired and click **Confirm**
5. Refresh a performer page
## Settings
### Backdrop opacity
Controls how visible the backdrop is.
- Range: `0``1`
- Default: `1`
- Examples:
- `0.7` → subtle
- `0.5` → very soft
- `0` → invisible
Leaving this field blank uses the default.
### Backdrop blur
Controls how blurred the backdrop appears (in pixels).
- Default: `10`
- Examples:
- `5` → light blur
- `15` → strong blur
- `0` → no blur
Leaving this field blank uses the default.
### Default Y offset
Controls the vertical alignment of the backdrop image.
- Range: `0``100`
- Default: `20`
- Meaning:
- `0` → favor top of image
- `50` → center
- `100` → favor bottom
Leaving this field blank uses the default.
## Per-performer Y overrides
Use this when a specific performers poster needs different vertical positioning.
Enter overrides as a **comma-separated list** in a single text field.
### Format
`PERFORMER_ID:OFFSET`
### Example
`142:35, 219:20, 501:50`
Accepted separators:
- `:` (recommended)
- `=`
- `-`
Whitespace is ignored.

Binary file not shown.

After

Width:  |  Height:  |  Size: 577 KiB

View File

@ -0,0 +1,54 @@
.pb-hero {
position: absolute;
inset: 2px;
background-size: cover;
background-position: center var(--pb-y, 20%);
opacity: var(--pb-opacity, 1);
filter: blur(var(--pb-blur, 10px));
transform: scale(1.2);
z-index: 0;
pointer-events: none;
}
.pb-hero::after {
content: "";
position: absolute;
inset: 0;
background:
radial-gradient(
ellipse at center,
rgba(0,0,0,0.0) 0%,
rgba(0,0,0,0.35) 55%,
rgba(0,0,0,0.75) 100%
),
linear-gradient(
to bottom,
rgba(0,0,0,0.15),
rgba(0,0,0,0.75)
);
box-shadow:
inset 0 0 0 1px rgba(255,255,255,0.025),
inset 0 14px 28px rgba(0,0,0,0.55),
inset 0 -22px 36px rgba(0,0,0,0.85);
}
/* IMPORTANT: only the REAL header */
#performer-page .detail-header.full-width {
position: relative;
overflow: hidden;
}
/* Lift content above banner ONLY in the REAL header */
#performer-page .detail-header.full-width > *:not(.pb-hero) {
position: relative;
z-index: 1;
}
/* Hide while editing performer */
#performer-page .detail-header.edit.full-width .pb-hero {
display: none;
}

View File

@ -0,0 +1,164 @@
(function () {
const HERO_CLASS = "pb-hero";
const PLUGIN_ID = "performer-poster-backdrop";
// HARD DEFAULTS (used when fields are blank)
const DEFAULTS = {
opacity: 1, // 0..1
blur: 10, // px
y: 20, // %
};
let opacity = DEFAULTS.opacity;
let blur = DEFAULTS.blur;
let defaultY = DEFAULTS.y;
let overrides = new Map();
let lastSettingsFetch = 0;
const isBlank = (v) =>
v === null || v === undefined || String(v).trim() === "";
function isPerformerRoute() {
return /^\/performers\/\d+/.test(location.pathname);
}
function getPerformerIdFromPath() {
const m = location.pathname.match(/^\/performers\/(\d+)/);
return m ? String(Number(m[1])) : null;
}
function getHeader() {
return document.querySelector("#performer-page .detail-header.full-width");
}
function getStickyHeader() {
return document.querySelector("#performer-page .sticky.detail-header");
}
function getPosterImg() {
return (
document.querySelector("#performer-page .detail-header-image img.performer") ||
document.querySelector("#performer-page img.performer")
);
}
function clamp(n, min, max, fallback) {
const x = Number(n);
if (!Number.isFinite(x)) return fallback;
return Math.min(max, Math.max(min, x));
}
// COMMA-SEPARATED overrides: "142:35, 219:20"
function parseOverrides(text) {
const map = new Map();
if (isBlank(text)) return map;
text.split(",").forEach((chunk) => {
const s = chunk.trim();
if (!s) return;
// Accept 142:35, 142-35, 142=35
const m = s.match(/^(\d+)\s*[:=-]\s*(\d{1,3})$/);
if (!m) return;
const id = String(Number(m[1]));
const pct = clamp(m[2], 0, 100, DEFAULTS.y);
map.set(id, pct);
});
return map;
}
function removeHero(el) {
el?.querySelector("." + HERO_CLASS)?.remove();
}
function upsertHero(header, url) {
let hero = header.querySelector("." + HERO_CLASS);
if (!hero) {
hero = document.createElement("div");
hero.className = HERO_CLASS;
header.prepend(hero);
}
hero.style.backgroundImage = `url("${url}")`;
return hero;
}
function apply(hero) {
hero.style.setProperty("--pb-opacity", opacity);
hero.style.setProperty("--pb-blur", `${blur}px`);
const pid = getPerformerIdFromPath();
const y = pid && overrides.has(pid) ? overrides.get(pid) : defaultY;
hero.style.setProperty("--pb-y", `${y}%`);
}
async function refreshSettings() {
if (Date.now() - lastSettingsFetch < 5000) return;
lastSettingsFetch = Date.now();
try {
const res = await fetch("/graphql", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
query: `{ configuration { plugins } }`,
}),
});
if (!res.ok) return;
const cfg = (await res.json())
?.data?.configuration?.plugins?.[PLUGIN_ID];
if (!cfg) return;
opacity = isBlank(cfg.opacity)
? DEFAULTS.opacity
: clamp(cfg.opacity, 0, 1, DEFAULTS.opacity);
blur = isBlank(cfg.blur)
? DEFAULTS.blur
: clamp(cfg.blur, 0, 100, DEFAULTS.blur);
defaultY = isBlank(cfg.defaultYOffset)
? DEFAULTS.y
: clamp(cfg.defaultYOffset, 0, 100, DEFAULTS.y);
overrides = isBlank(cfg.perPerformerOffsets)
? new Map()
: parseOverrides(cfg.perPerformerOffsets);
const hero = getHeader()?.querySelector("." + HERO_CLASS);
if (hero) apply(hero);
} catch {
// silent fail — plugin should never break UI
}
}
function run() {
removeHero(getStickyHeader());
const header = getHeader();
if (!header || !isPerformerRoute()) return removeHero(header);
const img = getPosterImg();
if (!img) return;
const hero = upsertHero(header, img.currentSrc || img.src);
apply(hero);
refreshSettings();
}
new MutationObserver(run).observe(document.body, {
childList: true,
subtree: true,
});
["load", "resize", "popstate"].forEach((e) =>
window.addEventListener(e, () => setTimeout(run, 50))
);
setTimeout(run, 50);
})();

View File

@ -0,0 +1,30 @@
name: Performer Poster Backdrop
description: Adds a blurred poster backdrop to performer pages.
version: 1.0.3
ui:
javascript:
- performer-poster-backdrop.js
css:
- performer-poster-backdrop.css
settings:
opacity:
displayName: Backdrop opacity
description: "01 (leave blank for default: 1)"
type: STRING
blur:
displayName: Backdrop blur
description: "Pixels (leave blank for default: 10)"
type: STRING
defaultYOffset:
displayName: Default Y offset
description: "Background position % (leave blank for default: 20)"
type: STRING
perPerformerOffsets:
displayName: Per-performer Y overrides
description: "Comma seperated: performerId:percent (blank = none)"
type: STRING