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:
@@ -1,7 +1,20 @@
|
||||
import { lazy, Suspense, useEffect, useLayoutEffect, useRef, useState, type CSSProperties } from "react";
|
||||
import {
|
||||
lazy,
|
||||
Suspense,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
type CSSProperties,
|
||||
} from "react";
|
||||
import { createPortal } from "react-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useInfiniteQuery, useQuery, useQueryClient, type InfiniteData } from "@tanstack/react-query";
|
||||
import {
|
||||
useInfiniteQuery,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type InfiniteData,
|
||||
} from "@tanstack/react-query";
|
||||
import {
|
||||
ArrowLeft,
|
||||
Check,
|
||||
@@ -32,7 +45,12 @@ import {
|
||||
import { formatRuntime } from "../lib/format";
|
||||
import { useDebounced } from "../lib/useDebounced";
|
||||
import { useHistorySubview } from "../lib/history";
|
||||
import { DetailCustomizeMenu, Filterable, useArtBackdrop, useDetailPrefs } from "../lib/plexDetailUi";
|
||||
import {
|
||||
DetailCustomizeMenu,
|
||||
Filterable,
|
||||
useArtBackdrop,
|
||||
useDetailPrefs,
|
||||
} from "../lib/plexDetailUi";
|
||||
import PlexPlaylistAdd, { type PlexAddTarget } from "./PlexPlaylistAdd";
|
||||
import PlexCollectionEditor from "./PlexCollectionEditor";
|
||||
import { PageToolbar } from "./PageShell";
|
||||
@@ -95,7 +113,8 @@ export default function PlexBrowse() {
|
||||
const el = scroller();
|
||||
if (!el) return;
|
||||
if (sub.view.kind === "grid" && scrollRef.current) el.scrollTop = scrollRef.current;
|
||||
else if (sub.view.kind === "info" && infoScrollRef.current) el.scrollTop = infoScrollRef.current;
|
||||
else if (sub.view.kind === "info" && infoScrollRef.current)
|
||||
el.scrollTop = infoScrollRef.current;
|
||||
}, [sub.view.kind]);
|
||||
|
||||
// Backspace steps back one drill-down level (grid ← show ← season, and out of info/playlist),
|
||||
@@ -106,7 +125,8 @@ export default function PlexBrowse() {
|
||||
if (e.key !== "Backspace") return;
|
||||
const el = document.activeElement as HTMLElement | null;
|
||||
const tag = (el?.tagName || "").toLowerCase();
|
||||
if (tag === "input" || tag === "textarea" || tag === "select" || el?.isContentEditable) return;
|
||||
if (tag === "input" || tag === "textarea" || tag === "select" || el?.isContentEditable)
|
||||
return;
|
||||
if (["show", "season", "info", "playlist"].includes(sub.view.kind)) {
|
||||
e.preventDefault();
|
||||
sub.back();
|
||||
@@ -227,7 +247,9 @@ export default function PlexBrowse() {
|
||||
// The fallback is portaled for the same reason the player itself is: it is a fixed
|
||||
// full-viewport overlay, and this tree renders inside the page scroller.
|
||||
return (
|
||||
<Suspense fallback={createPortal(<div className="fixed inset-0 z-overlay bg-black" />, document.body)}>
|
||||
<Suspense
|
||||
fallback={createPortal(<div className="fixed inset-0 z-overlay bg-black" />, document.body)}
|
||||
>
|
||||
<PlexPlayer itemId={sub.view.id} queue={sub.view.queue} onClose={sub.back} />
|
||||
</Suspense>
|
||||
);
|
||||
@@ -288,7 +310,11 @@ export default function PlexBrowse() {
|
||||
<PageToolbar>
|
||||
<div className="px-4 pt-3 pb-1 max-w-[1600px] mx-auto">
|
||||
<p className="text-xs text-muted">
|
||||
{browseQ.isLoading ? " " : dq ? t("plex.searchCount", { count: total }) : t("plex.count", { count: total })}
|
||||
{browseQ.isLoading
|
||||
? " "
|
||||
: dq
|
||||
? t("plex.searchCount", { count: total })
|
||||
: t("plex.count", { count: total })}
|
||||
</p>
|
||||
</div>
|
||||
</PageToolbar>
|
||||
@@ -314,7 +340,9 @@ export default function PlexBrowse() {
|
||||
)}
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium truncate max-w-[150px]">{p.name}</div>
|
||||
<div className="text-[11px] text-muted">{t("plex.people.count", { count: p.count })}</div>
|
||||
<div className="text-[11px] text-muted">
|
||||
{t("plex.people.count", { count: p.count })}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => removeActor(p.name)}
|
||||
@@ -389,7 +417,9 @@ export default function PlexBrowse() {
|
||||
)}
|
||||
|
||||
<div ref={setSentinelEl} className="h-8" />
|
||||
{isFetchingNextPage && <p className="text-center text-xs text-muted pb-4">{t("plex.loading")}</p>}
|
||||
{isFetchingNextPage && (
|
||||
<p className="text-center text-xs text-muted pb-4">{t("plex.loading")}</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -448,7 +478,11 @@ function PlexPosterCard({
|
||||
<div className="flex flex-col items-center gap-1 text-white">
|
||||
<Play className="w-10 h-10" fill="currentColor" />
|
||||
<span className="text-xs font-medium">
|
||||
{card.type === "show" ? t("plex.openShow") : inProgress ? t("plex.resume") : t("plex.play")}
|
||||
{card.type === "show"
|
||||
? t("plex.openShow")
|
||||
: inProgress
|
||||
? t("plex.resume")
|
||||
: t("plex.play")}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -462,7 +496,9 @@ function PlexPosterCard({
|
||||
title={card.status === "watched" ? t("plex.markUnwatched") : t("plex.markWatched")}
|
||||
className="absolute top-1.5 right-1.5 p-1 rounded-full bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80 transition"
|
||||
>
|
||||
<CheckCircle2 className={`w-4 h-4 ${card.status === "watched" ? "text-emerald-400" : "text-white"}`} />
|
||||
<CheckCircle2
|
||||
className={`w-4 h-4 ${card.status === "watched" ? "text-emerald-400" : "text-white"}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{/* Media info page (movies/episodes). */}
|
||||
@@ -504,7 +540,9 @@ function PlexPosterCard({
|
||||
{/* Type tag — tell a movie card from a show card at a glance (unified feed). */}
|
||||
<span
|
||||
className="absolute bottom-1.5 right-1.5 rounded bg-black/65 p-0.5 text-white/90"
|
||||
title={t(card.type === "show" ? "plex.filter.scopeOpt.show" : "plex.filter.scopeOpt.movie")}
|
||||
title={t(
|
||||
card.type === "show" ? "plex.filter.scopeOpt.show" : "plex.filter.scopeOpt.movie",
|
||||
)}
|
||||
>
|
||||
{card.type === "show" ? <Tv2 className="w-3 h-3" /> : <Film className="w-3 h-3" />}
|
||||
</span>
|
||||
@@ -519,8 +557,12 @@ function PlexPosterCard({
|
||||
className="mt-1.5 rounded-lg px-2 py-1.5 cursor-pointer"
|
||||
style={{ background: "color-mix(in srgb, var(--card) 60%, transparent)" }}
|
||||
>
|
||||
<div className="text-sm font-medium line-clamp-2 leading-tight hover:text-accent">{card.title}</div>
|
||||
<div className="text-xs text-muted">{[card.year, formatRuntime(card.duration_seconds)].filter(Boolean).join(" · ")}</div>
|
||||
<div className="text-sm font-medium line-clamp-2 leading-tight hover:text-accent">
|
||||
{card.title}
|
||||
</div>
|
||||
<div className="text-xs text-muted">
|
||||
{[card.year, formatRuntime(card.duration_seconds)].filter(Boolean).join(" · ")}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -581,7 +623,8 @@ function PlexInfoView({
|
||||
|
||||
function epLabel(ep?: PlexCard | null): string | undefined {
|
||||
if (!ep) return undefined;
|
||||
if (ep.season_number != null && ep.episode_number != null) return `S${ep.season_number} · E${ep.episode_number}`;
|
||||
if (ep.season_number != null && ep.episode_number != null)
|
||||
return `S${ep.season_number} · E${ep.episode_number}`;
|
||||
return ep.title;
|
||||
}
|
||||
|
||||
@@ -675,7 +718,9 @@ function SeasonCard({
|
||||
e.stopPropagation();
|
||||
onToggleWatched();
|
||||
}}
|
||||
title={watched ? t("plex.series.markSeasonUnwatched") : t("plex.series.markSeasonWatched")}
|
||||
title={
|
||||
watched ? t("plex.series.markSeasonUnwatched") : t("plex.series.markSeasonWatched")
|
||||
}
|
||||
className="absolute top-1.5 right-1.5 p-1 rounded-full bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80 transition"
|
||||
>
|
||||
<CheckCircle2 className={`w-4 h-4 ${watched ? "text-emerald-400" : "text-white"}`} />
|
||||
@@ -719,7 +764,13 @@ function SeasonCard({
|
||||
);
|
||||
}
|
||||
|
||||
function CastStrip({ cast, onFilter }: { cast: PlexCastMember[]; onFilter?: (patch: Partial<PlexFilters>) => void }) {
|
||||
function CastStrip({
|
||||
cast,
|
||||
onFilter,
|
||||
}: {
|
||||
cast: PlexCastMember[];
|
||||
onFilter?: (patch: Partial<PlexFilters>) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<section
|
||||
@@ -731,14 +782,20 @@ function CastStrip({ cast, onFilter }: { cast: PlexCastMember[]; onFilter?: (pat
|
||||
{cast.map((c, i) => {
|
||||
const inner = (
|
||||
<>
|
||||
<div className={`mx-auto h-20 w-20 overflow-hidden rounded-full border bg-surface ${onFilter ? "border-border group-hover/cast:border-accent" : "border-border"}`}>
|
||||
<div
|
||||
className={`mx-auto h-20 w-20 overflow-hidden rounded-full border bg-surface ${onFilter ? "border-border group-hover/cast:border-accent" : "border-border"}`}
|
||||
>
|
||||
{c.thumb ? (
|
||||
<img src={c.thumb} alt="" loading="lazy" className="h-full w-full object-cover" />
|
||||
) : (
|
||||
<div className="grid h-full w-full place-items-center opacity-40">{c.name.charAt(0)}</div>
|
||||
<div className="grid h-full w-full place-items-center opacity-40">
|
||||
{c.name.charAt(0)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className={`mt-1.5 text-xs font-medium line-clamp-2 leading-tight ${onFilter ? "group-hover/cast:text-accent" : ""}`}>
|
||||
<div
|
||||
className={`mt-1.5 text-xs font-medium line-clamp-2 leading-tight ${onFilter ? "group-hover/cast:text-accent" : ""}`}
|
||||
>
|
||||
{c.name}
|
||||
</div>
|
||||
{c.role && <div className="text-[11px] text-muted line-clamp-1">{c.role}</div>}
|
||||
@@ -774,7 +831,11 @@ function RelatedStrip({ related, onOpen }: { related: PlexCard[]; onOpen: (id: s
|
||||
<h2 className="text-sm font-semibold mb-3">{t("plex.series.related")}</h2>
|
||||
<div className="flex gap-3 overflow-x-auto pb-2">
|
||||
{related.map((r) => (
|
||||
<button key={r.id} onClick={() => onOpen(r.id)} className="group w-[130px] shrink-0 text-left">
|
||||
<button
|
||||
key={r.id}
|
||||
onClick={() => onOpen(r.id)}
|
||||
className="group w-[130px] shrink-0 text-left"
|
||||
>
|
||||
<div className="relative aspect-[2/3] rounded-xl overflow-hidden bg-card border border-border">
|
||||
<img
|
||||
src={r.thumb}
|
||||
@@ -848,7 +909,9 @@ function EpisodeCard({
|
||||
title={ep.status === "watched" ? t("plex.markUnwatched") : t("plex.markWatched")}
|
||||
className="absolute top-1.5 right-1.5 p-1 rounded-full bg-black/60 opacity-0 group-hover:opacity-100 hover:bg-black/80 transition"
|
||||
>
|
||||
<CheckCircle2 className={`w-4 h-4 ${ep.status === "watched" ? "text-emerald-400" : "text-white"}`} />
|
||||
<CheckCircle2
|
||||
className={`w-4 h-4 ${ep.status === "watched" ? "text-emerald-400" : "text-white"}`}
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
{ep.status === "watched" && (
|
||||
@@ -989,19 +1052,37 @@ function PlexShowView({
|
||||
<h1 className="text-2xl font-semibold leading-tight">{show.title}</h1>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-2 gap-y-0.5 text-sm text-muted">
|
||||
{show.year != null && (
|
||||
<Filterable onClick={onFilter ? () => onFilter({ yearMin: show.year, yearMax: show.year }) : undefined}>
|
||||
<Filterable
|
||||
onClick={
|
||||
onFilter
|
||||
? () => onFilter({ yearMin: show.year, yearMax: show.year })
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{show.year}
|
||||
</Filterable>
|
||||
)}
|
||||
{show.content_rating && (
|
||||
<Filterable onClick={onFilter ? () => onFilter({ contentRatings: [show.content_rating!] }) : undefined}>
|
||||
<Filterable
|
||||
onClick={
|
||||
onFilter
|
||||
? () => onFilter({ contentRatings: [show.content_rating!] })
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
· {show.content_rating}
|
||||
</Filterable>
|
||||
)}
|
||||
{show.season_count != null && <span>· {t("plex.seasons", { count: show.season_count })}</span>}
|
||||
{show.season_count != null && (
|
||||
<span>· {t("plex.seasons", { count: show.season_count })}</span>
|
||||
)}
|
||||
{show.imdb_rating != null && (
|
||||
<button
|
||||
onClick={onFilter ? () => onFilter({ ratingMin: Math.floor(show.imdb_rating!) }) : undefined}
|
||||
onClick={
|
||||
onFilter
|
||||
? () => onFilter({ ratingMin: Math.floor(show.imdb_rating!) })
|
||||
: undefined
|
||||
}
|
||||
className={`inline-flex items-center gap-1 ${onFilter ? "cursor-pointer hover:text-accent" : "cursor-default"}`}
|
||||
>
|
||||
· <Star className="w-3.5 h-3.5 text-amber-400" fill="currentColor" />
|
||||
@@ -1023,7 +1104,9 @@ function PlexShowView({
|
||||
</div>
|
||||
)}
|
||||
{show.summary && (
|
||||
<p className="text-sm text-muted mt-3 leading-relaxed line-clamp-5">{show.summary}</p>
|
||||
<p className="text-sm text-muted mt-3 leading-relaxed line-clamp-5">
|
||||
{show.summary}
|
||||
</p>
|
||||
)}
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{show.resume && (
|
||||
@@ -1036,23 +1119,35 @@ function PlexShowView({
|
||||
/>
|
||||
)}
|
||||
{show.first && show.status !== "new" && (
|
||||
<ActionBtn onClick={() => onPlay(show.first!.id, allKeys)} icon={RotateCcw} label={t("plex.series.playFromStart")} />
|
||||
<ActionBtn
|
||||
onClick={() => onPlay(show.first!.id, allKeys)}
|
||||
icon={RotateCcw}
|
||||
label={t("plex.series.playFromStart")}
|
||||
/>
|
||||
)}
|
||||
<ActionBtn
|
||||
onClick={() => markAll(!watched)}
|
||||
disabled={busy}
|
||||
icon={watched ? CheckCheck : Check}
|
||||
label={watched ? t("plex.series.markShowUnwatched") : t("plex.series.markShowWatched")}
|
||||
label={
|
||||
watched ? t("plex.series.markShowUnwatched") : t("plex.series.markShowWatched")
|
||||
}
|
||||
/>
|
||||
{allKeys.length > 0 && (
|
||||
<ActionBtn
|
||||
onClick={() => setAddTarget({ kind: "group", ratingKeys: allKeys, title: show.title })}
|
||||
onClick={() =>
|
||||
setAddTarget({ kind: "group", ratingKeys: allKeys, title: show.title })
|
||||
}
|
||||
icon={ListPlus}
|
||||
label={t("plex.playlist.addShow")}
|
||||
/>
|
||||
)}
|
||||
{isAdmin && showLib && (
|
||||
<ActionBtn onClick={() => setCollOpen(true)} icon={Layers} label={t("plex.series.addShowCollection")} />
|
||||
<ActionBtn
|
||||
onClick={() => setCollOpen(true)}
|
||||
icon={Layers}
|
||||
label={t("plex.series.addShowCollection")}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
@@ -1087,7 +1182,9 @@ function PlexShowView({
|
||||
</section>
|
||||
|
||||
{show.cast.length > 0 && showCast && <CastStrip cast={show.cast} onFilter={onFilter} />}
|
||||
{d.related.length > 0 && showRelated && <RelatedStrip related={d.related} onOpen={onOpenShow} />}
|
||||
{d.related.length > 0 && showRelated && (
|
||||
<RelatedStrip related={d.related} onOpen={onOpenShow} />
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -1180,11 +1277,16 @@ function PlexSeasonView({
|
||||
)}
|
||||
</button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<button onClick={onBack} className="text-sm text-muted hover:text-accent transition text-left">
|
||||
<button
|
||||
onClick={onBack}
|
||||
className="text-sm text-muted hover:text-accent transition text-left"
|
||||
>
|
||||
{d.show.title}
|
||||
</button>
|
||||
<h1 className="text-2xl font-semibold leading-tight">{se.title}</h1>
|
||||
<p className="text-sm text-muted mt-1">{t("plex.series.episodeCount", { count: se.episode_count })}</p>
|
||||
<p className="text-sm text-muted mt-1">
|
||||
{t("plex.series.episodeCount", { count: se.episode_count })}
|
||||
</p>
|
||||
<div className="mt-4 flex flex-wrap gap-2">
|
||||
{se.resume && (
|
||||
<ActionBtn
|
||||
@@ -1196,18 +1298,30 @@ function PlexSeasonView({
|
||||
/>
|
||||
)}
|
||||
{se.first && se.status !== "new" && (
|
||||
<ActionBtn onClick={() => onPlay(se.first!.id, seasonKeys)} icon={RotateCcw} label={t("plex.series.playFromStart")} />
|
||||
<ActionBtn
|
||||
onClick={() => onPlay(se.first!.id, seasonKeys)}
|
||||
icon={RotateCcw}
|
||||
label={t("plex.series.playFromStart")}
|
||||
/>
|
||||
)}
|
||||
<ActionBtn
|
||||
onClick={() => markAll(!watched)}
|
||||
disabled={busy}
|
||||
icon={watched ? CheckCheck : Check}
|
||||
label={watched ? t("plex.series.markSeasonUnwatched") : t("plex.series.markSeasonWatched")}
|
||||
label={
|
||||
watched
|
||||
? t("plex.series.markSeasonUnwatched")
|
||||
: t("plex.series.markSeasonWatched")
|
||||
}
|
||||
/>
|
||||
{seasonKeys.length > 0 && (
|
||||
<ActionBtn
|
||||
onClick={() =>
|
||||
setAddTarget({ kind: "group", ratingKeys: seasonKeys, title: `${d.show.title} — ${se.title}` })
|
||||
setAddTarget({
|
||||
kind: "group",
|
||||
ratingKeys: seasonKeys,
|
||||
title: `${d.show.title} — ${se.title}`,
|
||||
})
|
||||
}
|
||||
icon={ListPlus}
|
||||
label={t("plex.playlist.addSeason")}
|
||||
|
||||
Reference in New Issue
Block a user