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:
@@ -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 ? (
|
||||
|
||||
@@ -135,6 +135,7 @@ export interface TempUpload {
|
||||
iqdb_data: E621IqdbCandidate[] | null;
|
||||
library_j_id: string | null;
|
||||
file_url: string | null;
|
||||
preview_url: string | null;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user