Compare commits
No commits in common. "cb33268dcc3cb916193bae99a863c88c1c0c75d3" and "c7048678b2682c2a797cea4f835354de2fbd610a" have entirely different histories.
cb33268dcc
...
c7048678b2
|
|
@ -87,7 +87,6 @@
|
||||||
},
|
},
|
||||||
"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",
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
import {
|
import {
|
||||||
Album,
|
|
||||||
BookOpen,
|
BookOpen,
|
||||||
Braces,
|
Braces,
|
||||||
CassetteTape,
|
CassetteTape,
|
||||||
|
|
@ -13,7 +12,6 @@ import {
|
||||||
Folder,
|
Folder,
|
||||||
HardDrive,
|
HardDrive,
|
||||||
Image as ImageIcon,
|
Image as ImageIcon,
|
||||||
BookImage,
|
|
||||||
Layers,
|
Layers,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
Music,
|
Music,
|
||||||
|
|
@ -41,7 +39,6 @@ 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 ────────────────────────────────────────────────────────────────
|
||||||
|
|
@ -65,7 +62,6 @@ 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" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
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,
|
||||||
|
|
@ -460,16 +459,14 @@ 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, false, bytes => flushSync(() => setLoadedCount(bytes)));
|
const entries = await listDirectory(p);
|
||||||
setEntries(entries);
|
setEntries(entries);
|
||||||
try {
|
try {
|
||||||
const blob = await getFileContents(joinPath(p, '.config'));
|
const blob = await getFileContents(joinPath(p, '.config'));
|
||||||
|
|
@ -1182,8 +1179,7 @@ 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" />
|
<Loader2 className="w-6 h-6 animate-spin" /> Loading…
|
||||||
{loadedCount === null ? 'Loading…' : humanFileSize(loadedCount)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -171,44 +171,20 @@ 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(
|
export async function listDirectory(path: string, recursive = false): Promise<EntryInfo[]> {
|
||||||
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'))) {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user