Files
mule-image/frontend/src/components/ScanProgress.tsx
dtoro 8529771122 ui: rework selection/heap visuals, contextual shortcut hints, inline scan activity
- Selection now reads as a blue ring + tint with a springy scale-down,
  hover stays a subtle gray ring so keyboard-driven and mouse-driven
  states are tellable apart.
- Heap membership is signalled with a green tint only (no badge, no
  ring, no scale).
- Discard/restore is optimistic and non-yanking: photos stay in the
  grid greyed out until the next reload, X toggles based on the
  current state, and the same treatment applies in preview.
- Filmstrip mirrors the grid styling (selection blue, heap green,
  discarded grey).
- Preview close restores the LAST viewed photo as the focused/selected
  one in the grid.
- Right sidebar collapses on view change and re-opens when a photo is
  in focus; Esc clears active selection so the panel collapses too.
- Keyboard hints panel is context-aware (grid / preview / discarded
  section), collapsible with H, persisted, and rendered inside the
  preview column above the filmstrip.
- "Pick (P)" renamed to "Select (S)" everywhere.
- Needs review moved into the Flag pill dropdown.
- Fixed vertical videos overflowing the preview column (min-h-0).
- Replaced the bottom-right ScanProgress popover with an inline
  spinner next to the FOLDERS sidebar header (and on the specific
  folder row being scanned). ScanProgress is now a headless
  invalidator; useScanActivity exposes the live status.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-14 23:45:36 +02:00

102 lines
3.5 KiB
TypeScript

import { useEffect, useRef } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { library, WorkerStatus } from '../services/api'
interface ScanStatus {
is_scanning: boolean
current_folder?: string
processed_files: number
total_files: number
errors: string[]
}
type Phase = 'idle' | 'scanning' | 'processing' | 'done'
/**
* Headless background-activity orchestrator. Polls scan + worker status
* and invalidates affected query caches when a scan/processing pass
* completes. The visible status indicator now lives inline in the
* LeftSidebar (small spinner next to the FOLDERS header / specific
* folder rows) — see useScanActivity.
*/
export function ScanProgress() {
const queryClient = useQueryClient()
const wasScanningRef = useRef(false)
const wasProcessingRef = useRef(false)
const { data: scanStatus } = useQuery<ScanStatus>({
queryKey: ['scan-status'],
queryFn: () => library.scanStatus(),
refetchInterval: (query) =>
query.state.data?.is_scanning ? 2000 : 10000,
enabled: true,
})
const isScanning = scanStatus?.is_scanning ?? false
// Poll worker status to track vision queue activity.
// Fast polling (3s) while processing, slow (15s) otherwise.
const { data: workerStatus } = useQuery<WorkerStatus>({
queryKey: ['worker-status-progress'],
queryFn: () => library.maintenance.workerStatus(),
refetchInterval: (query) => {
const q = totalQueued(query.state.data)
return q > 0 ? 3000 : 15000
},
enabled: true,
})
const totalActive = totalQueued(workerStatus)
const phase: Phase = isScanning
? 'scanning'
: totalActive > 0
? 'processing'
: 'idle'
useEffect(() => {
if (phase === 'scanning') {
wasScanningRef.current = true
wasProcessingRef.current = false
} else if (phase === 'processing') {
wasProcessingRef.current = true
if (wasScanningRef.current) {
wasScanningRef.current = false
queryClient.invalidateQueries({ queryKey: ['photos'] })
queryClient.invalidateQueries({ queryKey: ['folders'] })
queryClient.invalidateQueries({ queryKey: ['folders', 'tree'] })
queryClient.invalidateQueries({ queryKey: ['heaps'] })
queryClient.invalidateQueries({ queryKey: ['tags'] })
queryClient.invalidateQueries({ queryKey: ['library', 'stats'] })
}
} else if (phase === 'idle') {
if (wasScanningRef.current) {
wasScanningRef.current = false
queryClient.invalidateQueries({ queryKey: ['photos'] })
queryClient.invalidateQueries({ queryKey: ['folders'] })
queryClient.invalidateQueries({ queryKey: ['folders', 'tree'] })
queryClient.invalidateQueries({ queryKey: ['heaps'] })
queryClient.invalidateQueries({ queryKey: ['tags'] })
queryClient.invalidateQueries({ queryKey: ['library', 'stats'] })
}
if (wasProcessingRef.current) {
wasProcessingRef.current = false
queryClient.invalidateQueries({ queryKey: ['tags'] })
queryClient.invalidateQueries({ queryKey: ['photos'] })
queryClient.invalidateQueries({ queryKey: ['library', 'stats'] })
}
}
}, [phase, queryClient])
return null
}
function totalQueued(ws: WorkerStatus | undefined): number {
if (!ws) return 0
const queued = Object.values(ws.queues ?? {}).reduce((a, b) => a + b, 0)
const active = ws.workers?.reduce(
(sum, w) => sum + (w.active ?? 0) + (w.reserved ?? 0), 0
) ?? 0
return queued + active
}