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
+131 -114
View File
@@ -16,7 +16,19 @@ import { LS } from "../lib/storage";
// applied together via one Save/Discard bar (consistent with the Settings page); nothing hits
// the server until Save. Group order is fixed where known so logically related settings (e.g.
// Email/SMTP) stay together.
const GROUP_ORDER = ["access", "email", "youtube", "google", "quota", "backfill", "shorts", "batch", "downloads", "audit", "plex"];
const GROUP_ORDER = [
"access",
"email",
"youtube",
"google",
"quota",
"backfill",
"shorts",
"batch",
"downloads",
"audit",
"plex",
];
export default function ConfigPanel() {
const { t } = useTranslation();
@@ -121,16 +133,17 @@ export default function ConfigPanel() {
notify({ level: "error", message: t("config.plexTestFailed") });
},
});
const plexLibs = (draft["plex_libraries"] ?? "").split(",").map((s) => s.trim()).filter(Boolean);
const plexLibs = (draft["plex_libraries"] ?? "")
.split(",")
.map((s) => s.trim())
.filter(Boolean);
const togglePlexLib = (key: string) => {
// An empty list means "all libraries" — every box renders checked. Unchecking one from that
// state must select all-except-this, so expand "all" to the explicit section set first;
// otherwise the toggle would ADD the key and leave it as the ONLY selected library.
const allKeys = plexResult?.sections?.map((s) => s.key) ?? [];
const current = plexLibs.length === 0 ? allKeys : plexLibs;
const next = current.includes(key)
? current.filter((k) => k !== key)
: [...current, key];
const next = current.includes(key) ? current.filter((k) => k !== key) : [...current, key];
setDraft((d) => ({ ...d, plex_libraries: next.join(",") }));
};
@@ -139,7 +152,7 @@ export default function ConfigPanel() {
}
const groupKeys = Object.keys(data.groups).sort(
(a, b) => (GROUP_ORDER.indexOf(a) + 1 || 99) - (GROUP_ORDER.indexOf(b) + 1 || 99)
(a, b) => (GROUP_ORDER.indexOf(a) + 1 || 99) - (GROUP_ORDER.indexOf(b) + 1 || 99),
);
// Clamp the persisted tab to a real group (the registry can change between sessions). The
// tab pill carries a dot when that group has unsaved edits, so a draft in a hidden tab is
@@ -161,114 +174,120 @@ export default function ConfigPanel() {
</div>
</PageToolbar>
<div className="px-4 pb-24 pt-3 max-w-3xl w-full mx-auto">
{activeGroup && (
<div className="glass rounded-2xl p-4 mb-4">
<div className="divide-y divide-border/60">
{data.groups[activeGroup].map((item) => (
<Field
key={item.key}
item={item}
value={draft[item.key] ?? ""}
secretsManageable={data.secrets_manageable}
pendingSecretReset={!!secretReset[item.key]}
onChange={(v) => setDraft((d) => ({ ...d, [item.key]: v }))}
onResetField={() => {
if (item.secret) setSecretReset((s) => ({ ...s, [item.key]: !s[item.key] }));
else setDraft((d) => ({ ...d, [item.key]: "" }));
}}
/>
))}
</div>
{activeGroup === "email" && (
<div className="mt-3 pt-3 border-t border-border/60">
<Tooltip hint={t("config.testEmailHint")}>
<button
onClick={() => testEmail.mutate()}
disabled={testEmail.isPending || dirty}
className="glass-card glass-hover inline-flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
>
<Send className={`w-4 h-4 ${testEmail.isPending ? "animate-pulse" : ""}`} />
{t("config.testEmail")}
</button>
</Tooltip>
{activeGroup && (
<div className="glass rounded-2xl p-4 mb-4">
<div className="divide-y divide-border/60">
{data.groups[activeGroup].map((item) => (
<Field
key={item.key}
item={item}
value={draft[item.key] ?? ""}
secretsManageable={data.secrets_manageable}
pendingSecretReset={!!secretReset[item.key]}
onChange={(v) => setDraft((d) => ({ ...d, [item.key]: v }))}
onResetField={() => {
if (item.secret) setSecretReset((s) => ({ ...s, [item.key]: !s[item.key] }));
else setDraft((d) => ({ ...d, [item.key]: "" }));
}}
/>
))}
</div>
)}
{activeGroup === "plex" && (
<div className="mt-3 pt-3 border-t border-border/60">
<Tooltip hint={t("config.plexTestHint")}>
<button
onClick={() => testPlex.mutate()}
disabled={testPlex.isPending || dirty}
className="glass-card glass-hover inline-flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
>
<Plug className={`w-4 h-4 ${testPlex.isPending ? "animate-pulse" : ""}`} />
{t("config.plexTest")}
</button>
</Tooltip>
{dirty && <p className="text-[11px] text-muted mt-2">{t("config.plexTestDirty")}</p>}
{plexResult && (
<div className="mt-3">
<p className="text-xs text-muted mb-2">
{t("config.plexConnected", {
name: plexResult.server_name,
version: plexResult.version ?? "",
})}
</p>
{plexResult.media_check?.checked &&
(plexResult.media_check.ok ? (
<p className="text-[11px] text-emerald-500 mb-2">{t("config.plexMediaOk")}</p>
) : (
<p className="text-[11px] text-red-400 mb-2">
{t("config.plexMediaMissing", { path: plexResult.media_check.local_path ?? "" })}
</p>
))}
<p className="text-[11px] text-muted mb-1.5">{t("config.plexPickLibraries")}</p>
<div className="flex flex-col gap-1.5">
{plexResult.sections.map((s) => (
<label
key={s.key}
className="inline-flex items-center gap-2 text-sm cursor-pointer"
>
<input
type="checkbox"
checked={plexLibs.length === 0 || plexLibs.includes(s.key)}
onChange={() => togglePlexLib(s.key)}
className="accent-accent"
/>
<span>{s.title}</span>
<span className="text-[11px] text-muted">
{s.type === "movie" ? t("config.plexMovies") : t("config.plexShows")}
{s.count != null ? ` · ${s.count}` : ""}
</span>
</label>
))}
{activeGroup === "email" && (
<div className="mt-3 pt-3 border-t border-border/60">
<Tooltip hint={t("config.testEmailHint")}>
<button
onClick={() => testEmail.mutate()}
disabled={testEmail.isPending || dirty}
className="glass-card glass-hover inline-flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
>
<Send className={`w-4 h-4 ${testEmail.isPending ? "animate-pulse" : ""}`} />
{t("config.testEmail")}
</button>
</Tooltip>
</div>
)}
{activeGroup === "plex" && (
<div className="mt-3 pt-3 border-t border-border/60">
<Tooltip hint={t("config.plexTestHint")}>
<button
onClick={() => testPlex.mutate()}
disabled={testPlex.isPending || dirty}
className="glass-card glass-hover inline-flex items-center gap-2 px-3 py-2 rounded-xl text-sm disabled:opacity-50 transition"
>
<Plug className={`w-4 h-4 ${testPlex.isPending ? "animate-pulse" : ""}`} />
{t("config.plexTest")}
</button>
</Tooltip>
{dirty && (
<p className="text-[11px] text-muted mt-2">{t("config.plexTestDirty")}</p>
)}
{plexResult && (
<div className="mt-3">
<p className="text-xs text-muted mb-2">
{t("config.plexConnected", {
name: plexResult.server_name,
version: plexResult.version ?? "",
})}
</p>
{plexResult.media_check?.checked &&
(plexResult.media_check.ok ? (
<p className="text-[11px] text-emerald-500 mb-2">
{t("config.plexMediaOk")}
</p>
) : (
<p className="text-[11px] text-red-400 mb-2">
{t("config.plexMediaMissing", {
path: plexResult.media_check.local_path ?? "",
})}
</p>
))}
<p className="text-[11px] text-muted mb-1.5">{t("config.plexPickLibraries")}</p>
<div className="flex flex-col gap-1.5">
{plexResult.sections.map((s) => (
<label
key={s.key}
className="inline-flex items-center gap-2 text-sm cursor-pointer"
>
<input
type="checkbox"
checked={plexLibs.length === 0 || plexLibs.includes(s.key)}
onChange={() => togglePlexLib(s.key)}
className="accent-accent"
/>
<span>{s.title}</span>
<span className="text-[11px] text-muted">
{s.type === "movie" ? t("config.plexMovies") : t("config.plexShows")}
{s.count != null ? ` · ${s.count}` : ""}
</span>
</label>
))}
</div>
<p className="text-[11px] text-muted mt-2">{t("config.plexPickHint")}</p>
</div>
<p className="text-[11px] text-muted mt-2">{t("config.plexPickHint")}</p>
</div>
)}
</div>
)}
</div>
)}
)}
</div>
)}
</div>
)}
<DraftSaveBar
variant="floating"
dirty={dirty}
state={saveState}
onSave={save}
onDiscard={discard}
labels={{
saved: t("config.saved"),
failed: t("config.saveFailed"),
unsaved: t("config.unsaved"),
discard: t("config.discard"),
save: t("config.save"),
saving: t("config.saving"),
}}
/>
</div>
<DraftSaveBar
variant="floating"
dirty={dirty}
state={saveState}
onSave={save}
onDiscard={discard}
labels={{
saved: t("config.saved"),
failed: t("config.saveFailed"),
unsaved: t("config.unsaved"),
discard: t("config.discard"),
save: t("config.save"),
saving: t("config.saving"),
}}
/>
</div>
</>
);
}
@@ -349,9 +368,7 @@ function Field({
<button
onClick={onResetField}
className={`inline-flex items-center gap-1 text-xs transition ${
item.secret && pendingSecretReset
? "text-accent"
: "text-muted hover:text-fg"
item.secret && pendingSecretReset ? "text-accent" : "text-muted hover:text-fg"
}`}
>
<RotateCcw className="w-3.5 h-3.5" />
@@ -362,4 +379,4 @@ function Field({
</div>
</div>
);
}
}