diff --git a/src/app/components/FileManager.tsx b/src/app/components/FileManager.tsx index 64df6c0..98ad9b8 100644 --- a/src/app/components/FileManager.tsx +++ b/src/app/components/FileManager.tsx @@ -43,6 +43,7 @@ import { createFolder, deletePath, getFileContents, + getFileRange, humanFileSize, joinPath, listDirectory, @@ -197,14 +198,16 @@ function MarkdownEditor({ text, onSave }: { text: string; onSave?: (s: string) = return (
- + {onSave && ( + + )} {editMode && onSave && ( - {viewEntry.name} - {/* Mode switcher */} -
- {viewMode && availableViewers(viewEntry).map(mode => ( - - ))} -
- -
+ {viewEntry && (() => { + const fileSize = viewEntry.size; + const isFullyLoaded = fileSize === 0 || fileSize <= bufferSize; + const windowEnd = fileSize > 0 ? Math.min(viewOffset + bufferSize, fileSize) : viewOffset + bufferSize; + const hasPrev = viewOffset > 0; + const hasNext = fileSize > 0 ? viewOffset + bufferSize < fileSize : false; -
- {viewLoading && ( -
- Loading… + return ( +
+ {/* Title + mode switcher */} +
+ + {viewEntry.name} +
+ {viewMode && availableViewers(viewEntry).map(mode => ( + + ))} +
+ +
+ + {/* Buffer navigation bar (hidden for images) */} + {viewMode !== 'image' && ( +
+ + + {fileSize > 0 + ? `${viewOffset.toLocaleString()} – ${windowEnd.toLocaleString()} of ${fileSize.toLocaleString()} bytes` + : `offset ${viewOffset.toLocaleString()}`} + + + Buffer +
)} - {!viewLoading && viewMode === 'image' && viewImgUrl && ( -
- {viewEntry.name} -
- )} - {!viewLoading && viewMode === 'hex' && viewHexData && ( - saveViewFile(d)} /> - )} - {!viewLoading && viewMode === 'markdown' && viewText !== null && ( - saveViewFile(s)} /> - )} - {!viewLoading && (viewMode === 'text' || viewMode === 'json' || viewMode === 'xml') && viewText !== null && ( - saveViewFile(s)} /> - )} + +
+ {viewLoading && ( +
+ Loading… +
+ )} + {!viewLoading && viewMode === 'image' && viewImgUrl && ( +
+ {viewEntry.name} +
+ )} + {!viewLoading && viewMode === 'hex' && viewHexData && ( + saveViewFile(d) : undefined} /> + )} + {!viewLoading && viewMode === 'markdown' && viewText !== null && ( + saveViewFile(s) : undefined} /> + )} + {!viewLoading && (viewMode === 'text' || viewMode === 'json' || viewMode === 'xml') && viewText !== null && ( + saveViewFile(s) : undefined} /> + )} +
-
- )} + ); + })()} {/* ── Mount dialog ── */} !open && setMountEntry(null)}> diff --git a/src/app/webdav.ts b/src/app/webdav.ts index 4f1b58c..e2263a3 100644 --- a/src/app/webdav.ts +++ b/src/app/webdav.ts @@ -317,6 +317,19 @@ export async function getFileContents(path: string): Promise { return r.blob(); } +/** + * Fetch a byte range of a file using HTTP Range requests. + * Returns a Blob containing the requested bytes. Falls back to a full + * fetch if the server responds 200 (no Range support). + */ +export async function getFileRange(path: string, start: number, end: number): Promise { + const base = getWebDAVClient().client.baseUrl; + const url = pathToUrl(normalizePath(path), base); + const r = await fetch(url, { headers: { Range: `bytes=${start}-${end}` } }); + if (r.status === 206 || r.status === 200) return r.blob(); + throw new Error(`GET range failed: ${r.status} ${r.statusText}`); +} + // ----- Helpers -------------------------------------------------------- /** Convert a server-relative path to an absolute URL on the configured base. */