diff --git a/src/app/components/SearchLocal.tsx b/src/app/components/SearchLocal.tsx index 058d713..e5a3f0f 100644 --- a/src/app/components/SearchLocal.tsx +++ b/src/app/components/SearchLocal.tsx @@ -16,6 +16,7 @@ import { subscribeLoadProgress, type LocateEntry, type ScanPhase, + type ScanCounts, } from '../locate-db'; interface SearchLocalProps { @@ -117,6 +118,15 @@ function TypeBadge({ type, isDir }: { type: string; isDir: boolean }) { return {type}; } +function formatDuration(seconds: number): string { + const h = Math.floor(seconds / 3600); + const m = Math.floor((seconds % 3600) / 60); + const s = seconds % 60; + if (h > 0) return `${h}h ${m}m ${s}s`; + if (m > 0) return `${m}m ${s}s`; + return `${s}s`; +} + function scanPhaseLabel(phase: ScanPhase, value: number): string { if (phase === 'scanning') return `Scanning… ${humanFileSize(value)}`; if (phase === 'building') return `Building… ${value.toLocaleString()} entries`; @@ -168,13 +178,16 @@ function FilterChips({ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }: SearchLocalProps) { const scrollRef = useRef(null); - const scanAbortRef = useRef(null); + const scanAbortRef = useRef(null); + const scanStartRef = useRef(0); const [query, setQuery] = useState(() => _store.query); const [isSearching, setIsSearching] = useState(false); const [isScanning, setIsScanning] = useState(false); const [scanPhase, setScanPhase] = useState(null); const [scanValue, setScanValue] = useState(0); - const [scanPath, setScanPath] = useState(null); + const [scanPath, setScanPath] = useState(null); + const [scanCounts, setScanCounts] = useState(null); + const [scanElapsed, setScanElapsed] = useState(0); const [results, setResults] = useState(() => _store.results); const [hasSearched, setHasSearched] = useState(() => _store.hasSearched); const [mountEntry, setMountEntry] = useState(null); @@ -301,13 +314,22 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder } const handleScan = async () => { const controller = new AbortController(); scanAbortRef.current = controller; + scanStartRef.current = Date.now(); setIsScanning(true); setScanPhase('scanning'); setScanValue(0); + setScanCounts(null); + setScanElapsed(0); try { const { count, bytes } = await buildLocateDb( - (phase, value, path) => - flushSync(() => { setScanPhase(phase); setScanValue(value); if (path !== undefined) setScanPath(path); }), + (phase, value, path, counts) => + flushSync(() => { + setScanPhase(phase); + setScanValue(value); + if (path !== undefined) setScanPath(path); + if (counts !== undefined) setScanCounts(counts); + setScanElapsed(Math.floor((Date.now() - scanStartRef.current) / 1000)); + }), controller.signal, ); toast.success(`Indexed ${count.toLocaleString()} items · ${humanFileSize(bytes)}`); @@ -321,6 +343,8 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder } setIsScanning(false); setScanPhase(null); setScanPath(null); + setScanCounts(null); + setScanElapsed(0); } }; @@ -535,6 +559,12 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }

{scanPhaseLabel(scanPhase, scanValue)}

+ {scanCounts && ( +

+ {scanCounts.dirs.toLocaleString()} folders · {scanCounts.files.toLocaleString()} files + {scanElapsed > 0 && · {formatDuration(scanElapsed)}} +

+ )} {scanPath ? (

)} - {!isSearching && !hasSearched && ( + {!isSearching && !isScanning && !hasSearched && (

Search your device

diff --git a/src/app/locate-db.ts b/src/app/locate-db.ts index bc91136..35c1c09 100644 --- a/src/app/locate-db.ts +++ b/src/app/locate-db.ts @@ -173,8 +173,10 @@ export type ScanPhase = 'scanning' | 'building' | 'saving' | 'compressing'; * building → number of entries inserted so far * saving → bytes of the serialized DB written to the server */ +export type ScanCounts = { dirs: number; files: number }; + export async function buildLocateDb( - onProgress?: (phase: ScanPhase, value: number, path?: string) => void, + onProgress?: (phase: ScanPhase, value: number, path?: string, counts?: ScanCounts) => void, signal?: AbortSignal, ): Promise<{ count: number; bytes: number }> { signal?.throwIfAborted(); @@ -186,6 +188,8 @@ export async function buildLocateDb( const entries: Awaited> = []; const queue: string[] = ['/sd']; let totalBytes = 0; + let dirCount = 0; + let fileCount = 0; while (queue.length > 0) { signal?.throwIfAborted(); @@ -194,7 +198,7 @@ export async function buildLocateDb( const batch = await listDirectory(dir, false, bytes => { totalBytes += bytes - prevBytes; prevBytes = bytes; - onProgress?.('scanning', totalBytes, dir); + onProgress?.('scanning', totalBytes, dir, { dirs: dirCount, files: fileCount }); }, signal); // If this directory has a .config with a URL-scheme base_url it points to @@ -216,7 +220,8 @@ export async function buildLocateDb( for (const e of batch) { entries.push(e); - if (e.type === 'folder') queue.push(e.path); + if (e.type === 'folder') { dirCount++; queue.push(e.path); } + else fileCount++; } } signal?.throwIfAborted(); @@ -246,7 +251,7 @@ export async function buildLocateDb( ]).stepReset(); if (i % SCAN_PROGRESS_INTERVAL === 0) { signal?.throwIfAborted(); - onProgress?.('building', i, e.path); + onProgress?.('building', i, e.path, { dirs: dirCount, files: fileCount }); await new Promise(resolve => setTimeout(resolve, 0)); } }