import { useEffect, useRef, useState } from 'react'; import { __, sprintf } from '@wordpress/i18n'; import { Check, RefreshCw, Search } from 'lucide-react'; import { toast } from 'sonner'; import { cancelScan, getScanProgress, startScan, type ScanProgress } from '../../api'; import { dateFormat, numberFormat } from '../../lib/i18n'; const POLL_MS = 3000; /** * Owns the library scan: reads where it stands, polls while it runs, and starts or cancels it. * * A hook, not a panel, because the dashboard shows it in two places: on top while it needs attention * (never run, running) and as a quiet line at the bottom once it is complete. */ export function useScan({ onComplete, onState }: { onComplete?: () => void; onState?: (progress: ScanProgress) => void }) { const [progress, setProgress] = useState(null); const [busy, setBusy] = useState(false); const wasRunning = useRef(false); const read = async () => { try { const res: any = await getScanProgress(); if (res?.data) { setProgress(res.data); onState?.(res.data); if (wasRunning.current && !res.data.is_running) { onComplete?.(); } wasRunning.current = res.data.is_running; } } catch { // A failed poll is not worth a toast; the next one will tell the same story. } }; useEffect(() => { read(); // A card can start the scan; the panel owns the progress bar from then on. const onStarted = () => { wasRunning.current = true; read(); }; window.addEventListener('thumbpress:scan-started', onStarted); return () => window.removeEventListener('thumbpress:scan-started', onStarted); }, []); useEffect(() => { if (!progress?.is_running) { return; } const timer = setInterval(read, POLL_MS); return () => clearInterval(timer); }, [progress?.is_running]); const run = async () => { setBusy(true); try { const res: any = await startScan(); if (res?.data) { setProgress(res.data); onState?.(res.data); wasRunning.current = true; } toast.success(__('Scan started. You can leave this page — it runs in the background.', 'image-sizes')); } catch { toast.error(__('Could not start the scan.', 'image-sizes')); } finally { setBusy(false); } }; const stop = async () => { setBusy(true); try { const res: any = await cancelScan(); if (res?.data) { setProgress(res.data); onState?.(res.data); } toast.success(__('Scan cancelled.', 'image-sizes')); } catch { toast.error(__('Could not cancel the scan.', 'image-sizes')); } finally { setBusy(false); } }; return { progress, busy, run, stop }; } export type Scan = ReturnType; /** Scanning or never scanned: the person needs to see this before anything else. */ export function ScanBar({ scan, imageCount = 0 }: { scan: Scan; imageCount?: number }) { const { progress, busy, run, stop } = scan; if (!progress || (progress.is_ready && !progress.is_running)) { return null; } const linkClass = 'font-medium text-[13px] whitespace-nowrap text-thumbpress-primary bg-transparent border-0 p-0 cursor-pointer disabled:opacity-50'; const iconClass = 'w-6 h-6 rounded-full flex items-center justify-center shrink-0'; const running = progress.is_running; return (
{running ? : }
{running ? __('Scanning your library', 'image-sizes') : __('Library not scanned yet', 'image-sizes')} {running ? sprintf( /* translators: 1: images checked, 2: images in the library, 3: percent done. */ __('%1$s of %2$s images · %3$s%% · safe to leave this page', 'image-sizes'), numberFormat(progress.processed), numberFormat(progress.total), numberFormat(progress.percent), ) : sprintf( /* translators: %s: number of images in the library. */ __('%s images found. Grades and counts need one pass over your library. Nothing is scanned until you ask.', 'image-sizes'), numberFormat(imageCount), )}
{running && (
)}
); } /** A finished scan is a fact, not a task: one quiet line at the end of the page, with the way to run it again. */ export function ScanFooter({ scan }: { scan: Scan }) { const { progress, busy, run } = scan; if (!progress || !progress.is_ready || progress.is_running) { return null; } return (

); }