style(frontend): Prettier config + one-time repo-wide format pass

Prettier 3 (pinned), printWidth 100, double quotes — chosen to match the existing
hand-formatting and minimise reflow. This commit is ONLY the mechanical format pass
(`prettier --write`) plus the config/ignore/scripts, isolated so it never pollutes a
feature diff. Verified behaviour-neutral: typecheck, eslint (0 errors), and vitest
all green before and after. See .git-blame-ignore-revs — `git blame` skips this sha.
This commit is contained in:
2026-07-21 02:26:40 +02:00
parent 04ecd8b3a7
commit e257e2aca0
101 changed files with 4388 additions and 2641 deletions
+170 -54
View File
@@ -126,8 +126,14 @@ function subShadowCss(px: number, color: string): string {
const o = px;
const b = Math.max(1, Math.round(px / 2));
const dirs: [number, number][] = [
[o, 0], [-o, 0], [0, o], [0, -o],
[o, o], [o, -o], [-o, o], [-o, -o],
[o, 0],
[-o, 0],
[0, o],
[0, -o],
[o, o],
[o, -o],
[-o, o],
[-o, -o],
];
const outline = dirs.map(([x, y]) => `${x}px ${y}px ${b}px ${color}`).join(",");
return `text-shadow:${outline},0 0 ${o * 2}px ${color};`;
@@ -147,7 +153,8 @@ function clockParts(now: Date, p: PlexPlayerPrefs): { time: string; date: string
const yyyy = now.getFullYear();
const mon = now.toLocaleString(loc, { month: "short" }).replace(/\./g, "").toUpperCase();
const dow = now.toLocaleString(loc, { weekday: "short" }).replace(/\./g, "");
const date = p.clockDateStyle === "hu" ? `${yyyy}-${mon}-${dd} ${dow}` : `${dd}-${mon}-${yyyy} ${dow}`;
const date =
p.clockDateStyle === "hu" ? `${yyyy}-${mon}-${dd} ${dow}` : `${dd}-${mon}-${yyyy} ${dow}`;
return { time, date };
}
// hls.js plays our non-zero-start `-copyts` HLS stream ~1.0s BEHIND the media clock (measured by
@@ -242,7 +249,9 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
}, [prefs.subOffset]);
const [menuOpen, setMenuOpen] = useState(false); // gear = tuning (tabbed)
const [tracksOpen, setTracksOpen] = useState(false); // separate quick menu = audio/subtitle tracks
const [settingsTab, setSettingsTab] = useState<"sync" | "playback" | "subtitle" | "clock">("sync");
const [settingsTab, setSettingsTab] = useState<"sync" | "playback" | "subtitle" | "clock">(
"sync",
);
const audioRef = useRef<number | null>(null);
const menuOpenRef = useRef(false);
const menuRef = useRef<HTMLDivElement>(null);
@@ -263,7 +272,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
tracksRef.current = node as HTMLDivElement | null;
tracksFade.ref(node);
},
[tracksFade.ref]
[tracksFade.ref],
);
// Keyboard-shortcut cheat sheet (toggled with "h") + a live wall clock for the top bar.
const [helpOpen, setHelpOpen] = useState(false);
@@ -313,7 +322,8 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
// hls.js plays our non-zero-start copyts stream ~1s BEHIND the media clock (see HLS_SUB_LAG);
// fold that into the session start so the seekbar clock, seeking AND the subtitle shift are all
// content-accurate. Direct-play (start_s 0) uses the file's real clock — no compensation.
const trueStart = sess.mode === "hls" && sess.start_s > 0 ? sess.start_s - HLS_SUB_LAG : sess.start_s;
const trueStart =
sess.mode === "hls" && sess.start_s > 0 ? sess.start_s - HLS_SUB_LAG : sess.start_s;
sessionStartRef.current = trueStart;
setSessionK(trueStart);
// Belt-and-suspenders resume: the play() fired on MANIFEST_PARSED / loadedmetadata can be
@@ -466,7 +476,9 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
// sees the resume position when the user actually stops — not a timeline call every 10 seconds.
const flush = (final = false) => {
if (absRef.current > 0 && durationRef.current > 0)
api.plexProgress(id, Math.floor(absRef.current), durationRef.current, final).catch(() => {});
api
.plexProgress(id, Math.floor(absRef.current), durationRef.current, final)
.catch(() => {});
};
const iv = window.setInterval(flush, 10000);
// Pausing is a settled position — push it to Plex now rather than waiting for unmount/unload.
@@ -544,8 +556,14 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
},
[applyVolume, patchPrefs],
);
const nudgeVolume = useCallback((d: number) => setVol(clamp01(prefs.volume + d), clamp01(prefs.volume + d) === 0), [prefs.volume, setVol]);
const toggleMute = useCallback(() => setVol(prefs.volume, !prefs.muted), [prefs.volume, prefs.muted, setVol]);
const nudgeVolume = useCallback(
(d: number) => setVol(clamp01(prefs.volume + d), clamp01(prefs.volume + d) === 0),
[prefs.volume, setVol],
);
const toggleMute = useCallback(
() => setVol(prefs.volume, !prefs.muted),
[prefs.volume, prefs.muted, setVol],
);
// Re-apply the persisted volume/mute to the <video> each time a session becomes ready (the element
// is fresh after a load/seek-restart, and starts at its default volume otherwise).
useEffect(() => {
@@ -572,29 +590,30 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
return () => document.removeEventListener("fullscreenchange", onFs);
}, []);
const go = useCallback(
(otherId: string | null | undefined) => {
if (otherId) {
// Stop the outgoing media BEFORE switching. If the next item fails to load (e.g. no local
// file), loadSession returns without replacing the <video> source, leaving the old episode
// still playing — it would then fire 'ended' against the NEW id and mark the wrong item
// watched (and scrobble it to Plex). Pausing here severs that stale-tail race.
try {
videoRef.current?.pause();
} catch {
/* ignore */
}
setAbs(0);
setId(otherId);
const go = useCallback((otherId: string | null | undefined) => {
if (otherId) {
// Stop the outgoing media BEFORE switching. If the next item fails to load (e.g. no local
// file), loadSession returns without replacing the <video> source, leaving the old episode
// still playing — it would then fire 'ended' against the NEW id and mark the wrong item
// watched (and scrobble it to Plex). Pausing here severs that stale-tail race.
try {
videoRef.current?.pause();
} catch {
/* ignore */
}
},
[],
);
setAbs(0);
setId(otherId);
}
}, []);
// Prev/next: from the playlist queue when playing one, else the item's own episode neighbours.
const qIdx = queue ? queue.indexOf(id) : -1;
const prevId = queue ? (qIdx > 0 ? queue[qIdx - 1] : null) : detail?.prev_id;
const nextId = queue ? (qIdx >= 0 && qIdx < queue.length - 1 ? queue[qIdx + 1] : null) : detail?.next_id;
const nextId = queue
? qIdx >= 0 && qIdx < queue.length - 1
? queue[qIdx + 1]
: null
: detail?.next_id;
// Download the ORIGINAL physical file (no re-encode/repackage). One user gesture, direct link.
const download = useCallback(() => {
@@ -613,7 +632,10 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
audioRef.current = ord;
setAudioOrd(ord);
// Remember the LANGUAGE (not the ord) so F5 / the next item restores this choice. Menu stays open.
patchPrefs({ audioLang: ord == null ? "" : (detail?.audio_streams.find((s) => s.ord === ord)?.language ?? "") });
patchPrefs({
audioLang:
ord == null ? "" : (detail?.audio_streams.find((s) => s.ord === ord)?.language ?? ""),
});
if (multiAudioRef.current && hlsRef.current) {
// Multi-rendition: switch the audio track CLIENT-SIDE — instant, same timeline, no restart.
try {
@@ -632,7 +654,10 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
const changeSubtitle = useCallback(
(ord: number | null) => {
setSubOrd(ord);
patchPrefs({ subLang: ord == null ? "" : (detail?.subtitle_streams.find((s) => s.ord === ord)?.language ?? "") });
patchPrefs({
subLang:
ord == null ? "" : (detail?.subtitle_streams.find((s) => s.ord === ord)?.language ?? ""),
});
},
[detail, patchPrefs],
);
@@ -677,7 +702,9 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
const settingsTabs = [
{ key: "sync" as const, label: t("plex.player.sync") },
{ key: "playback" as const, label: t("plex.player.playback") },
...(textSubs.length > 0 ? [{ key: "subtitle" as const, label: t("plex.player.subStyle") }] : []),
...(textSubs.length > 0
? [{ key: "subtitle" as const, label: t("plex.player.subStyle") }]
: []),
{ key: "clock" as const, label: t("plex.player.clock") },
];
const activeTab = settingsTabs.some((x) => x.key === settingsTab) ? settingsTab : "sync";
@@ -829,7 +856,8 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
case "Backspace":
// HTPC-style "stop & back to the feed" — mirrors the mouse Back button (shared cascade).
e.preventDefault();
if (skipProgressRef.current != null) cancelAutoSkipRef.current(); // cancel auto-skip, don't go back
if (skipProgressRef.current != null)
cancelAutoSkipRef.current(); // cancel auto-skip, don't go back
else handleBack();
break;
case "Escape":
@@ -845,7 +873,22 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
};
window.addEventListener("keydown", onKey);
return () => window.removeEventListener("keydown", onKey);
}, [togglePlay, toggleFs, seekTo, go, nudgeVolume, toggleMute, cycleAudio, cycleSubtitle, detail, prevId, nextId, onClose, wake, handleBack]);
}, [
togglePlay,
toggleFs,
seekTo,
go,
nudgeVolume,
toggleMute,
cycleAudio,
cycleSubtitle,
detail,
prevId,
nextId,
onClose,
wake,
handleBack,
]);
const activeMarker: PlexMarker | undefined = detail?.markers.find(
(m) => abs >= m.start_s && abs < m.end_s - 1,
@@ -919,7 +962,14 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
skipTimerRef.current = null;
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [activeMarker?.type, activeMarker?.start_s, prefs.autoSkipIntro, prefs.autoSkipCredits, prefs.autoSkipDelay, doSkip]);
}, [
activeMarker?.type,
activeMarker?.start_s,
prefs.autoSkipIntro,
prefs.autoSkipCredits,
prefs.autoSkipDelay,
doSkip,
]);
// Seekbar hover: a timestamp tooltip that follows the cursor. Store the position as a FRACTION
// (0..1), not pixels: the bar sits inside the transform:scale(1.25) subtree, so a px `left` would be
@@ -974,7 +1024,12 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
cursor: uiVisible ? "default" : "none",
...(fs
? { width: "100vw", height: "100vh" }
: { width: "80vw", height: "80vh", transform: "scale(1.25)", transformOrigin: "top left" }),
: {
width: "80vw",
height: "80vh",
transform: "scale(1.25)",
transformOrigin: "top left",
}),
}}
>
{/* Per-user subtitle appearance (font size / colour / background). ::cue is global, but this
@@ -1031,7 +1086,11 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
}`}
>
<div className="flex items-center gap-3 text-white">
<button onClick={handleBack} className="p-1.5 rounded-lg hover:bg-white/15" title={t("plex.player.back")}>
<button
onClick={handleBack}
className="p-1.5 rounded-lg hover:bg-white/15"
title={t("plex.player.back")}
>
<ArrowLeft className="w-5 h-5" />
</button>
<div className="min-w-0">
@@ -1087,7 +1146,9 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
</>
)}
<span className="relative">
{activeMarker.type === "intro" ? t("plex.player.skipIntro") : t("plex.player.skipCredits")}
{activeMarker.type === "intro"
? t("plex.player.skipIntro")
: t("plex.player.skipCredits")}
</span>
</button>
)}
@@ -1137,16 +1198,25 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
{fmt(hover.t)}
</div>
)}
<div className="absolute inset-y-0 left-0 bg-white/30 rounded-full" style={{ width: `${bufPct}%` }} />
<div
className="absolute inset-y-0 left-0 bg-white/30 rounded-full"
style={{ width: `${bufPct}%` }}
/>
{/* intro/credit marker regions */}
{detail?.markers.map((m, i) => (
<div
key={i}
className="absolute inset-y-0 bg-amber-400/50"
style={{ left: `${(m.start_s / duration) * 100}%`, width: `${((m.end_s - m.start_s) / duration) * 100}%` }}
style={{
left: `${(m.start_s / duration) * 100}%`,
width: `${((m.end_s - m.start_s) / duration) * 100}%`,
}}
/>
))}
<div className="absolute inset-y-0 left-0 bg-accent rounded-full" style={{ width: `${headPct}%` }} />
<div
className="absolute inset-y-0 left-0 bg-accent rounded-full"
style={{ width: `${headPct}%` }}
/>
<div
className={`absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-3 h-3 rounded-full bg-accent transition-opacity ${
scrub != null ? "opacity-100" : "opacity-0 group-hover:opacity-100"
@@ -1178,7 +1248,11 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
<div className="flex items-center gap-2">
<Ctrl label={t("plex.player.mute")} onClick={toggleMute}>
{muted || volume === 0 ? <VolumeX className="w-5 h-5" /> : <Volume2 className="w-5 h-5" />}
{muted || volume === 0 ? (
<VolumeX className="w-5 h-5" />
) : (
<Volume2 className="w-5 h-5" />
)}
</Ctrl>
{/* Volume bar: plain accent fill on a neutral track (as before); hover shows the 0–100
value under the cursor; click/drag sets it. */}
@@ -1230,7 +1304,12 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
pushes later while paused, since the remaining time then rides the advancing clock. */}
{duration > 0 && (
<span className="ml-1.5 text-white/45">
({clockParts(new Date(now.getTime() + Math.max(0, duration - abs) * 1000), prefs).time})
(
{
clockParts(new Date(now.getTime() + Math.max(0, duration - abs) * 1000), prefs)
.time
}
)
</span>
)}
</div>
@@ -1324,7 +1403,9 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
key={tab.key}
onClick={() => setSettingsTab(tab.key)}
className={`flex-1 whitespace-nowrap rounded px-2 py-1 text-[11px] ${
activeTab === tab.key ? "bg-white/15 text-accent" : "text-white/70 hover:bg-white/10"
activeTab === tab.key
? "bg-white/15 text-accent"
: "text-white/70 hover:bg-white/10"
}`}
>
{tab.label}
@@ -1427,7 +1508,9 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
onChange={(v) => patchPrefs({ subPos: v })}
/>
<div className="flex items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">{t("plex.player.subColor")}</span>
<span className="text-[12px] text-white/80">
{t("plex.player.subColor")}
</span>
<input
type="color"
value={prefs.subColor}
@@ -1436,13 +1519,19 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
/>
</div>
<div className="flex items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">{t("plex.player.subBg")}</span>
<span className="text-[12px] text-white/80">
{t("plex.player.subBg")}
</span>
<div className="flex items-center gap-2">
<label className="flex items-center gap-1 text-[11px] text-white/70">
<input
type="checkbox"
checked={prefs.subBg === "transparent"}
onChange={(e) => patchPrefs({ subBg: e.target.checked ? "transparent" : "#000000" })}
onChange={(e) =>
patchPrefs({
subBg: e.target.checked ? "transparent" : "#000000",
})
}
className="accent-accent"
/>
{t("plex.player.subBgNone")}
@@ -1468,7 +1557,9 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
/>
{prefs.subShadow > 0 && (
<div className="flex items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">{t("plex.player.subShadowColor")}</span>
<span className="text-[12px] text-white/80">
{t("plex.player.subShadowColor")}
</span>
<input
type="color"
value={prefs.subShadowColor}
@@ -1502,7 +1593,9 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
onChange={(v) => patchPrefs({ clockSize: v })}
/>
<div className="flex items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">{t("plex.player.clockColor")}</span>
<span className="text-[12px] text-white/80">
{t("plex.player.clockColor")}
</span>
<input
type="color"
value={prefs.clockColor}
@@ -1522,10 +1615,15 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
key={st}
onClick={() => patchPrefs({ clockDateStyle: st })}
className={`flex-1 rounded px-2 py-1 text-[11px] ${
prefs.clockDateStyle === st ? "bg-white/15 text-accent" : "text-white/70 hover:bg-white/10"
prefs.clockDateStyle === st
? "bg-white/15 text-accent"
: "text-white/70 hover:bg-white/10"
}`}
>
{clockParts(now, { ...prefs, clockDate: true, clockDateStyle: st }).date}
{
clockParts(now, { ...prefs, clockDate: true, clockDateStyle: st })
.date
}
</button>
))}
</div>
@@ -1635,7 +1733,7 @@ export default function PlexPlayer({ itemId, onClose, queue }: Props) {
</div>
)}
</div>,
document.body
document.body,
);
}
@@ -1648,7 +1746,11 @@ function BackClose({ onBack }: { onBack: () => void }) {
// --- settings-panel building blocks ------------------------------------------------------------
function PanelHead({ children }: { children: ReactNode }) {
return <div className="px-1 pt-2 pb-1 text-[11px] uppercase tracking-wide text-white/50">{children}</div>;
return (
<div className="px-1 pt-2 pb-1 text-[11px] uppercase tracking-wide text-white/50">
{children}
</div>
);
}
function Slider({
label,
@@ -1697,11 +1799,24 @@ function Slider({
</div>
);
}
function Toggle({ label, checked, onChange }: { label: string; checked: boolean; onChange: (v: boolean) => void }) {
function Toggle({
label,
checked,
onChange,
}: {
label: string;
checked: boolean;
onChange: (v: boolean) => void;
}) {
return (
<label className="flex cursor-pointer items-center justify-between px-1 py-1.5">
<span className="text-[12px] text-white/80">{label}</span>
<input type="checkbox" checked={checked} onChange={(e) => onChange(e.target.checked)} className="h-4 w-4 accent-accent" />
<input
type="checkbox"
checked={checked}
onChange={(e) => onChange(e.target.checked)}
className="h-4 w-4 accent-accent"
/>
</label>
);
}
@@ -1722,7 +1837,8 @@ function Ctrl({
align?: "start" | "center" | "end";
children: ReactNode;
}) {
const pos = align === "start" ? "left-0" : align === "end" ? "right-0" : "left-1/2 -translate-x-1/2";
const pos =
align === "start" ? "left-0" : align === "end" ? "right-0" : "left-1/2 -translate-x-1/2";
return (
<div className="relative flex group/ctrl">
<button