fix(MediaManager): display loading progress in MediaManager while fetching directory contents

fix(webdav): enhance listDirectory to report progress during directory listing
fix(package): add @types/react-dom dependency for improved TypeScript support
This commit is contained in:
Jaime Idolpx 2026-06-13 00:04:00 -04:00
parent c7048678b2
commit 421a667548
3 changed files with 38 additions and 9 deletions

View File

@ -87,6 +87,7 @@
},
"devDependencies": {
"@tailwindcss/vite": "4.1.12",
"@types/react-dom": "^19.2.3",
"@types/three": "^0.160.0",
"@vitejs/plugin-react": "4.7.0",
"tailwindcss": "4.1.12",

View File

@ -1,4 +1,5 @@
import { lazy, Suspense, useCallback, useEffect, useRef, useState } from 'react';
import { flushSync } from 'react-dom';
import {
AlignLeft,
ArrowLeft,
@ -459,14 +460,16 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
// ── Directory loading ────────────────────────────────────────────────────
const [folderConfig, setFolderConfig] = useState<Record<string, string> | null>(null);
const [loadedCount, setLoadedCount] = useState<number | null>(null);
const load = useCallback(async (p: string) => {
setLoading(true);
setError(null);
setSelected(new Set());
setFolderConfig(null);
setLoadedCount(null);
try {
const entries = await listDirectory(p);
const entries = await listDirectory(p, false, bytes => flushSync(() => setLoadedCount(bytes)));
setEntries(entries);
try {
const blob = await getFileContents(joinPath(p, '.config'));
@ -1179,7 +1182,8 @@ export default function MediaManager({ initialPath, rootPath, title, config, set
<div className="flex-1 overflow-y-auto">
{loading && (
<div className="p-8 flex flex-col items-center gap-2 text-neutral-500 text-sm">
<Loader2 className="w-6 h-6 animate-spin" /> Loading
<Loader2 className="w-6 h-6 animate-spin" />
{loadedCount === null ? 'Loading…' : humanFileSize(loadedCount)}
</div>
)}

View File

@ -171,20 +171,44 @@ function toEntryInfo(e: WebDAVEntry, baseUrl: string): EntryInfo {
* return all descendants flattened requires the server to support it
* (webdav3.py does after the depth-infinity fix).
*/
export async function listDirectory(path: string, recursive = false): Promise<EntryInfo[]> {
export async function listDirectory(
path: string,
recursive = false,
onProgress?: (bytes: number) => void,
): Promise<EntryInfo[]> {
const manager = getWebDAVClient();
const base = manager.client.baseUrl;
const collectionUrl = pathToUrl(normalizePath(path), base);
const selfPath = normalizePath(path);
// Parse the PROPFIND response directly rather than going through
// parsePropfindListing, which keys entries by display name and would
// silently drop any entries that share the same filename. Parsing
// the XML ourselves also avoids mutating the shared WebDAVManager
// navigation state (navToken, this.files).
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const depth: any = recursive ? 'infinity' : 1;
const doc = await manager.client.propfind(collectionUrl, PROPFIND_LIST_BODY, depth);
let doc: Document;
if (onProgress) {
const response = await fetch(collectionUrl, {
method: 'PROPFIND',
headers: { 'Depth': String(depth), 'Content-Type': 'text/xml; charset=utf-8' },
body: PROPFIND_LIST_BODY,
});
if (!response.body) throw new Error('No response body');
const reader = response.body.getReader();
const chunks: Uint8Array[] = [];
let received = 0;
for (;;) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
received += value.byteLength;
onProgress(received);
}
const combined = new Uint8Array(received);
let offset = 0;
for (const chunk of chunks) { combined.set(chunk, offset); offset += chunk.byteLength; }
doc = new DOMParser().parseFromString(new TextDecoder().decode(combined), 'text/xml');
} else {
doc = await manager.client.propfind(collectionUrl, PROPFIND_LIST_BODY, depth);
}
const entries: EntryInfo[] = [];
for (const node of Array.from(doc.querySelectorAll('response'))) {