feat: expand media exploration and tagging controls
This commit is contained in:
@@ -58,6 +58,8 @@ export function BackgroundTasks() {
|
||||
const mediaJobProgress = useGalleryStore((state) => state.mediaJobProgress);
|
||||
const retryFailedEmbeddings = useGalleryStore((state) => state.retryFailedEmbeddings);
|
||||
const clearTaggingJobs = useGalleryStore((state) => state.clearTaggingJobs);
|
||||
const duplicateScanning = useGalleryStore((state) => state.duplicateScanning);
|
||||
const duplicateScanProgress = useGalleryStore((state) => state.duplicateScanProgress);
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [dismissed, setDismissed] = useState<Record<number, string>>({});
|
||||
const [paused, setPaused] = useState<Record<number, Record<WorkerKey, boolean>>>({});
|
||||
@@ -127,6 +129,7 @@ export function BackgroundTasks() {
|
||||
};
|
||||
|
||||
const dismissTask = (id: number, snapshot: string) => {
|
||||
if (id < 0) return; // system tasks (duplicate scan) cannot be dismissed
|
||||
void clearTaggingJobs(id);
|
||||
setDismissed((prev) => ({ ...prev, [id]: snapshot }));
|
||||
setExpanded(false);
|
||||
@@ -244,10 +247,35 @@ export function BackgroundTasks() {
|
||||
.filter((t) => dismissed[t.id] !== t.snapshot);
|
||||
}, [folders, indexingProgress, mediaJobProgress, dismissed]);
|
||||
|
||||
if (tasks.length === 0) return null;
|
||||
// Synthetic task for duplicate scanning — negative id so dismiss/retry are suppressed
|
||||
const duplicateScanTask: Task | null = duplicateScanning ? {
|
||||
id: -1,
|
||||
name: "Duplicate Scan",
|
||||
stages: [{
|
||||
label: "Hashing",
|
||||
detail: duplicateScanProgress
|
||||
? `${duplicateScanProgress.scanned.toLocaleString()} / ${duplicateScanProgress.total.toLocaleString()}`
|
||||
: "Starting…",
|
||||
progress: duplicateScanProgress && duplicateScanProgress.total > 0
|
||||
? (duplicateScanProgress.scanned / duplicateScanProgress.total) * 100
|
||||
: null,
|
||||
failed: false,
|
||||
}],
|
||||
hasFailedEmbeddings: false,
|
||||
hasFailedTagging: false,
|
||||
pendingMediaWork: 1,
|
||||
embeddingProcessed: 0,
|
||||
embeddingTotal: 0,
|
||||
currentFile: null,
|
||||
snapshot: "",
|
||||
} : null;
|
||||
|
||||
const primary = tasks[0];
|
||||
const extraCount = tasks.length - 1;
|
||||
const allTasks = duplicateScanTask ? [duplicateScanTask, ...tasks] : tasks;
|
||||
|
||||
if (allTasks.length === 0) return null;
|
||||
|
||||
const primary = allTasks[0];
|
||||
const extraCount = allTasks.length - 1;
|
||||
const hasFailed = tasks.some((t) => (t.hasFailedEmbeddings || t.hasFailedTagging) && t.pendingMediaWork === 0);
|
||||
|
||||
// Best progress bar value: use embedding progress if available (most informative),
|
||||
@@ -348,8 +376,8 @@ export function BackgroundTasks() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Expand chevron (only when multiple folders) */}
|
||||
{tasks.length > 1 && (
|
||||
{/* Expand chevron (only when multiple tasks) */}
|
||||
{allTasks.length > 1 && (
|
||||
<svg
|
||||
className={`h-3.5 w-3.5 text-gray-600 transition-transform duration-200 shrink-0 ${expanded ? "rotate-180" : ""}`}
|
||||
fill="none" viewBox="0 0 24 24" stroke="currentColor"
|
||||
@@ -358,22 +386,24 @@ export function BackgroundTasks() {
|
||||
</svg>
|
||||
)}
|
||||
|
||||
{/* Dismiss */}
|
||||
<button
|
||||
className="p-1 rounded-md text-gray-600 hover:text-gray-300 hover:bg-white/8 transition-colors shrink-0"
|
||||
title="Dismiss"
|
||||
onClick={(e) => { e.stopPropagation(); dismissTask(primary.id, primary.snapshot); }}
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
{/* Dismiss — hidden for system tasks like duplicate scan */}
|
||||
{primary.id >= 0 && (
|
||||
<button
|
||||
className="p-1 rounded-md text-gray-600 hover:text-gray-300 hover:bg-white/8 transition-colors shrink-0"
|
||||
title="Dismiss"
|
||||
onClick={(e) => { e.stopPropagation(); dismissTask(primary.id, primary.snapshot); }}
|
||||
>
|
||||
<svg className="h-3.5 w-3.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Expanded panel — one row per folder */}
|
||||
{expanded && (
|
||||
<div className="border-t border-white/[0.06] bg-white/[0.02] px-5 py-3 space-y-3">
|
||||
{tasks.map((task) => {
|
||||
{allTasks.map((task) => {
|
||||
const taskEmbeddingStage = task.stages.find((s) => s.label === "Embeddings");
|
||||
const taskTaggingStage = task.stages.find((s) => s.label === "Tags");
|
||||
const taskScanningStage = task.stages.find((s) => s.label === "Scanning");
|
||||
@@ -453,15 +483,17 @@ export function BackgroundTasks() {
|
||||
</button>
|
||||
)}
|
||||
|
||||
<button
|
||||
className="p-1 rounded-md text-gray-600 hover:text-gray-300 hover:bg-white/8 transition-colors shrink-0"
|
||||
title="Dismiss"
|
||||
onClick={() => dismissTask(task.id, task.snapshot)}
|
||||
>
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
{task.id >= 0 && (
|
||||
<button
|
||||
className="p-1 rounded-md text-gray-600 hover:text-gray-300 hover:bg-white/8 transition-colors shrink-0"
|
||||
title="Dismiss"
|
||||
onClick={() => dismissTask(task.id, task.snapshot)}
|
||||
>
|
||||
<svg className="h-3 w-3" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{task.currentFile && (
|
||||
|
||||
Reference in New Issue
Block a user