feat(locate-db, SearchLocal): enhance scan progress reporting with directory and file counts
This commit is contained in:
parent
ba8de6634e
commit
ecc54c1fc9
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user