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:
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user