feat(locate-db, SearchLocal): enhance scan progress reporting with directory and file counts

This commit is contained in:
Jaime Idolpx 2026-06-17 14:29:29 -04:00
parent ba8de6634e
commit ecc54c1fc9
2 changed files with 44 additions and 9 deletions

View File

@ -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 <span className="text-xs px-1.5 py-0.5 rounded bg-neutral-100 text-neutral-600 font-mono">{type}</span>;
}
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<HTMLDivElement>(null);
const scanAbortRef = useRef<AbortController | null>(null);
const scanAbortRef = useRef<AbortController | null>(null);
const scanStartRef = useRef<number>(0);
const [query, setQuery] = useState(() => _store.query);
const [isSearching, setIsSearching] = useState(false);
const [isScanning, setIsScanning] = useState(false);
const [scanPhase, setScanPhase] = useState<ScanPhase | null>(null);
const [scanValue, setScanValue] = useState(0);
const [scanPath, setScanPath] = useState<string | null>(null);
const [scanPath, setScanPath] = useState<string | null>(null);
const [scanCounts, setScanCounts] = useState<ScanCounts | null>(null);
const [scanElapsed, setScanElapsed] = useState(0);
const [results, setResults] = useState<SearchResult[]>(() => _store.results);
const [hasSearched, setHasSearched] = useState(() => _store.hasSearched);
const [mountEntry, setMountEntry] = useState<SearchResult | null>(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 }
<div className="flex flex-col items-center justify-center py-16 gap-3 px-6">
<Loader2 className="w-8 h-8 text-blue-500 animate-spin" />
<p className="text-sm font-medium text-neutral-700">{scanPhaseLabel(scanPhase, scanValue)}</p>
{scanCounts && (
<p className="text-xs text-neutral-500 tabular-nums">
{scanCounts.dirs.toLocaleString()} folders · {scanCounts.files.toLocaleString()} files
{scanElapsed > 0 && <span className="text-neutral-400"> · {formatDuration(scanElapsed)}</span>}
</p>
)}
{scanPath ? (
<p
className="text-xs text-neutral-400 w-full text-center truncate"
@ -620,7 +650,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
</div>
)}
{!isSearching && !hasSearched && (
{!isSearching && !isScanning && !hasSearched && (
<div className="py-16 text-center px-6">
<Search className="w-10 h-10 mx-auto mb-3 text-neutral-300" />
<p className="text-sm font-medium text-neutral-600 mb-1">Search your device</p>

View File

@ -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<ReturnType<typeof listDirectory>> = [];
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<void>(resolve => setTimeout(resolve, 0));
}
}