Mercurial
diff hg-web/src/components/directory-browser.tsx @ 224:3007ef5fc0ed hg-web
[hg-web] Simplify visuals and preview static files
| author | MrJuneJune <me@mrjunejune.com> |
|---|---|
| date | Sun, 02 Aug 2026 13:51:11 -0700 |
| parents | 0e7b9464248d |
| children | 70de0c80d093 |
line wrap: on
line diff
--- a/hg-web/src/components/directory-browser.tsx Sun Aug 02 10:07:40 2026 -0700 +++ b/hg-web/src/components/directory-browser.tsx Sun Aug 02 13:51:11 2026 -0700 @@ -22,6 +22,14 @@ 'gradle', 'pom', 'lock', 'gitignore', 'env', 'example', 'sample' ]); +type StaticPreviewKind = 'image' | 'video' | 'audio' | 'pdf'; + +const IMAGE_EXTENSIONS = new Set([ + 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'bmp', 'ico', 'svg' +]); +const VIDEO_EXTENSIONS = new Set(['mp4', 'm4v', 'webm', 'mov', 'ogv']); +const AUDIO_EXTENSIONS = new Set(['mp3', 'wav', 'ogg', 'oga', 'flac', 'm4a', 'aac']); + // Prefetch cache const prefetchCache = new Map<string, Promise<any>>(); @@ -40,6 +48,15 @@ return ext === 'md' || ext === 'markdown'; } +function getStaticPreviewKind(filename: string): StaticPreviewKind | null { + const ext = filename.split('.').pop()?.toLowerCase() || ''; + if (IMAGE_EXTENSIONS.has(ext)) return 'image'; + if (VIDEO_EXTENSIONS.has(ext)) return 'video'; + if (AUDIO_EXTENSIONS.has(ext)) return 'audio'; + if (ext === 'pdf') return 'pdf'; + return null; +} + function prefetchDirectory(path: string): void { const cacheKey = `dir:${path}`; if (prefetchCache.has(cacheKey)) return; @@ -276,6 +293,81 @@ ); } +function StaticFileViewer({ filePath, onClose }: { filePath: string; onClose: () => void }) { + const filename = filePath.split('/').pop() || filePath; + const previewKind = getStaticPreviewKind(filename); + const fileUrl = `${API_BASE}/file?path=${encodeURIComponent(filePath)}`; + const [failed, setFailed] = useState(false); + + useEffect(() => { + const handleKeyDown = (event: KeyboardEvent) => { + if (event.key === 'Escape') onClose(); + }; + window.addEventListener('keydown', handleKeyDown); + return () => window.removeEventListener('keydown', handleKeyDown); + }, [onClose]); + + return ( + <div className="file-viewer-overlay" onClick={onClose}> + <div + className="file-viewer static-file-viewer" + role="dialog" + aria-modal="true" + aria-label={`Preview ${filename}`} + onClick={(event) => event.stopPropagation()} + > + <div className="file-viewer-header"> + <span className="file-viewer-title"> + <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} /> + {filename} + </span> + <span className="file-viewer-actions"> + <a href={fileUrl} download={filename}>Download</a> + <button className="file-viewer-close" onClick={onClose} title="Close (Esc)"> + <img className="icon-invert" src={ICONS.close} alt="Close" /> + </button> + </span> + </div> + <div className="file-viewer-content static-file-preview"> + {failed && <div className="error-message">Unable to preview this file.</div>} + {!failed && previewKind === 'image' && ( + <img + className="static-file-image" + src={fileUrl} + alt={filename} + onError={() => setFailed(true)} + /> + )} + {!failed && previewKind === 'video' && ( + <video + className="static-file-video" + src={fileUrl} + controls + onError={() => setFailed(true)} + /> + )} + {!failed && previewKind === 'audio' && ( + <audio + className="static-file-audio" + src={fileUrl} + controls + onError={() => setFailed(true)} + /> + )} + {!failed && previewKind === 'pdf' && ( + <iframe + className="static-file-pdf" + src={fileUrl} + title={filename} + onError={() => setFailed(true)} + /> + )} + </div> + </div> + </div> + ); +} + /** * Component: FileList */ @@ -338,7 +430,7 @@ e.preventDefault(); if (isDir) { onNavigate(item.abspath); - } else if (isCodeFile(item.basename)) { + } else if (isCodeFile(item.basename) || getStaticPreviewKind(item.basename)) { onOpenFile(item.abspath); } else { window.open(`/api/repo/file?path=${encodeURIComponent(item.abspath)}`, '_blank'); @@ -534,6 +626,8 @@ {viewingFile && ( isMarkdownFile(viewingFile) ? ( <MarkdownViewerModal filePath={viewingFile} onClose={handleCloseFile} /> + ) : getStaticPreviewKind(viewingFile) ? ( + <StaticFileViewer filePath={viewingFile} onClose={handleCloseFile} /> ) : ( <FileViewer filePath={viewingFile} onClose={handleCloseFile} /> )