comparison 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
comparison
equal deleted inserted replaced
223:0e7b9464248d 224:3007ef5fc0ed
20 'conf', 'md', 'markdown', 'txt', 'log', 'sql', 'graphql', 'vue', 20 'conf', 'md', 'markdown', 'txt', 'log', 'sql', 'graphql', 'vue',
21 'svelte', 'astro', 'prisma', 'dockerfile', 'makefile', 'cmake', 21 'svelte', 'astro', 'prisma', 'dockerfile', 'makefile', 'cmake',
22 'gradle', 'pom', 'lock', 'gitignore', 'env', 'example', 'sample' 22 'gradle', 'pom', 'lock', 'gitignore', 'env', 'example', 'sample'
23 ]); 23 ]);
24 24
25 type StaticPreviewKind = 'image' | 'video' | 'audio' | 'pdf';
26
27 const IMAGE_EXTENSIONS = new Set([
28 'png', 'jpg', 'jpeg', 'gif', 'webp', 'avif', 'bmp', 'ico', 'svg'
29 ]);
30 const VIDEO_EXTENSIONS = new Set(['mp4', 'm4v', 'webm', 'mov', 'ogv']);
31 const AUDIO_EXTENSIONS = new Set(['mp3', 'wav', 'ogg', 'oga', 'flac', 'm4a', 'aac']);
32
25 // Prefetch cache 33 // Prefetch cache
26 const prefetchCache = new Map<string, Promise<any>>(); 34 const prefetchCache = new Map<string, Promise<any>>();
27 35
28 function isCodeFile(filename: string): boolean { 36 function isCodeFile(filename: string): boolean {
29 const ext = filename.split('.').pop()?.toLowerCase() || ''; 37 const ext = filename.split('.').pop()?.toLowerCase() || '';
36 } 44 }
37 45
38 function isMarkdownFile(filename: string): boolean { 46 function isMarkdownFile(filename: string): boolean {
39 const ext = filename.split('.').pop()?.toLowerCase() || ''; 47 const ext = filename.split('.').pop()?.toLowerCase() || '';
40 return ext === 'md' || ext === 'markdown'; 48 return ext === 'md' || ext === 'markdown';
49 }
50
51 function getStaticPreviewKind(filename: string): StaticPreviewKind | null {
52 const ext = filename.split('.').pop()?.toLowerCase() || '';
53 if (IMAGE_EXTENSIONS.has(ext)) return 'image';
54 if (VIDEO_EXTENSIONS.has(ext)) return 'video';
55 if (AUDIO_EXTENSIONS.has(ext)) return 'audio';
56 if (ext === 'pdf') return 'pdf';
57 return null;
41 } 58 }
42 59
43 function prefetchDirectory(path: string): void { 60 function prefetchDirectory(path: string): void {
44 const cacheKey = `dir:${path}`; 61 const cacheKey = `dir:${path}`;
45 if (prefetchCache.has(cacheKey)) return; 62 if (prefetchCache.has(cacheKey)) return;
274 </div> 291 </div>
275 </div> 292 </div>
276 ); 293 );
277 } 294 }
278 295
296 function StaticFileViewer({ filePath, onClose }: { filePath: string; onClose: () => void }) {
297 const filename = filePath.split('/').pop() || filePath;
298 const previewKind = getStaticPreviewKind(filename);
299 const fileUrl = `${API_BASE}/file?path=${encodeURIComponent(filePath)}`;
300 const [failed, setFailed] = useState(false);
301
302 useEffect(() => {
303 const handleKeyDown = (event: KeyboardEvent) => {
304 if (event.key === 'Escape') onClose();
305 };
306 window.addEventListener('keydown', handleKeyDown);
307 return () => window.removeEventListener('keydown', handleKeyDown);
308 }, [onClose]);
309
310 return (
311 <div className="file-viewer-overlay" onClick={onClose}>
312 <div
313 className="file-viewer static-file-viewer"
314 role="dialog"
315 aria-modal="true"
316 aria-label={`Preview ${filename}`}
317 onClick={(event) => event.stopPropagation()}
318 >
319 <div className="file-viewer-header">
320 <span className="file-viewer-title">
321 <img src={ICONS.file} alt="" style={{ width: 16, height: 16 }} />
322 {filename}
323 </span>
324 <span className="file-viewer-actions">
325 <a href={fileUrl} download={filename}>Download</a>
326 <button className="file-viewer-close" onClick={onClose} title="Close (Esc)">
327 <img className="icon-invert" src={ICONS.close} alt="Close" />
328 </button>
329 </span>
330 </div>
331 <div className="file-viewer-content static-file-preview">
332 {failed && <div className="error-message">Unable to preview this file.</div>}
333 {!failed && previewKind === 'image' && (
334 <img
335 className="static-file-image"
336 src={fileUrl}
337 alt={filename}
338 onError={() => setFailed(true)}
339 />
340 )}
341 {!failed && previewKind === 'video' && (
342 <video
343 className="static-file-video"
344 src={fileUrl}
345 controls
346 onError={() => setFailed(true)}
347 />
348 )}
349 {!failed && previewKind === 'audio' && (
350 <audio
351 className="static-file-audio"
352 src={fileUrl}
353 controls
354 onError={() => setFailed(true)}
355 />
356 )}
357 {!failed && previewKind === 'pdf' && (
358 <iframe
359 className="static-file-pdf"
360 src={fileUrl}
361 title={filename}
362 onError={() => setFailed(true)}
363 />
364 )}
365 </div>
366 </div>
367 </div>
368 );
369 }
370
279 /** 371 /**
280 * Component: FileList 372 * Component: FileList
281 */ 373 */
282 function FileList({ directories, files, onNavigate, onOpenFile }: { 374 function FileList({ directories, files, onNavigate, onOpenFile }: {
283 directories: any[]; 375 directories: any[];
336 }) { 428 }) {
337 const handleClick = (e: React.MouseEvent) => { 429 const handleClick = (e: React.MouseEvent) => {
338 e.preventDefault(); 430 e.preventDefault();
339 if (isDir) { 431 if (isDir) {
340 onNavigate(item.abspath); 432 onNavigate(item.abspath);
341 } else if (isCodeFile(item.basename)) { 433 } else if (isCodeFile(item.basename) || getStaticPreviewKind(item.basename)) {
342 onOpenFile(item.abspath); 434 onOpenFile(item.abspath);
343 } else { 435 } else {
344 window.open(`/api/repo/file?path=${encodeURIComponent(item.abspath)}`, '_blank'); 436 window.open(`/api/repo/file?path=${encodeURIComponent(item.abspath)}`, '_blank');
345 } 437 }
346 }; 438 };
532 624
533 {/* File Viewer Modal */} 625 {/* File Viewer Modal */}
534 {viewingFile && ( 626 {viewingFile && (
535 isMarkdownFile(viewingFile) ? ( 627 isMarkdownFile(viewingFile) ? (
536 <MarkdownViewerModal filePath={viewingFile} onClose={handleCloseFile} /> 628 <MarkdownViewerModal filePath={viewingFile} onClose={handleCloseFile} />
629 ) : getStaticPreviewKind(viewingFile) ? (
630 <StaticFileViewer filePath={viewingFile} onClose={handleCloseFile} />
537 ) : ( 631 ) : (
538 <FileViewer filePath={viewingFile} onClose={handleCloseFile} /> 632 <FileViewer filePath={viewingFile} onClose={handleCloseFile} />
539 ) 633 )
540 )} 634 )}
541 </> 635 </>