import { useQuery, useQueryClient } from "@tanstack/react-query"; import { CheckCircle2, UploadCloud, X, } from "lucide-react"; import { useRef, useState, type DragEvent } from "react"; import { Link } from "react-router-dom"; import { Button, EmptyState, Spinner, inputClass } from "@/components/ui"; import { api, authHeaders, errorMessage, uploadFile } from "@/lib/api"; import { cn } from "@/lib/cn"; import { E621Error, e621ErrorMessage, effectiveCredentials, fetchPost, fetchPostsByMd5, iqdbSearch, } from "@/lib/e621"; import { formatBytes } from "@/lib/format"; import type { E621IqdbCandidate, Paginated, Rating, TempUpload, } from "@/lib/types"; import { useAuth } from "@/store/auth"; import { useE621 } from "@/store/e621"; type UploadStatus = "queued" | "uploading" | "done" | "error"; interface UploadEntry { id: string; file: File; progress: number; status: UploadStatus; error?: string; } const RESOLUTION_LABELS: Record = { auto_md5: "auto-matched on e621", duplicate: "already in the library", linked: "linked to a post", custom: "custom metadata", }; const ratingChoices: { value: Rating; label: string; active: string }[] = [ { value: "", label: "None", active: "border-ctp-surface2 bg-ctp-surface0 text-ctp-text", }, { 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", }, ]; function describeError(error: unknown): string { if (error instanceof E621Error) return e621ErrorMessage(error); return errorMessage(error); } async function fetchTempBlob(url: string): Promise { const response = await fetch(url, { headers: authHeaders() }); if (!response.ok) { throw new Error(`Could not read the staged file (${response.status}).`); } return response.blob(); } function TempCard({ temp, checking, onOpen, onCheck, onDismiss, }: { temp: TempUpload; checking: boolean; onOpen: () => void; onCheck: () => void; onDismiss: () => void; }) { const isVideo = /\.(mp4|webm)$/i.test(temp.original_filename); return (
{temp.file_url ? ( isVideo ? (

{temp.original_filename}

{formatBytes(temp.size)} · {temp.md5.slice(0, 10)}…

{temp.status === "completed" && temp.library_j_id ? (

{temp.library_j_id} {RESOLUTION_LABELS[temp.resolution] ?? temp.resolution}

) : null} {temp.status === "error" ? (

Something went wrong while indexing this file.

) : null} {temp.status === "pending" || temp.status === "visual_match" ? (
{temp.status === "pending" ? ( ) : null}
) : null}
); } function MetadataModal({ temp, onClose, }: { temp: TempUpload; onClose: () => void; }) { const queryClient = useQueryClient(); const credentials = useE621((state) => state.credentials); const [tab, setTab] = useState<"link" | "custom">("link"); const [postId, setPostId] = useState(""); const [rating, setRating] = useState(temp.custom_rating); const [tags, setTags] = useState(temp.custom_tags.join(", ")); const [notes, setNotes] = useState(temp.custom_notes); const [busy, setBusy] = useState(false); const [error, setError] = useState(null); const isVideo = /\.(mp4|webm)$/i.test(temp.original_filename); async function run(task: () => Promise) { setBusy(true); setError(null); try { await task(); void queryClient.invalidateQueries({ queryKey: ["uploads"] }); void queryClient.invalidateQueries({ queryKey: ["files"] }); onClose(); } catch (err) { setError(describeError(err)); setBusy(false); } } function linkById() { const parsed = Number(postId.trim()); if (!Number.isFinite(parsed) || parsed <= 0) { setError("Enter a valid e621 post ID."); return; } return run(async () => { const post = await fetchPost(effectiveCredentials(credentials), parsed); await api(`/api/uploads/${temp.temp_id}/resolve/`, { method: "POST", json: { mode: "link", post, rating, tags, notes }, }); }); } function linkCandidate(candidate: E621IqdbCandidate) { if (!candidate.post_id) return; return run(async () => { const post = await fetchPost( effectiveCredentials(credentials), candidate.post_id as number, ); await api(`/api/uploads/${temp.temp_id}/resolve/`, { method: "POST", json: { mode: "link", post, rating, tags, notes }, }); }); } function saveCustom() { return run(async () => { await api(`/api/uploads/${temp.temp_id}/resolve/`, { method: "POST", json: { mode: "custom", rating, tags, notes }, }); }); } return (
event.stopPropagation()} >

Set metadata

{temp.original_filename}

{temp.file_url ? ( isVideo ? (
{tab === "link" ? (
{temp.iqdb_data && temp.iqdb_data.length > 0 ? (

IQDB candidates

{temp.iqdb_data.map((candidate) => ( ))}
) : (

No IQDB candidates yet — run "Check similarity" from the card, or enter a post ID below.

)}
setPostId(event.target.value)} />
) : (
{ratingChoices.map((choice) => ( ))}
setTags(event.target.value)} />