Upload board: dismiss all, real previews for indexed records

- 'dismiss all' clears every indexed record at once
- Indexed cards show the actual file preview: completed records now get a
  signed library media URL (raw for images, thumbnail for videos) so
  <img>/<video> tags can load it, including items hidden from guests
- Media raw/thumbnail endpoints accept the signature for anonymous
  requests and fall back to the normal guest-filtered path otherwise
- Guest blacklist keeps a persistent Redis mirror: an expired TTL or an
  unreachable e621 keeps the last successful list instead of falling
  back to the small local list
This commit is contained in:
2026-09-17 11:29:56 -05:00
parent b71ec729e0
commit 1086beb974
6 changed files with 117 additions and 19 deletions
+39 -11
View File
@@ -4,7 +4,7 @@ import {
UploadCloud,
X,
} from "lucide-react";
import { useRef, useState, type DragEvent } from "react";
import { useRef, useState, type DragEvent, type ReactNode } from "react";
import { Link } from "react-router-dom";
import { Button, EmptyState, Spinner, inputClass } from "@/components/ui";
@@ -102,14 +102,15 @@ function TempCard({
onDismiss: () => void;
}) {
const isVideo = /\.(mp4|webm)$/i.test(temp.original_filename);
const preview = temp.preview_url ?? temp.file_url;
return (
<div className="rounded-lg border border-ctp-surface0 bg-ctp-base p-2.5">
<div className="relative aspect-video overflow-hidden rounded-md bg-ctp-mantle">
{temp.file_url ? (
{preview ? (
isVideo ? (
<video
src={temp.file_url}
src={preview}
muted
playsInline
preload="metadata"
@@ -117,7 +118,7 @@ function TempCard({
/>
) : (
<img
src={temp.file_url}
src={preview}
alt={temp.original_filename}
loading="lazy"
className="h-full w-full object-cover"
@@ -125,7 +126,7 @@ function TempCard({
)
) : (
<div className="flex h-full w-full items-center justify-center text-xs text-ctp-overlay0">
{temp.status === "completed" ? "Indexed" : "No preview"}
No preview
</div>
)}
{temp.status === "visual_match" && temp.iqdb_data?.length ? (
@@ -303,17 +304,17 @@ function MetadataModal({
<div className="mt-4 grid gap-4 sm:grid-cols-[200px_minmax(0,1fr)]">
<div className="overflow-hidden rounded-md border border-ctp-surface0 bg-ctp-mantle">
{temp.file_url ? (
{(temp.preview_url ?? temp.file_url) ? (
isVideo ? (
<video
src={temp.file_url}
src={(temp.preview_url ?? temp.file_url) as string}
controls
muted
className="max-h-52 w-full object-contain"
/>
) : (
<img
src={temp.file_url}
src={(temp.preview_url ?? temp.file_url) as string}
alt={temp.original_filename}
className="max-h-52 w-full object-contain"
/>
@@ -715,6 +716,15 @@ export default function UploadPage() {
);
}
function dismissAll(items: TempUpload[]) {
if (items.length === 0) return;
void Promise.allSettled(
items.map((temp) =>
api(`/api/uploads/${temp.temp_id}/`, { method: "DELETE" }),
),
).then(() => invalidateUploads());
}
function clearFinished() {
setEntries((current) => current.filter((entry) => entry.status !== "done"));
}
@@ -737,12 +747,16 @@ export default function UploadPage() {
title: string,
items: TempUpload[],
emptyText: string,
action?: ReactNode,
) => (
<section className="flex min-w-0 flex-col gap-3">
<header className="flex items-center justify-between gap-2">
<h2 className="text-sm font-semibold text-ctp-subtext1">{title}</h2>
<span className="font-mono text-[11px] text-ctp-overlay0">
{items.length}
<span className="flex items-center gap-2">
{action}
<span className="font-mono text-[11px] text-ctp-overlay0">
{items.length}
</span>
</span>
</header>
{items.length === 0 ? (
@@ -888,7 +902,21 @@ export default function UploadPage() {
visual,
"No IQDB matches right now.",
)}
{column("Auto-uploaded & Indexed", completed, "Nothing indexed yet.")}
{column(
"Auto-uploaded & Indexed",
completed,
"Nothing indexed yet.",
completed.length > 0 ? (
<button
type="button"
onClick={() => dismissAll(completed)}
title="Dismiss every indexed record"
className="rounded-md px-1.5 py-0.5 font-mono text-[10px] text-ctp-overlay0 transition hover:bg-ctp-surface0 hover:text-ctp-text"
>
dismiss all
</button>
) : null,
)}
</div>
{modalFor ? (