mirror of
https://github.com/stashapp/CommunityScripts.git
synced 2026-02-04 01:52:30 -06:00
Add performer-poster-backdrop plugin (#648)
This commit is contained in:
parent
9374193341
commit
e7aea56b16
89
plugins/performer-poster-backdrop/README.md
Normal file
89
plugins/performer-poster-backdrop/README.md
Normal file
@ -0,0 +1,89 @@
|
||||
# Performer Poster Backdrop (Stash UI Plugin)
|
||||
|
||||
Adds a blurred poster-style backdrop behind performer headers using the performer’s poster image.
|
||||
|
||||

|
||||
|
||||
|
||||
## Features
|
||||
|
||||
- Applies only to **Performer pages**
|
||||
- Uses the performer’s **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 performer’s 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 |
@ -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;
|
||||
}
|
||||
164
plugins/performer-poster-backdrop/performer-poster-backdrop.js
Normal file
164
plugins/performer-poster-backdrop/performer-poster-backdrop.js
Normal 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);
|
||||
})();
|
||||
@ -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: "0–1 (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
|
||||
Loading…
x
Reference in New Issue
Block a user