Files
pinkudex/components/settings/SettingsToggles.tsx
T
2026-05-26 22:46:00 +02:00

298 lines
9.5 KiB
TypeScript

"use client";
import { useSettings } from "./SettingsProvider";
import { PurgeOrphansButton } from "./PurgeOrphansButton";
import { ReorganizeButton } from "./ReorganizeButton";
import { RegenThumbnailsButton } from "./RegenThumbnailsButton";
import { UndersizedCoversButton } from "./UndersizedCoversButton";
import { NearDupesButton } from "./NearDupesButton";
import { ReparseCodesButton } from "./ReparseCodesButton";
import { ClearCacheButton } from "./ClearCacheButton";
import { BackupButtons } from "./BackupButtons";
import { cn } from "@/lib/utils";
/** Display group: grid columns + fade transitions. */
export function DisplayGroup() {
const { settings, set } = useSettings();
return (
<div className="space-y-1">
<SliderRow
label="Grid Columns (Landscape)"
description="Number of cover columns shown when the library is in L (landscape / full cover) view."
min={2}
max={4}
step={1}
value={settings.gridColumns}
onChange={(v) => set("gridColumns", v)}
format={(v) => `${v} per row`}
/>
<SliderRow
label="Grid Columns (Portrait)"
description="Number of cover columns shown when the library is in P (portrait / front-only) view."
min={4}
max={10}
step={1}
value={settings.gridColumnsPortrait}
onChange={(v) => set("gridColumnsPortrait", v)}
format={(v) => `${v} per row`}
/>
<SliderRow
label="Items Per Page"
description="Cover grid page size. Pagination + infinite scroll fetch this many at a time."
min={25}
max={500}
step={25}
value={settings.coverPageSize}
onChange={(v) => set("coverPageSize", v)}
format={(v) => `${v} per page`}
/>
<SegmentedRow
label="Pagination Behavior"
description={
settings.paginationMode === "url"
? "Prev / Next / Jump always pushes a new URL and remounts the grid. Predictable; small flash on each click."
: "Prev / Next scrolls within the loaded buffer. Forward jumps prefetch missing pages on the fly. Backward across the SSR anchor falls back to URL nav."
}
value={settings.paginationMode}
options={[
{ value: "url", label: "URL" },
{ value: "scroll", label: "Scroll" },
]}
onChange={(v) => set("paginationMode", v as "url" | "scroll")}
/>
<ToggleRow
label="Fade Transitions"
description="Fade in pages and image details when navigating."
value={settings.fadeTransitions}
onChange={(v) => set("fadeTransitions", v)}
/>
{settings.fadeTransitions && (
<SliderRow
label="Fade Duration"
description="How long the fade-in animation takes."
min={100}
max={2000}
step={50}
value={settings.fadeDurationMs}
onChange={(v) => set("fadeDurationMs", v)}
format={(v) => (v >= 1000 ? `${(v / 1000).toFixed(2)}s` : `${v}ms`)}
/>
)}
</div>
);
}
/** Trash & deletion group. */
export function TrashGroup() {
const { settings, set } = useSettings();
return (
<div className="space-y-1">
<ToggleRow
label="Use Recycle Bin"
description="Send deletes to the recycle bin instead of removing immediately. Off makes every delete permanent."
value={settings.useRecycleBin}
onChange={(v) => set("useRecycleBin", v)}
/>
{settings.useRecycleBin && (
<SliderRow
label="Trash Retention"
description="Automatically purge trashed images older than this. 0 keeps them forever."
min={0}
max={365}
step={1}
value={settings.trashRetentionDays}
onChange={(v) => set("trashRetentionDays", v)}
format={(v) => v === 0 ? "Forever" : `${v} day${v === 1 ? "" : "s"}`}
/>
)}
<ToggleRow
label="Delete Files From Disk When Emptying Trash"
description="When permanently removing an image (or emptying the bin), also delete the file and thumbnail from disk. Off keeps files on disk."
value={settings.purgeFilesOnDelete}
onChange={(v) => set("purgeFilesOnDelete", v)}
/>
<SliderRow
label="Superseded Retention"
description={`When you replace a cover via the collision dialog, the old file is moved to library/.superseded/ as a recovery snapshot. Files older than this are auto-purged on each app start. 0 = keep forever.`}
min={0}
max={365}
step={1}
value={settings.supersededRetentionDays}
onChange={(v) => set("supersededRetentionDays", v)}
format={(v) => v === 0 ? "Forever" : `${v} day${v === 1 ? "" : "s"}`}
/>
</div>
);
}
/** Maintenance buttons group. */
export function MaintenanceGroup() {
return (
<div className="divide-y divide-[var(--color-glass-border)]">
<PurgeOrphansButton />
<ReparseCodesButton />
<ReorganizeButton />
<RegenThumbnailsButton />
<UndersizedCoversButton />
<NearDupesButton />
<ClearCacheButton />
</div>
);
}
/** Backup group. */
export function BackupGroup() {
return (
<div className="space-y-1">
<BackupButtons />
</div>
);
}
/** Legacy combined view — preserved for any caller still rendering all
* groups inline. The new SettingsPanel layouts use the individual
* exports above. */
export function SettingsToggles() {
return (
<div className="space-y-5">
<Group title="Display"><DisplayGroup /></Group>
<Group title="Deletion & trash"><TrashGroup /></Group>
<Group title="Maintenance"><MaintenanceGroup /></Group>
<Group title="Backup"><BackupGroup /></Group>
</div>
);
}
function Group({ title, children }: { title: string; children: React.ReactNode }) {
return (
<div>
<div className="text-[10px] uppercase tracking-wider font-mono text-[var(--color-cyan)] mb-1.5">{title}</div>
<div className="border-t border-[var(--color-glass-border)] pt-1">{children}</div>
</div>
);
}
function SliderRow({
label, description, min, max, step, value, onChange, format,
}: {
label: string;
description?: string;
min: number;
max: number;
step: number;
value: number;
onChange: (v: number) => void;
format?: (v: number) => string;
}) {
return (
<div className="py-2">
<div className="flex items-start justify-between gap-4 mb-2">
<div>
<div className="text-sm font-medium">{label}</div>
{description && (
<div className="text-xs text-[var(--color-fg-muted)] mt-0.5">{description}</div>
)}
</div>
<span className="text-xs font-mono text-[var(--color-cyan)] tabular-nums whitespace-nowrap">
{format ? format(value) : value}
</span>
</div>
<input
type="range"
min={min}
max={max}
step={step}
value={value}
onChange={(e) => onChange(Number(e.target.value))}
className="w-full accent-[var(--color-cyan)]"
/>
</div>
);
}
function SegmentedRow<T extends string>({
label,
description,
value,
options,
onChange,
}: {
label: string;
description?: string;
value: T;
options: ReadonlyArray<{ value: T; label: string }>;
onChange: (v: T) => void;
}) {
return (
<div className="py-2">
<div className="flex items-start justify-between gap-4 mb-2">
<div className="min-w-0">
<div className="text-sm font-medium">{label}</div>
{description && (
<div className="text-xs text-[var(--color-fg-muted)] mt-0.5">{description}</div>
)}
</div>
<div className="flex flex-shrink-0 rounded-lg border border-[var(--color-glass-border-strong)] overflow-hidden">
{options.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => onChange(opt.value)}
className={cn(
"min-w-[64px] px-3 py-1 text-xs font-mono transition-colors text-center",
value === opt.value
? "bg-[var(--color-cyan)]/20 text-[var(--color-cyan)]"
: "bg-[var(--color-glass)] text-[var(--color-fg-dim)] hover:text-[var(--color-fg)]",
)}
>
{opt.label}
</button>
))}
</div>
</div>
</div>
);
}
function ToggleRow({
label,
description,
value,
onChange,
}: {
label: string;
description?: string;
value: boolean;
onChange: (v: boolean) => void;
}) {
return (
<div className="flex items-start justify-between gap-4 py-2">
<div className="min-w-0">
<div className="text-sm font-medium">{label}</div>
{description && (
<div className="text-xs text-[var(--color-fg-muted)] mt-0.5">{description}</div>
)}
</div>
<button
role="switch"
aria-checked={value}
onClick={() => onChange(!value)}
className={cn(
"relative w-11 h-6 rounded-full border transition-colors flex-shrink-0",
value
? "bg-[var(--color-cyan)]/30 border-[var(--color-cyan)]"
: "bg-[var(--color-glass)] border-[var(--color-glass-border-strong)]"
)}
>
<span
className={cn(
"absolute top-0.5 w-4 h-4 rounded-full transition-all",
value
? "left-[22px] bg-[var(--color-cyan)] shadow-[var(--shadow-glow-cyan)]"
: "left-0.5 bg-[var(--color-fg-dim)]"
)}
/>
</button>
</div>
);
}