mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-07-22 23:12:02 +00:00
feat(stls): enlarge candidate images in preview picker
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
Each loaded candidate gets an enlarge button that opens the ImageLightbox without changing selection, so the right preview is easier to choose. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import {
|
|||||||
Check,
|
Check,
|
||||||
AlertCircle,
|
AlertCircle,
|
||||||
ImageOff,
|
ImageOff,
|
||||||
|
Maximize2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Dialog,
|
Dialog,
|
||||||
@@ -20,6 +21,7 @@ import { Button } from "@/components/ui/button";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { setPreviewFromExtract } from "../actions";
|
import { setPreviewFromExtract } from "../actions";
|
||||||
|
import { ImageLightbox } from "./image-lightbox";
|
||||||
|
|
||||||
interface ArchiveImage {
|
interface ArchiveImage {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -65,6 +67,7 @@ export function ArchivePreviewPicker({
|
|||||||
const [thumbnails, setThumbnails] = useState<Map<string, ThumbnailState>>(new Map());
|
const [thumbnails, setThumbnails] = useState<Map<string, ThumbnailState>>(new Map());
|
||||||
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
const [selectedPath, setSelectedPath] = useState<string | null>(null);
|
||||||
const [isPending, startTransition] = useTransition();
|
const [isPending, startTransition] = useTransition();
|
||||||
|
const [lightboxSrc, setLightboxSrc] = useState<string | null>(null);
|
||||||
const pollTimers = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
|
const pollTimers = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map());
|
||||||
// Track which paths have already been requested to avoid re-requesting
|
// Track which paths have already been requested to avoid re-requesting
|
||||||
const requestedPaths = useRef<Set<string>>(new Set());
|
const requestedPaths = useRef<Set<string>>(new Set());
|
||||||
@@ -290,71 +293,85 @@ export function ArchivePreviewPicker({
|
|||||||
const isFailed = thumbState?.status === "failed";
|
const isFailed = thumbState?.status === "failed";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<div key={img.id} className="group relative">
|
||||||
key={img.id}
|
{isLoaded && thumbState?.imageUrl && (
|
||||||
type="button"
|
<button
|
||||||
className={cn(
|
type="button"
|
||||||
"relative aspect-square rounded-lg overflow-hidden border-2 transition-all",
|
className="absolute top-1.5 left-1.5 z-10 flex h-6 w-6 items-center justify-center rounded-md bg-black/60 text-white opacity-0 transition-opacity hover:bg-black/80 group-hover:opacity-100"
|
||||||
"hover:border-primary/50 cursor-pointer group",
|
onClick={(e) => {
|
||||||
isSelected
|
e.stopPropagation();
|
||||||
? "border-primary ring-2 ring-primary/30"
|
setLightboxSrc(thumbState.imageUrl!);
|
||||||
: "border-border",
|
}}
|
||||||
isFailed && "opacity-60"
|
title="Enlarge"
|
||||||
)}
|
>
|
||||||
onClick={() => {
|
<Maximize2 className="h-3.5 w-3.5" />
|
||||||
if (isLoaded) {
|
</button>
|
||||||
setSelectedPath(img.path);
|
|
||||||
} else if (isFailed) {
|
|
||||||
// Allow retry on failed
|
|
||||||
requestedPaths.current.delete(img.path);
|
|
||||||
requestThumbnail(img.path);
|
|
||||||
} else if (!thumbState || thumbState.status === "idle") {
|
|
||||||
requestThumbnail(img.path);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
title={img.path}
|
|
||||||
>
|
|
||||||
{isLoaded && thumbState.imageUrl ? (
|
|
||||||
<img
|
|
||||||
src={thumbState.imageUrl}
|
|
||||||
alt={img.fileName}
|
|
||||||
className="h-full w-full object-cover"
|
|
||||||
loading="lazy"
|
|
||||||
/>
|
|
||||||
) : isLoading ? (
|
|
||||||
<div className="h-full w-full flex items-center justify-center bg-muted">
|
|
||||||
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
) : isFailed ? (
|
|
||||||
<div className="h-full w-full flex flex-col items-center justify-center bg-muted gap-1">
|
|
||||||
<AlertCircle className="h-4 w-4 text-destructive" />
|
|
||||||
<span className="text-[10px] text-destructive px-1 text-center">
|
|
||||||
Click to retry
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="h-full w-full flex items-center justify-center bg-muted">
|
|
||||||
<ImageIcon className="h-5 w-5 text-muted-foreground" />
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cn(
|
||||||
|
"relative aspect-square w-full rounded-lg overflow-hidden border-2 transition-all",
|
||||||
|
"hover:border-primary/50 cursor-pointer",
|
||||||
|
isSelected
|
||||||
|
? "border-primary ring-2 ring-primary/30"
|
||||||
|
: "border-border",
|
||||||
|
isFailed && "opacity-60"
|
||||||
|
)}
|
||||||
|
onClick={() => {
|
||||||
|
if (isLoaded) {
|
||||||
|
setSelectedPath(img.path);
|
||||||
|
} else if (isFailed) {
|
||||||
|
// Allow retry on failed
|
||||||
|
requestedPaths.current.delete(img.path);
|
||||||
|
requestThumbnail(img.path);
|
||||||
|
} else if (!thumbState || thumbState.status === "idle") {
|
||||||
|
requestThumbnail(img.path);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
title={img.path}
|
||||||
|
>
|
||||||
|
{isLoaded && thumbState.imageUrl ? (
|
||||||
|
<img
|
||||||
|
src={thumbState.imageUrl}
|
||||||
|
alt={img.fileName}
|
||||||
|
className="h-full w-full object-cover"
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
) : isLoading ? (
|
||||||
|
<div className="h-full w-full flex items-center justify-center bg-muted">
|
||||||
|
<Loader2 className="h-5 w-5 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
) : isFailed ? (
|
||||||
|
<div className="h-full w-full flex flex-col items-center justify-center bg-muted gap-1">
|
||||||
|
<AlertCircle className="h-4 w-4 text-destructive" />
|
||||||
|
<span className="text-[10px] text-destructive px-1 text-center">
|
||||||
|
Click to retry
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="h-full w-full flex items-center justify-center bg-muted">
|
||||||
|
<ImageIcon className="h-5 w-5 text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* Selection checkmark */}
|
{/* Selection checkmark */}
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
<div className="absolute top-1.5 right-1.5 h-5 w-5 rounded-full bg-primary flex items-center justify-center">
|
<div className="absolute top-1.5 right-1.5 h-5 w-5 rounded-full bg-primary flex items-center justify-center">
|
||||||
<Check className="h-3 w-3 text-primary-foreground" />
|
<Check className="h-3 w-3 text-primary-foreground" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* File info overlay */}
|
{/* File info overlay */}
|
||||||
<div className="absolute bottom-0 left-0 right-0 bg-black/60 px-1.5 py-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
<div className="absolute bottom-0 left-0 right-0 bg-black/60 px-1.5 py-1 opacity-0 group-hover:opacity-100 transition-opacity">
|
||||||
<p className="text-[10px] text-white truncate">
|
<p className="text-[10px] text-white truncate">
|
||||||
{img.fileName}
|
{img.fileName}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-[9px] text-white/70">
|
<p className="text-[9px] text-white/70">
|
||||||
{formatBytes(img.size)}
|
{formatBytes(img.size)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -394,6 +411,13 @@ export function ArchivePreviewPicker({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
<ImageLightbox
|
||||||
|
src={lightboxSrc}
|
||||||
|
open={!!lightboxSrc}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) setLightboxSrc(null);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user