feat: implement thumbnail generation and management
- Remove unused image dependencies from Cargo.lock. - Update API to handle thumbnail generation and checkup processes. - Introduce new routes for rebuilding and regenerating thumbnails. - Enhance job tracking with progress indicators for thumbnail jobs. - Update front-end components to display thumbnail job status and progress. - Add backend logic for managing thumbnail jobs and integrating with the API. - Refactor existing code to accommodate new thumbnail functionalities.
This commit is contained in:
@@ -87,6 +87,8 @@ export function JobProgress({ jobId, onComplete }: JobProgressProps) {
|
||||
const percent = progress.progress_percent ?? 0;
|
||||
const processed = progress.processed_files ?? 0;
|
||||
const total = progress.total_files ?? 0;
|
||||
const isThumbnailsPhase = progress.status === "generating_thumbnails";
|
||||
const unitLabel = isThumbnailsPhase ? "thumbnails" : "files";
|
||||
|
||||
return (
|
||||
<div className="p-4 bg-card rounded-lg border border-border">
|
||||
@@ -100,7 +102,7 @@ export function JobProgress({ jobId, onComplete }: JobProgressProps) {
|
||||
<ProgressBar value={percent} showLabel size="lg" className="mb-3" />
|
||||
|
||||
<div className="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm text-muted-foreground mb-3">
|
||||
<span>{processed} / {total} files</span>
|
||||
<span>{processed} / {total} {unitLabel}</span>
|
||||
{progress.current_file && (
|
||||
<span className="truncate max-w-md" title={progress.current_file}>
|
||||
Current: {progress.current_file.length > 40
|
||||
@@ -110,7 +112,7 @@ export function JobProgress({ jobId, onComplete }: JobProgressProps) {
|
||||
)}
|
||||
</div>
|
||||
|
||||
{progress.stats_json && (
|
||||
{progress.stats_json && !isThumbnailsPhase && (
|
||||
<div className="flex flex-wrap gap-3 text-xs">
|
||||
<Badge variant="primary">Scanned: {progress.stats_json.scanned_files}</Badge>
|
||||
<Badge variant="success">Indexed: {progress.stats_json.indexed_files}</Badge>
|
||||
|
||||
@@ -33,9 +33,8 @@ interface JobRowProps {
|
||||
}
|
||||
|
||||
export function JobRow({ job, libraryName, highlighted, onCancel, formatDate, formatDuration }: JobRowProps) {
|
||||
const [showProgress, setShowProgress] = useState(
|
||||
highlighted || job.status === "running" || job.status === "pending"
|
||||
);
|
||||
const isActive = job.status === "running" || job.status === "pending" || job.status === "generating_thumbnails";
|
||||
const [showProgress, setShowProgress] = useState(highlighted || isActive);
|
||||
|
||||
const handleComplete = () => {
|
||||
setShowProgress(false);
|
||||
@@ -53,12 +52,32 @@ export function JobRow({ job, libraryName, highlighted, onCancel, formatDate, fo
|
||||
const removed = job.stats_json?.removed_files ?? 0;
|
||||
const errors = job.stats_json?.errors ?? 0;
|
||||
|
||||
// Format files display
|
||||
const filesDisplay = job.status === "running" && job.total_files
|
||||
? `${job.processed_files || 0}/${job.total_files}`
|
||||
: scanned > 0
|
||||
? `${scanned} scanned`
|
||||
: "-";
|
||||
const isThumbnailPhase = job.status === "generating_thumbnails";
|
||||
const isThumbnailJob = job.type === "thumbnail_rebuild" || job.type === "thumbnail_regenerate";
|
||||
const hasThumbnailPhase = isThumbnailPhase || isThumbnailJob;
|
||||
|
||||
// Files column: index-phase stats only
|
||||
const filesDisplay =
|
||||
job.status === "running" && !isThumbnailPhase
|
||||
? job.total_files != null
|
||||
? `${job.processed_files ?? 0}/${job.total_files}`
|
||||
: scanned > 0
|
||||
? `${scanned} scanned`
|
||||
: "-"
|
||||
: job.status === "success" && (indexed > 0 || removed > 0 || errors > 0)
|
||||
? null // rendered below as ✓ / − / ⚠
|
||||
: scanned > 0
|
||||
? `${scanned} scanned`
|
||||
: "—";
|
||||
|
||||
// Thumbnails column
|
||||
const thumbInProgress = hasThumbnailPhase && (job.status === "running" || isThumbnailPhase);
|
||||
const thumbDisplay =
|
||||
thumbInProgress && job.total_files != null
|
||||
? `${job.processed_files ?? 0}/${job.total_files}`
|
||||
: job.status === "success" && job.total_files != null && hasThumbnailPhase
|
||||
? `✓ ${job.total_files}`
|
||||
: "—";
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -86,7 +105,7 @@ export function JobRow({ job, libraryName, highlighted, onCancel, formatDate, fo
|
||||
!
|
||||
</span>
|
||||
)}
|
||||
{(job.status === "running" || job.status === "pending") && (
|
||||
{isActive && (
|
||||
<button
|
||||
className="text-xs text-primary hover:text-primary/80 hover:underline"
|
||||
onClick={() => setShowProgress(!showProgress)}
|
||||
@@ -98,21 +117,26 @@ export function JobRow({ job, libraryName, highlighted, onCancel, formatDate, fo
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm text-foreground">{filesDisplay}</span>
|
||||
{job.status === "running" && job.total_files && (
|
||||
<MiniProgressBar
|
||||
value={job.processed_files || 0}
|
||||
max={job.total_files}
|
||||
className="w-24"
|
||||
/>
|
||||
)}
|
||||
{job.status === "success" && (
|
||||
{filesDisplay !== null ? (
|
||||
<span className="text-sm text-foreground">{filesDisplay}</span>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<span className="text-success">✓ {indexed}</span>
|
||||
{removed > 0 && <span className="text-warning">− {removed}</span>}
|
||||
{errors > 0 && <span className="text-error">⚠ {errors}</span>}
|
||||
</div>
|
||||
)}
|
||||
{job.status === "running" && !isThumbnailPhase && job.total_files != null && (
|
||||
<MiniProgressBar value={job.processed_files ?? 0} max={job.total_files} className="w-24" />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<div className="flex flex-col gap-1">
|
||||
<span className="text-sm text-foreground">{thumbDisplay}</span>
|
||||
{thumbInProgress && job.total_files != null && (
|
||||
<MiniProgressBar value={job.processed_files ?? 0} max={job.total_files} className="w-24" />
|
||||
)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-muted-foreground">
|
||||
@@ -129,7 +153,7 @@ export function JobRow({ job, libraryName, highlighted, onCancel, formatDate, fo
|
||||
>
|
||||
View
|
||||
</Link>
|
||||
{(job.status === "pending" || job.status === "running") && (
|
||||
{(job.status === "pending" || job.status === "running" || job.status === "generating_thumbnails") && (
|
||||
<Button
|
||||
variant="danger"
|
||||
size="sm"
|
||||
@@ -141,9 +165,9 @@ export function JobRow({ job, libraryName, highlighted, onCancel, formatDate, fo
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{showProgress && (job.status === "running" || job.status === "pending") && (
|
||||
{showProgress && isActive && (
|
||||
<tr>
|
||||
<td colSpan={8} className="px-4 py-3 bg-muted/50">
|
||||
<td colSpan={9} className="px-4 py-3 bg-muted/50">
|
||||
<JobProgress
|
||||
jobId={job.id}
|
||||
onComplete={handleComplete}
|
||||
|
||||
@@ -78,7 +78,7 @@ export function JobsIndicator() {
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const runningJobs = activeJobs.filter(j => j.status === "running");
|
||||
const runningJobs = activeJobs.filter(j => j.status === "running" || j.status === "generating_thumbnails");
|
||||
const pendingJobs = activeJobs.filter(j => j.status === "pending");
|
||||
const totalCount = activeJobs.length;
|
||||
|
||||
@@ -210,19 +210,19 @@ export function JobsIndicator() {
|
||||
>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="mt-0.5">
|
||||
{job.status === "running" && <span className="animate-spin inline-block">⏳</span>}
|
||||
{(job.status === "running" || job.status === "generating_thumbnails") && <span className="animate-spin inline-block">⏳</span>}
|
||||
{job.status === "pending" && <span>⏸</span>}
|
||||
</div>
|
||||
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<code className="text-xs px-1.5 py-0.5 bg-muted rounded font-mono">{job.id.slice(0, 8)}</code>
|
||||
<Badge variant={job.type === 'rebuild' ? 'primary' : 'secondary'} className="text-[10px]">
|
||||
{job.type}
|
||||
<Badge variant={job.type === 'rebuild' ? 'primary' : job.type === 'thumbnail_regenerate' ? 'warning' : 'secondary'} className="text-[10px]">
|
||||
{job.type === 'thumbnail_rebuild' ? 'Thumbnails' : job.type === 'thumbnail_regenerate' ? 'Regenerate' : job.type}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
{job.status === "running" && job.progress_percent !== null && (
|
||||
{(job.status === "running" || job.status === "generating_thumbnails") && job.progress_percent != null && (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<MiniProgressBar value={job.progress_percent} />
|
||||
<span className="text-xs font-medium text-muted-foreground">{job.progress_percent}%</span>
|
||||
|
||||
@@ -111,6 +111,7 @@ export function JobsList({ initialJobs, libraries, highlightJobId }: JobsListPro
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Type</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Status</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Files</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Thumbnails</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Duration</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Created</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-semibold text-muted-foreground uppercase tracking-wider">Actions</th>
|
||||
|
||||
@@ -60,6 +60,7 @@ export function Badge({ children, variant = "default", className = "" }: BadgePr
|
||||
// Status badge for jobs/tasks
|
||||
const statusVariants: Record<string, BadgeVariant> = {
|
||||
running: "in-progress",
|
||||
generating_thumbnails: "in-progress",
|
||||
success: "completed",
|
||||
completed: "completed",
|
||||
failed: "error",
|
||||
@@ -68,20 +69,33 @@ const statusVariants: Record<string, BadgeVariant> = {
|
||||
unread: "unread",
|
||||
};
|
||||
|
||||
const statusLabels: Record<string, string> = {
|
||||
generating_thumbnails: "Thumbnails",
|
||||
};
|
||||
|
||||
interface StatusBadgeProps {
|
||||
status: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
export function StatusBadge({ status, className = "" }: StatusBadgeProps) {
|
||||
const variant = statusVariants[status.toLowerCase()] || "default";
|
||||
return <Badge variant={variant} className={className}>{status}</Badge>;
|
||||
const key = status.toLowerCase();
|
||||
const variant = statusVariants[key] || "default";
|
||||
const label = statusLabels[key] ?? status;
|
||||
return <Badge variant={variant} className={className}>{label}</Badge>;
|
||||
}
|
||||
|
||||
// Job type badge
|
||||
const jobTypeVariants: Record<string, BadgeVariant> = {
|
||||
rebuild: "primary",
|
||||
full_rebuild: "warning",
|
||||
thumbnail_rebuild: "secondary",
|
||||
thumbnail_regenerate: "warning",
|
||||
};
|
||||
|
||||
const jobTypeLabels: Record<string, string> = {
|
||||
thumbnail_rebuild: "Thumbnails",
|
||||
thumbnail_regenerate: "Regenerate",
|
||||
};
|
||||
|
||||
interface JobTypeBadgeProps {
|
||||
@@ -90,8 +104,10 @@ interface JobTypeBadgeProps {
|
||||
}
|
||||
|
||||
export function JobTypeBadge({ type, className = "" }: JobTypeBadgeProps) {
|
||||
const variant = jobTypeVariants[type.toLowerCase()] || "default";
|
||||
return <Badge variant={variant} className={className}>{type}</Badge>;
|
||||
const key = type.toLowerCase();
|
||||
const variant = jobTypeVariants[key] || "default";
|
||||
const label = jobTypeLabels[key] ?? type;
|
||||
return <Badge variant={variant} className={className}>{label}</Badge>;
|
||||
}
|
||||
|
||||
// Progress badge (shows percentage)
|
||||
|
||||
Reference in New Issue
Block a user