Initial commit
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
import { useState, useTransition } from "react";
|
||||
import { FolderTree, Loader2, CheckCircle2 } from "lucide-react";
|
||||
import { previewReorganize, reorganizeFiles } from "@/app/actions/maintenance";
|
||||
|
||||
type State =
|
||||
| { kind: "idle" }
|
||||
| { kind: "scanning" }
|
||||
| { kind: "preview"; total: number; toMove: number }
|
||||
| { kind: "running" }
|
||||
| { kind: "done"; moved: number; skipped: number; errors: number };
|
||||
|
||||
export function ReorganizeButton() {
|
||||
const [state, setState] = useState<State>({ kind: "idle" });
|
||||
const [pending, start] = useTransition();
|
||||
|
||||
const scan = () => {
|
||||
setState({ kind: "scanning" });
|
||||
start(async () => {
|
||||
const r = await previewReorganize();
|
||||
setState({ kind: "preview", total: r.total, toMove: r.toMove });
|
||||
});
|
||||
};
|
||||
|
||||
const run = () => {
|
||||
if (state.kind !== "preview") return;
|
||||
if (!confirm(`Move ${state.toMove} file${state.toMove === 1 ? "" : "s"} into letter buckets on disk? This relocates files; cannot be undone.`)) return;
|
||||
setState({ kind: "running" });
|
||||
start(async () => {
|
||||
const r = await reorganizeFiles();
|
||||
setState({ kind: "done", ...r });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex items-start justify-between gap-4 py-2">
|
||||
<div className="min-w-0">
|
||||
<div className="text-sm font-medium">Re-organize Files</div>
|
||||
<div className="text-xs text-[var(--color-fg-muted)] mt-0.5">
|
||||
Move covers into letter buckets on disk —{" "}
|
||||
<code className="font-mono">A-E / F-J / K-P / Q-U / V-Z</code> at the top level, single-letter
|
||||
folders inside, keyed off each cover's code. Files without a code go to{" "}
|
||||
<code className="font-mono">#/#/</code>. Attached images bucket with their parent.
|
||||
</div>
|
||||
{state.kind === "preview" && (
|
||||
<div className="text-xs mt-2">
|
||||
<span className="font-mono text-[var(--color-cyan)]">{state.toMove}</span>
|
||||
<span className="text-[var(--color-fg-dim)]"> of {state.total} need to move</span>
|
||||
</div>
|
||||
)}
|
||||
{state.kind === "done" && (
|
||||
<div className="flex items-center gap-1.5 text-xs text-[var(--color-mint)] mt-2">
|
||||
<CheckCircle2 className="w-3.5 h-3.5" />
|
||||
Moved {state.moved} · skipped {state.skipped}
|
||||
{state.errors > 0 && <span className="text-[var(--color-coral)]">· {state.errors} error{state.errors === 1 ? "" : "s"}</span>}
|
||||
</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" || state.kind === "running") && (
|
||||
<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" />
|
||||
{state.kind === "scanning" ? "Scanning…" : "Moving…"}
|
||||
</span>
|
||||
)}
|
||||
{state.kind === "preview" && state.toMove > 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={run}
|
||||
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-cyan)]/15 text-[var(--color-cyan)] border border-[var(--color-cyan)]/40 hover:bg-[var(--color-cyan)]/25 disabled:opacity-40 whitespace-nowrap"
|
||||
>
|
||||
<FolderTree className="w-3.5 h-3.5" /> Re-organize
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{state.kind === "preview" && state.toMove === 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 === "done" && (
|
||||
<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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user