fix(SearchLocal, locate-db): enhance database download progress tracking and UI feedback
This commit is contained in:
parent
3b94d3d956
commit
9d7034e7d2
|
|
@ -175,7 +175,7 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
const [mountEntry, setMountEntry] = useState<SearchResult | null>(null);
|
const [mountEntry, setMountEntry] = useState<SearchResult | null>(null);
|
||||||
const [actionEntry, setActionEntry] = useState<SearchResult | null>(null);
|
const [actionEntry, setActionEntry] = useState<SearchResult | null>(null);
|
||||||
const [searchError, setSearchError] = useState<string | null>(null);
|
const [searchError, setSearchError] = useState<string | null>(null);
|
||||||
const [dbBytes, setDbBytes] = useState<number | null>(null);
|
const [dbProgress, setDbProgress] = useState<{ received: number; total: number | null }>({ received: 0, total: null });
|
||||||
const [dbPhase, setDbPhase] = useState<'idle' | 'downloading' | 'ready'>('idle');
|
const [dbPhase, setDbPhase] = useState<'idle' | 'downloading' | 'ready'>('idle');
|
||||||
const [showFilter, setShowFilter] = useState(() => _store.showFilter);
|
const [showFilter, setShowFilter] = useState(() => _store.showFilter);
|
||||||
const [filterSystem, setFilterSystem] = useState<string | null>(() => _store.filterSystem);
|
const [filterSystem, setFilterSystem] = useState<string | null>(() => _store.filterSystem);
|
||||||
|
|
@ -229,8 +229,8 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
try {
|
try {
|
||||||
if (!isLocateDbLoaded()) {
|
if (!isLocateDbLoaded()) {
|
||||||
setDbPhase('downloading');
|
setDbPhase('downloading');
|
||||||
setDbBytes(null);
|
setDbProgress({ received: 0, total: null });
|
||||||
await openLocateDb(bytes => flushSync(() => setDbBytes(bytes)));
|
await openLocateDb(p => flushSync(() => setDbProgress(p)));
|
||||||
setDbPhase('ready');
|
setDbPhase('ready');
|
||||||
}
|
}
|
||||||
const needle = query.trim();
|
const needle = query.trim();
|
||||||
|
|
@ -297,9 +297,17 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadingLabel = dbPhase === 'downloading'
|
const loadingLabel = dbPhase === 'downloading'
|
||||||
? (dbBytes === null ? 'Loading database…' : `Loading database… ${humanFileSize(dbBytes)}`)
|
? (dbProgress.received === 0
|
||||||
|
? 'Loading database…'
|
||||||
|
: dbProgress.total === null
|
||||||
|
? `Loading database… ${humanFileSize(dbProgress.received)}`
|
||||||
|
: `Loading database… ${humanFileSize(dbProgress.received)} / ${humanFileSize(dbProgress.total)}`)
|
||||||
: 'Searching…';
|
: 'Searching…';
|
||||||
|
|
||||||
|
const downloadPct = dbProgress.total && dbProgress.total > 0
|
||||||
|
? Math.min(100, Math.round((dbProgress.received / dbProgress.total) * 100))
|
||||||
|
: null;
|
||||||
|
|
||||||
const busy = isSearching || isScanning;
|
const busy = isSearching || isScanning;
|
||||||
|
|
||||||
const facets = useMemo(() => {
|
const facets = useMemo(() => {
|
||||||
|
|
@ -439,9 +447,22 @@ export default function SearchLocal({ config, setConfig, onClose, onOpenFolder }
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isSearching && !hasSearched && (
|
{isSearching && !hasSearched && (
|
||||||
<div className="flex flex-col items-center justify-center py-16 gap-3">
|
<div className="flex flex-col items-center justify-center py-16 gap-3 w-full px-8">
|
||||||
<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 text-neutral-500">{loadingLabel}</p>
|
<p className="text-sm text-neutral-500 text-center">{loadingLabel}</p>
|
||||||
|
{dbPhase === 'downloading' && (
|
||||||
|
<div className="w-full max-w-xs">
|
||||||
|
<div className="h-1.5 bg-neutral-200 rounded-full overflow-hidden">
|
||||||
|
<div
|
||||||
|
className={`h-full bg-blue-500 transition-all duration-150 ease-out ${downloadPct === null ? 'animate-pulse w-1/3' : ''}`}
|
||||||
|
style={downloadPct !== null ? { width: `${downloadPct}%` } : undefined}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
{downloadPct !== null && (
|
||||||
|
<p className="text-xs text-neutral-400 text-center mt-1.5">{downloadPct}%</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,12 @@ import { getWebDAVBaseUrl, basename, listDirectory, putFileContents, deletePath
|
||||||
|
|
||||||
const LOCATE_PATH = '/sd/.locate';
|
const LOCATE_PATH = '/sd/.locate';
|
||||||
|
|
||||||
|
function parseContentLength(header: string | null): number | null {
|
||||||
|
if (!header) return null;
|
||||||
|
const n = Number(header);
|
||||||
|
return Number.isFinite(n) && n > 0 ? n : null;
|
||||||
|
}
|
||||||
|
|
||||||
// Memoize the module init — loading the WASM binary is expensive.
|
// Memoize the module init — loading the WASM binary is expensive.
|
||||||
let _sqlite3Promise: Promise<any> | null = null;
|
let _sqlite3Promise: Promise<any> | null = null;
|
||||||
function getSqlite3(): Promise<any> {
|
function getSqlite3(): Promise<any> {
|
||||||
|
|
@ -26,9 +32,13 @@ export function resetLocateDb(): void {
|
||||||
/**
|
/**
|
||||||
* Fetch /sd/.locate and open it as an in-memory SQLite database.
|
* Fetch /sd/.locate and open it as an in-memory SQLite database.
|
||||||
* Calling again when already loaded is a no-op unless you call resetLocateDb() first.
|
* Calling again when already loaded is a no-op unless you call resetLocateDb() first.
|
||||||
* onProgress receives raw bytes received so far during the download.
|
* onProgress receives { received, total } for each chunk; total is the value
|
||||||
|
* of the Content-Length response header, or null if the server didn't send
|
||||||
|
* one (chunked transfer, etc.).
|
||||||
*/
|
*/
|
||||||
export async function openLocateDb(onProgress?: (bytes: number) => void): Promise<void> {
|
export async function openLocateDb(
|
||||||
|
onProgress?: (progress: { received: number; total: number | null }) => void,
|
||||||
|
): Promise<void> {
|
||||||
if (_db) return;
|
if (_db) return;
|
||||||
|
|
||||||
const sqlite3 = await getSqlite3();
|
const sqlite3 = await getSqlite3();
|
||||||
|
|
@ -37,6 +47,8 @@ export async function openLocateDb(onProgress?: (bytes: number) => void): Promis
|
||||||
const response = await fetch(url);
|
const response = await fetch(url);
|
||||||
if (!response.ok) throw new Error(`Cannot fetch locate database: ${response.status} ${response.statusText}`);
|
if (!response.ok) throw new Error(`Cannot fetch locate database: ${response.status} ${response.statusText}`);
|
||||||
|
|
||||||
|
const total = parseContentLength(response.headers.get('content-length'));
|
||||||
|
|
||||||
// Stream the response body so onProgress gets called per chunk.
|
// Stream the response body so onProgress gets called per chunk.
|
||||||
let bytes: Uint8Array;
|
let bytes: Uint8Array;
|
||||||
if (response.body) {
|
if (response.body) {
|
||||||
|
|
@ -48,7 +60,7 @@ export async function openLocateDb(onProgress?: (bytes: number) => void): Promis
|
||||||
if (done) break;
|
if (done) break;
|
||||||
chunks.push(value);
|
chunks.push(value);
|
||||||
received += value.byteLength;
|
received += value.byteLength;
|
||||||
onProgress?.(received);
|
onProgress?.({ received, total });
|
||||||
}
|
}
|
||||||
const combined = new Uint8Array(received);
|
const combined = new Uint8Array(received);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|
@ -56,6 +68,7 @@ export async function openLocateDb(onProgress?: (bytes: number) => void): Promis
|
||||||
bytes = combined;
|
bytes = combined;
|
||||||
} else {
|
} else {
|
||||||
bytes = new Uint8Array(await response.arrayBuffer());
|
bytes = new Uint8Array(await response.arrayBuffer());
|
||||||
|
onProgress?.({ received: bytes.byteLength, total: total ?? bytes.byteLength });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allocate the bytes in WASM heap, then deserialize into a fresh in-memory DB.
|
// Allocate the bytes in WASM heap, then deserialize into a fresh in-memory DB.
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user