import { createFileRoute } from "@tanstack/react-router"; import { useState, useMemo } from "react"; import { useMutation } from "@tanstack/react-query"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Checkbox } from "@/components/ui/checkbox"; import { Badge } from "@/components/ui/badge"; import { Plus, ChevronLeft, ChevronRight, Download, Search, Trash2, Loader2 } from "lucide-react"; import { useSites } from "@/lib/grabber/useSites"; import { searchPosts } from "@/lib/grabber/search.functions"; import type { NormalizedPost } from "@/lib/grabber/sites"; import { AddSiteDialog } from "@/components/grabber/AddSiteDialog"; import { Lightbox } from "@/components/grabber/Lightbox"; export const Route = createFileRoute("/")({ head: () => ({ meta: [ { title: "Imageboard Grabber" }, { name: "description", content: "Search booru-style imageboards, browse thumbnails, and batch-download as ZIP.", }, { property: "og:title", content: "Imageboard Grabber" }, { property: "og:description", content: "Search booru-style imageboards, browse thumbnails, and batch-download as ZIP.", }, ], }), component: Grabber, }); function Grabber() { const { sites, custom, addSite, removeSite, hydrated } = useSites(); const [siteId, setSiteId] = useState("e621"); const [tags, setTags] = useState(""); const [limit, setLimit] = useState(40); const [page, setPage] = useState(1); const [selected, setSelected] = useState>(new Set()); const [addOpen, setAddOpen] = useState(false); const [lightbox, setLightbox] = useState(null); const [downloading, setDownloading] = useState(false); const [excludeTags, setExcludeTags] = useState(""); const [hfToken, setHfToken] = useState(""); const [datasetName, setDatasetName] = useState(""); const [uploading, setUploading] = useState(false); const [uploadStatus, setUploadStatus] = useState(""); const site = useMemo(() => sites.find((s) => s.id === siteId) ?? sites[0], [sites, siteId]); const searchMut = useMutation({ mutationFn: async (vars: { page: number }) => { return await searchPosts({ data: { site, tags, page: vars.page, limit } }); }, }); const posts = searchMut.data?.posts ?? []; const err = searchMut.data?.error; const runSearch = (p: number) => { setPage(p); setSelected(new Set()); searchMut.mutate({ page: p }); }; const toggle = (id: string) => { setSelected((prev) => { const n = new Set(prev); if (n.has(id)) n.delete(id); else n.add(id); return n; }); }; const selectAll = () => { if (selected.size === posts.length) setSelected(new Set()); else setSelected(new Set(posts.map((p) => p.id))); }; const downloadZip = async () => { const chosen = posts.filter((p) => selected.has(p.id)); if (!chosen.length) return; setDownloading(true); try { const res = await fetch("/api/download-zip", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ siteName: site.id, posts: chosen, excludeTags: excludeTags .split(/\s+|,/) .map((t) => t.trim().toLowerCase()) .filter(Boolean), }), }); if (!res.ok) { const t = await res.text(); alert(`Download failed: ${t}`); return; } const blob = await res.blob(); const url = URL.createObjectURL(blob); const a = document.createElement("a"); a.href = url; a.download = `grabber-${site.id}-${Date.now()}.zip`; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } finally { setDownloading(false); } }; const uploadToDataset = async () => { const chosen = posts.filter((p) => selected.has(p.id)); if (!chosen.length) { alert("Please select some images to upload."); return; } if (!hfToken.trim() || !datasetName.trim()) { alert("Hugging Face write token and dataset name are required."); return; } setUploading(true); setUploadStatus("Uploading to Hugging Face..."); try { const res = await fetch("/api/upload-to-dataset", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ siteName: site.id, posts: chosen, excludeTags: excludeTags .split(/\s+|,/) .map((t) => t.trim().toLowerCase()) .filter(Boolean), hfToken: hfToken.trim(), datasetName: datasetName.trim(), }), }); const data = await res.json(); if (!res.ok) { setUploadStatus(`Upload failed: ${data.error || "Unknown error"}`); } else { setUploadStatus(`Success! Uploaded ZIP as ${data.filename}`); } } catch (err: unknown) { const errMsg = err instanceof Error ? err.message : "Network error"; setUploadStatus(`Upload failed: ${errMsg}`); } finally { setUploading(false); } }; return (

Grabber

{hydrated && custom.some((c) => c.id === siteId) && ( )}
setTags(e.target.value)} onKeyDown={(e) => e.key === "Enter" && runSearch(1)} className="flex-1 min-w-64" /> setLimit(Number(e.target.value) || 40)} className="w-24" />
page {page}
selected: {selected.size} setExcludeTags(e.target.value)} className="w-64 max-w-xs" />
{/* Hugging Face Dataset Integration Panel */}
Hugging Face Upload: setHfToken(e.target.value)} className="w-48 max-w-xs" /> setDatasetName(e.target.value)} className="w-64 max-w-xs" /> {uploadStatus && ( {uploadStatus} )}
{err && (
{err}
)} {searchMut.isPending && (
Loading…
)} {!searchMut.isPending && posts.length === 0 && searchMut.isSuccess && !err && (
No results.
)} {!searchMut.isPending && !searchMut.isSuccess && (
Enter tags and press Search.
)}
{posts.map((p) => { const isSel = selected.has(p.id); return (
setLightbox(p)} >
{`post (e.currentTarget.style.opacity = "0.2")} />
{ e.stopPropagation(); toggle(p.id); }} >
{p.rating && ( {p.rating} )} {p.score !== undefined && ( ★{p.score} )}
); })}
{ addSite(s); setSiteId(s.id); }} /> setLightbox(null)} />
); }