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,
|
subscribeLoadProgress,
|
||||||
type LocateEntry,
|
type LocateEntry,
|
||||||
type ScanPhase,
|
type ScanPhase,
|
||||||
|
type ScanCounts,
|
||||||
} from '../locate-db';
|
} from '../locate-db';
|
||||||
|
|
||||||
interface SearchLocalProps {
|
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>;
|
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 {
|
function scanPhaseLabel(phase: ScanPhase, value: number): string {
|
||||||
if (phase === 'scanning') return `Scanning… ${humanFileSize(value)}`;
|
if (phase === 'scanning') return `Scanning… ${humanFileSize(value)}`;
|
||||||
if (phase === 'building') return `Building… ${value.toLocaleString()} entries`;
|
if (phase === 'building') return `Building… ${value.toLocaleString()} entries`;
|
||||||
|
|
@ -168,13 +178,16 @@ function FilterChips({
|
||||||
|
|
||||||
export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }: SearchLocalProps) {
|
export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }: SearchLocalProps) {
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
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 [query, setQuery] = useState(() => _store.query);
|
||||||
const [isSearching, setIsSearching] = useState(false);
|
const [isSearching, setIsSearching] = useState(false);
|
||||||
const [isScanning, setIsScanning] = useState(false);
|
const [isScanning, setIsScanning] = useState(false);
|
||||||
const [scanPhase, setScanPhase] = useState<ScanPhase | null>(null);
|
const [scanPhase, setScanPhase] = useState<ScanPhase | null>(null);
|
||||||
const [scanValue, setScanValue] = useState(0);
|
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 [results, setResults] = useState<SearchResult[]>(() => _store.results);
|
||||||
const [hasSearched, setHasSearched] = useState(() => _store.hasSearched);
|
const [hasSearched, setHasSearched] = useState(() => _store.hasSearched);
|
||||||
const [mountEntry, setMountEntry] = useState<SearchResult | null>(null);
|
const [mountEntry, setMountEntry] = useState<SearchResult | null>(null);
|
||||||
|
|
@ -301,13 +314,22 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
const handleScan = async () => {
|
const handleScan = async () => {
|
||||||
const controller = new AbortController();
|
const controller = new AbortController();
|
||||||
scanAbortRef.current = controller;
|
scanAbortRef.current = controller;
|
||||||
|
scanStartRef.current = Date.now();
|
||||||
setIsScanning(true);
|
setIsScanning(true);
|
||||||
setScanPhase('scanning');
|
setScanPhase('scanning');
|
||||||
setScanValue(0);
|
setScanValue(0);
|
||||||
|
setScanCounts(null);
|
||||||
|
setScanElapsed(0);
|
||||||
try {
|
try {
|
||||||
const { count, bytes } = await buildLocateDb(
|
const { count, bytes } = await buildLocateDb(
|
||||||
(phase, value, path) =>
|
(phase, value, path, counts) =>
|
||||||
flushSync(() => { setScanPhase(phase); setScanValue(value); if (path !== undefined) setScanPath(path); }),
|
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,
|
controller.signal,
|
||||||
);
|
);
|
||||||
toast.success(`Indexed ${count.toLocaleString()} items · ${humanFileSize(bytes)}`);
|
toast.success(`Indexed ${count.toLocaleString()} items · ${humanFileSize(bytes)}`);
|
||||||
|
|
@ -321,6 +343,8 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
setIsScanning(false);
|
setIsScanning(false);
|
||||||
setScanPhase(null);
|
setScanPhase(null);
|
||||||
setScanPath(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">
|
<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" />
|
<Loader2 className="w-8 h-8 text-blue-500 animate-spin" />
|
||||||
<p className="text-sm font-medium text-neutral-700">{scanPhaseLabel(scanPhase, scanValue)}</p>
|
<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 ? (
|
{scanPath ? (
|
||||||
<p
|
<p
|
||||||
className="text-xs text-neutral-400 w-full text-center truncate"
|
className="text-xs text-neutral-400 w-full text-center truncate"
|
||||||
|
|
@ -620,7 +650,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isSearching && !hasSearched && (
|
{!isSearching && !isScanning && !hasSearched && (
|
||||||
<div className="py-16 text-center px-6">
|
<div className="py-16 text-center px-6">
|
||||||
<Search className="w-10 h-10 mx-auto mb-3 text-neutral-300" />
|
<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>
|
<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
|
* building → number of entries inserted so far
|
||||||
* saving → bytes of the serialized DB written to the server
|
* saving → bytes of the serialized DB written to the server
|
||||||
*/
|
*/
|
||||||
|
export type ScanCounts = { dirs: number; files: number };
|
||||||
|
|
||||||
export async function buildLocateDb(
|
export async function buildLocateDb(
|
||||||
onProgress?: (phase: ScanPhase, value: number, path?: string) => void,
|
onProgress?: (phase: ScanPhase, value: number, path?: string, counts?: ScanCounts) => void,
|
||||||
signal?: AbortSignal,
|
signal?: AbortSignal,
|
||||||
): Promise<{ count: number; bytes: number }> {
|
): Promise<{ count: number; bytes: number }> {
|
||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
|
|
@ -186,6 +188,8 @@ export async function buildLocateDb(
|
||||||
const entries: Awaited<ReturnType<typeof listDirectory>> = [];
|
const entries: Awaited<ReturnType<typeof listDirectory>> = [];
|
||||||
const queue: string[] = ['/sd'];
|
const queue: string[] = ['/sd'];
|
||||||
let totalBytes = 0;
|
let totalBytes = 0;
|
||||||
|
let dirCount = 0;
|
||||||
|
let fileCount = 0;
|
||||||
|
|
||||||
while (queue.length > 0) {
|
while (queue.length > 0) {
|
||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
|
|
@ -194,7 +198,7 @@ export async function buildLocateDb(
|
||||||
const batch = await listDirectory(dir, false, bytes => {
|
const batch = await listDirectory(dir, false, bytes => {
|
||||||
totalBytes += bytes - prevBytes;
|
totalBytes += bytes - prevBytes;
|
||||||
prevBytes = bytes;
|
prevBytes = bytes;
|
||||||
onProgress?.('scanning', totalBytes, dir);
|
onProgress?.('scanning', totalBytes, dir, { dirs: dirCount, files: fileCount });
|
||||||
}, signal);
|
}, signal);
|
||||||
|
|
||||||
// If this directory has a .config with a URL-scheme base_url it points to
|
// 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) {
|
for (const e of batch) {
|
||||||
entries.push(e);
|
entries.push(e);
|
||||||
if (e.type === 'folder') queue.push(e.path);
|
if (e.type === 'folder') { dirCount++; queue.push(e.path); }
|
||||||
|
else fileCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
|
|
@ -246,7 +251,7 @@ export async function buildLocateDb(
|
||||||
]).stepReset();
|
]).stepReset();
|
||||||
if (i % SCAN_PROGRESS_INTERVAL === 0) {
|
if (i % SCAN_PROGRESS_INTERVAL === 0) {
|
||||||
signal?.throwIfAborted();
|
signal?.throwIfAborted();
|
||||||
onProgress?.('building', i, e.path);
|
onProgress?.('building', i, e.path, { dirs: dirCount, files: fileCount });
|
||||||
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
await new Promise<void>(resolve => setTimeout(resolve, 0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user