107 lines
4.5 KiB
TypeScript
107 lines
4.5 KiB
TypeScript
"use client";
|
|
import { useState, useTransition } from "react";
|
|
import { Trash2, Loader2, CheckCircle2 } from "lucide-react";
|
|
import { previewOrphanFiles, purgeOrphanFiles } from "@/app/actions/maintenance";
|
|
import { formatBytes } from "@/lib/utils";
|
|
|
|
type State =
|
|
| { kind: "idle" }
|
|
| { kind: "scanning" }
|
|
| { kind: "result"; count: number; bytes: number }
|
|
| { kind: "deleted"; deleted: number; bytes: number };
|
|
|
|
export function PurgeOrphansButton() {
|
|
const [state, setState] = useState<State>({ kind: "idle" });
|
|
const [pending, start] = useTransition();
|
|
|
|
const scan = () => {
|
|
setState({ kind: "scanning" });
|
|
start(async () => {
|
|
const r = await previewOrphanFiles();
|
|
setState({ kind: "result", count: r.count, bytes: r.bytes });
|
|
});
|
|
};
|
|
|
|
const purge = () => {
|
|
if (state.kind !== "result") return;
|
|
if (!confirm(`Delete ${state.count} orphan file${state.count === 1 ? "" : "s"} (${formatBytes(state.bytes)}) from disk? Cannot be undone.`)) return;
|
|
start(async () => {
|
|
const r = await purgeOrphanFiles();
|
|
setState({ kind: "deleted", deleted: r.deleted, bytes: r.bytes });
|
|
});
|
|
};
|
|
|
|
return (
|
|
<div className="flex items-start justify-between gap-4 py-2">
|
|
<div className="min-w-0">
|
|
<div className="text-sm font-medium">Purge Orphan Files</div>
|
|
<div className="text-xs text-[var(--color-fg-muted)] mt-0.5">
|
|
Find and delete files in the library / thumbnail folders that no image record references.
|
|
Useful after deleting images with “Delete files from disk” turned off.
|
|
</div>
|
|
{state.kind === "result" && (
|
|
<div className="text-xs mt-2">
|
|
<span className="font-mono text-[var(--color-cyan)]">{state.count}</span>
|
|
<span className="text-[var(--color-fg-dim)]"> orphan{state.count === 1 ? "" : "s"} · {formatBytes(state.bytes)}</span>
|
|
</div>
|
|
)}
|
|
{state.kind === "deleted" && (
|
|
<div className="flex items-center gap-1.5 text-xs text-[var(--color-mint)] mt-2">
|
|
<CheckCircle2 className="w-3.5 h-3.5" />
|
|
Deleted {state.deleted} file{state.deleted === 1 ? "" : "s"} · freed {formatBytes(state.bytes)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="flex-shrink-0 flex items-center gap-2">
|
|
{state.kind === "idle" && (
|
|
<button
|
|
onClick={scan}
|
|
disabled={pending}
|
|
className="inline-flex items-center justify-center gap-1.5 min-w-[100px] text-xs px-3 py-1.5 rounded-lg glass glass-hover text-[var(--color-fg-dim)] hover:text-[var(--color-fg)] whitespace-nowrap"
|
|
>
|
|
Scan
|
|
</button>
|
|
)}
|
|
{state.kind === "scanning" && (
|
|
<span className="flex items-center gap-1.5 text-xs text-[var(--color-fg-dim)]">
|
|
<Loader2 className="w-3.5 h-3.5 animate-spin" /> Scanning…
|
|
</span>
|
|
)}
|
|
{state.kind === "result" && state.count > 0 && (
|
|
<>
|
|
<button
|
|
onClick={() => setState({ kind: "idle" })}
|
|
className="inline-flex items-center justify-center min-w-[80px] text-xs px-3 py-1.5 rounded-lg text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] hover:bg-[var(--color-glass)]"
|
|
>
|
|
Cancel
|
|
</button>
|
|
<button
|
|
onClick={purge}
|
|
disabled={pending}
|
|
className="inline-flex items-center justify-center gap-1.5 min-w-[100px] text-xs px-3 py-1.5 rounded-lg bg-[var(--color-coral)]/15 text-[var(--color-coral)] border border-[var(--color-coral)]/40 hover:bg-[var(--color-coral)]/25 disabled:opacity-40 whitespace-nowrap"
|
|
>
|
|
<Trash2 className="w-3.5 h-3.5" /> {pending ? "Deleting…" : "Delete"}
|
|
</button>
|
|
</>
|
|
)}
|
|
{state.kind === "result" && state.count === 0 && (
|
|
<button
|
|
onClick={() => setState({ kind: "idle" })}
|
|
className="inline-flex items-center justify-center min-w-[80px] text-xs px-3 py-1.5 rounded-lg text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] hover:bg-[var(--color-glass)]"
|
|
>
|
|
Dismiss
|
|
</button>
|
|
)}
|
|
{state.kind === "deleted" && (
|
|
<button
|
|
onClick={() => setState({ kind: "idle" })}
|
|
className="inline-flex items-center justify-center min-w-[80px] text-xs px-3 py-1.5 rounded-lg text-[var(--color-fg-muted)] hover:text-[var(--color-fg)] hover:bg-[var(--color-glass)]"
|
|
>
|
|
Done
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|