Compare commits

...

2 Commits

Author SHA1 Message Date
cb33268dcc fix(MediaEntry): add support for comic file types in EntryIcon 2026-06-13 02:34:02 -04:00
421a667548 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
2026-06-13 00:04:00 -04:00
4 changed files with 42 additions and 9 deletions

View File

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

View File

@ -1,4 +1,5 @@
import { import {
Album,
BookOpen, BookOpen,
Braces, Braces,
CassetteTape, CassetteTape,
@ -12,6 +13,7 @@ import {
Folder, Folder,
HardDrive, HardDrive,
Image as ImageIcon, Image as ImageIcon,
BookImage,
Layers, Layers,
MoreVertical, MoreVertical,
Music, Music,
@ -39,6 +41,7 @@ export const DISK_EXTS = new Set(['c64', 'd41', 'd64', 'd67', 'd71', 'd80', '
export const DISC_EXTS = new Set(['iso', 'img', 'cue']); export const DISC_EXTS = new Set(['iso', 'img', 'cue']);
export const HD_EXTS = new Set(['d1m', 'd2m', 'd4m', 'd90', 'dhd', 'hdd', 'bbt', 'd8b', 'dfi']); export const HD_EXTS = new Set(['d1m', 'd2m', 'd4m', 'd90', 'dhd', 'hdd', 'bbt', 'd8b', 'dfi']);
export const ARCHIVE_EXTS = new Set(['zip', '7z', 'tar', 'gz', 'bz2', 'xz', 'rar', 'arj', 'lzh', 'ace', 'z', 'lha', 'cab', 'lbr', 'arc', 'ark', 'lnx']); export const ARCHIVE_EXTS = new Set(['zip', '7z', 'tar', 'gz', 'bz2', 'xz', 'rar', 'arj', 'lzh', 'ace', 'z', 'lha', 'cab', 'lbr', 'arc', 'ark', 'lnx']);
export const COMIC_EXTS = new Set(['cbz', 'cbr', 'cb7', 'cbt', 'cbz']);
export const CONFIG_EXTS = new Set(['config']); export const CONFIG_EXTS = new Set(['config']);
// ─── EntryIcon ──────────────────────────────────────────────────────────────── // ─── EntryIcon ────────────────────────────────────────────────────────────────
@ -62,6 +65,7 @@ export function EntryIcon({ entry }: { entry: EntryInfo }) {
if (DOC_EXTS.has(ext)) return <FileType className="w-5 h-5 text-blue-400 flex-shrink-0" />; if (DOC_EXTS.has(ext)) return <FileType className="w-5 h-5 text-blue-400 flex-shrink-0" />;
if (CODE_EXTS.has(ext)) return <Terminal className="w-5 h-5 text-green-600 flex-shrink-0" />; if (CODE_EXTS.has(ext)) return <Terminal className="w-5 h-5 text-green-600 flex-shrink-0" />;
if (TEXT_EXTS.has(ext)) return <FileText className="w-5 h-5 text-green-600 flex-shrink-0" />; if (TEXT_EXTS.has(ext)) return <FileText className="w-5 h-5 text-green-600 flex-shrink-0" />;
if (COMIC_EXTS.has(ext)) return <BookImage className="w-5 h-5 text-pink-500 flex-shrink-0" />;
return <File className="w-5 h-5 text-neutral-400 flex-shrink-0" />; return <File className="w-5 h-5 text-neutral-400 flex-shrink-0" />;
} }

View File

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

View File

@ -171,20 +171,44 @@ function toEntryInfo(e: WebDAVEntry, baseUrl: string): EntryInfo {
* return all descendants flattened requires the server to support it * return all descendants flattened requires the server to support it
* (webdav3.py does after the depth-infinity fix). * (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 manager = getWebDAVClient();
const base = manager.client.baseUrl; const base = manager.client.baseUrl;
const collectionUrl = pathToUrl(normalizePath(path), base); const collectionUrl = pathToUrl(normalizePath(path), base);
const selfPath = normalizePath(path); 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
const depth: any = recursive ? 'infinity' : 1; 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[] = []; const entries: EntryInfo[] = [];
for (const node of Array.from(doc.querySelectorAll('response'))) { for (const node of Array.from(doc.querySelectorAll('response'))) {