import { useQueryClient } from "@tanstack/react-query"; import { Check, Video, X } from "lucide-react"; import { useState } from "react"; import { Button, Spinner } from "@/components/ui"; import { api, apiUrl, errorMessage } from "@/lib/api"; import { cn } from "@/lib/cn"; import type { TempUpload } from "@/lib/types"; import { toast } from "@/store/toasts"; const RATING_OPTIONS = [ { value: "s", label: "Safe", active: "border-ctp-green/40 bg-ctp-green/15 text-ctp-green", }, { value: "q", label: "Questionable", active: "border-ctp-peach/40 bg-ctp-peach/15 text-ctp-peach", }, { value: "e", label: "Explicit", active: "border-ctp-red/40 bg-ctp-red/15 text-ctp-red", }, ]; /** * How many files one request resolves. Copying and hashing is serial per * file on the server, so a 200-file batch in one request risks the proxy * timeout and gives no feedback; small chunks keep every request short and * let the bar advance as they land. */ const CHUNK_SIZE = 8; interface BulkResult { resolved: string[]; errors: { temp_id: string; error: string }[]; } interface Failure { id: string; name: string; error: string; } type Phase = "select" | "running" | "done"; /** * Bulk-rating tool for the Pending & Unmatched column: pick a rating, tick * every thumbnail that belongs to it, and they all move into the library, * reporting progress chunk by chunk. */ export function BulkRateModal({ uploads, onClose, }: { uploads: TempUpload[]; onClose: () => void; }) { const queryClient = useQueryClient(); const [rating, setRating] = useState(null); const [selected, setSelected] = useState>(new Set()); const [phase, setPhase] = useState("select"); const [progress, setProgress] = useState({ done: 0, total: 0, moved: 0 }); const [failures, setFailures] = useState([]); const names = new Map(uploads.map((temp) => [temp.temp_id, temp.original_filename])); const busy = phase === "running"; const percent = progress.total > 0 ? Math.min(100, Math.round((progress.done / progress.total) * 100)) : 0; const retryable = failures.filter((failure) => names.has(failure.id)); async function run() { if (!rating || selected.size === 0 || busy) return; const ids = [...selected]; setPhase("running"); setFailures([]); setProgress({ done: 0, total: ids.length, moved: 0 }); let moved = 0; const problems: Failure[] = []; for (let index = 0; index < ids.length; index += CHUNK_SIZE) { const chunk = ids.slice(index, index + CHUNK_SIZE); try { const result = await api("/api/uploads/resolve-bulk/", { method: "POST", json: { temp_ids: chunk, rating }, }); moved += result.resolved.length; for (const problem of result.errors) { problems.push({ id: problem.temp_id, name: names.get(problem.temp_id) ?? problem.temp_id, error: problem.error, }); } } catch (error) { for (const id of chunk) { problems.push({ id, name: names.get(id) ?? id, error: errorMessage(error), }); } } setProgress({ done: Math.min(index + CHUNK_SIZE, ids.length), total: ids.length, moved, }); } void queryClient.invalidateQueries({ queryKey: ["uploads"] }); void queryClient.invalidateQueries({ queryKey: ["files"] }); setFailures(problems); setPhase("done"); if (problems.length === 0) { toast.ok(`Moved ${moved} upload(s) into the library.`); onClose(); } else if (moved > 0) { toast.error(`Moved ${moved}, ${problems.length} failed.`); } else { toast.error(`Nothing could be moved (${problems.length} failed).`); } } function toggle(id: string) { setSelected((current) => { const next = new Set(current); if (next.has(id)) next.delete(id); else next.add(id); return next; }); } function requestClose() { if (!busy) onClose(); } function retryFailed() { setSelected(new Set(retryable.map((failure) => failure.id))); setFailures([]); setPhase("select"); } return (
event.stopPropagation()} >

Rate unmatched uploads

{busy ? "Moving files into the library — this can take a moment, keep this open." : "Pick a rating, tick every file that belongs to it, and they move into the library in one go (" + uploads.length + " pending)."}

{RATING_OPTIONS.map((option) => ( ))}
{uploads.length === 0 ? (

Nothing pending.

) : (
{uploads.map((temp) => { const isSelected = selected.has(temp.temp_id); const preview = apiUrl( temp.preview_url ?? temp.file_url ?? "", ); return ( ); })}
)}
); }