1- import { useState , useRef } from "react" ;
1+ import { useState , useRef , useCallback } from "react" ;
2+ import { Document , Page , pdfjs } from "react-pdf" ;
3+ import "react-pdf/dist/Page/AnnotationLayer.css" ;
4+ import "react-pdf/dist/Page/TextLayer.css" ;
25import CodeEditor from "./CodeEditor" ;
36import ImageViewer from "./ImageViewer" ;
47
8+ pdfjs . GlobalWorkerOptions . workerSrc = `https://unpkg.com/pdfjs-dist@${ pdfjs . version } /build/pdf.worker.min.mjs` ;
9+
510const IMAGE_EXTS = new Set ( [ "png" , "jpg" , "jpeg" , "gif" , "svg" , "webp" , "ico" , "bmp" ] ) ;
611const VIDEO_EXTS = new Set ( [ "mp4" , "webm" , "ogg" , "mov" , "mkv" , "avi" ] ) ;
712const AUDIO_EXTS = new Set ( [ "mp3" , "wav" , "ogg" , "flac" , "aac" , "m4a" ] ) ;
@@ -22,7 +27,7 @@ function MediaViewer({ path, type, onClose }: { path: string; type: "video" | "a
2227 return (
2328 < div className = "flex flex-col h-full" >
2429 < div className = "flex items-center justify-between px-3 py-2 bg-[#16162a] border-b border-gray-700 shrink-0" >
25- < span className = "text-sm text-gray-300 overflow-x-auto scrollbar-none whitespace-nowrap min-w-0 flex-1 select-all " > { path } </ span >
30+ < span className = "text-sm text-gray-300 whitespace-nowrap min-w-0 flex-1 overflow-hidden text-ellipsis select-none " > { path } </ span >
2631 < div className = "flex items-center gap-2 shrink-0" >
2732 < a
2833 href = { `/api/files/download?path=${ encodeURIComponent ( path ) } ` }
@@ -53,23 +58,31 @@ function MediaViewer({ path, type, onClose }: { path: string; type: "video" | "a
5358}
5459
5560function PdfViewer ( { path, onClose } : { path : string ; onClose : ( ) => void } ) {
56- const [ zoomPct , setZoomPct ] = useState ( 100 ) ;
61+ const [ numPages , setNumPages ] = useState < number > ( 0 ) ;
62+ const [ scale , setScale ] = useState ( 1.0 ) ;
63+ const containerRef = useRef < HTMLDivElement > ( null ) ;
64+
65+ const zoomIn = ( ) => setScale ( ( s ) => Math . min ( 3 , s + 0.25 ) ) ;
66+ const zoomOut = ( ) => setScale ( ( s ) => Math . max ( 0.25 , s - 0.25 ) ) ;
67+ const zoomFit = ( ) => setScale ( 1.0 ) ;
5768
58- const zoomIn = ( ) => setZoomPct ( ( z ) => Math . min ( 500 , z + 25 ) ) ;
59- const zoomOut = ( ) => setZoomPct ( ( z ) => Math . max ( 25 , z - 25 ) ) ;
60- const zoomFit = ( ) => setZoomPct ( 100 ) ;
69+ const onDocumentLoadSuccess = useCallback ( ( { numPages } : { numPages : number } ) => {
70+ setNumPages ( numPages ) ;
71+ } , [ ] ) ;
6172
62- // Use PDF viewer's built-in zoom via URL fragment
63- const pdfUrl = `/api/files/download?path=${ encodeURIComponent ( path ) } &inline=1#zoom=${ zoomPct } ` ;
73+ const pdfUrl = `/api/files/download?path=${ encodeURIComponent ( path ) } &inline=1` ;
6474
6575 return (
6676 < div className = "flex flex-col h-full" >
6777 < div className = "flex items-center justify-between px-3 py-2 bg-[#16162a] border-b border-gray-700 shrink-0" >
68- < span className = "text-sm text-gray-300 overflow-x-auto scrollbar-none whitespace-nowrap min-w-0 flex-1 select-all " > { path } </ span >
78+ < span className = "text-sm text-gray-300 whitespace-nowrap min-w-0 flex-1 overflow-hidden text-ellipsis select-none " > { path } </ span >
6979 < div className = "flex items-center gap-2" >
80+ { numPages > 0 && (
81+ < span className = "text-[10px] text-gray-500" > { numPages } pg</ span >
82+ ) }
7083 < div className = "flex items-center gap-1 border border-gray-700 rounded overflow-hidden" >
7184 < button onClick = { zoomOut } className = "px-2 py-0.5 text-xs text-gray-400 hover:text-white hover:bg-gray-700" > -</ button >
72- < button onClick = { zoomFit } className = "px-2 py-0.5 text-[10px] text-gray-400 hover:text-white hover:bg-gray-700 tabular-nums" > { zoomPct } %</ button >
85+ < button onClick = { zoomFit } className = "px-2 py-0.5 text-[10px] text-gray-400 hover:text-white hover:bg-gray-700 tabular-nums" > { Math . round ( scale * 100 ) } %</ button >
7386 < button onClick = { zoomIn } className = "px-2 py-0.5 text-xs text-gray-400 hover:text-white hover:bg-gray-700" > +</ button >
7487 </ div >
7588 < a
@@ -89,13 +102,30 @@ function PdfViewer({ path, onClose }: { path: string; onClose: () => void }) {
89102 </ button >
90103 </ div >
91104 </ div >
92- < div className = "flex-1 min-h-0 bg-[#0d0d1a]" >
93- < iframe
94- key = { zoomPct }
95- src = { pdfUrl }
96- title = { path }
97- className = "w-full h-full border-0"
98- />
105+ < div ref = { containerRef } className = "flex-1 min-h-0 overflow-auto bg-[#0d0d1a]" >
106+ < Document
107+ file = { pdfUrl }
108+ onLoadSuccess = { onDocumentLoadSuccess }
109+ loading = {
110+ < div className = "flex items-center justify-center h-full text-gray-500 text-sm" > Loading PDF...</ div >
111+ }
112+ error = {
113+ < div className = "flex items-center justify-center h-full text-red-400 text-sm" > Failed to load PDF</ div >
114+ }
115+ >
116+ < div className = "flex flex-col items-center gap-2 p-4" >
117+ { Array . from ( { length : numPages } , ( _ , i ) => (
118+ < Page
119+ key = { i + 1 }
120+ pageNumber = { i + 1 }
121+ scale = { scale }
122+ className = "shadow-lg"
123+ renderTextLayer = { true }
124+ renderAnnotationLayer = { true }
125+ />
126+ ) ) }
127+ </ div >
128+ </ Document >
99129 </ div >
100130 </ div >
101131 ) ;
@@ -128,7 +158,7 @@ export default function FileViewer({ path, content, onSave, onClose }: FileViewe
128158 return (
129159 < div className = "flex flex-col h-full" >
130160 < div className = "flex items-center justify-between px-3 py-2 bg-[#16162a] border-b border-gray-700 shrink-0" >
131- < span className = "text-sm text-gray-300 overflow-x-auto scrollbar-none whitespace-nowrap min-w-0 flex-1 select-all " > { path } </ span >
161+ < span className = "text-sm text-gray-300 whitespace-nowrap min-w-0 flex-1 overflow-hidden text-ellipsis select-none " > { path } </ span >
132162 < div className = "flex items-center gap-2" >
133163 < a
134164 href = { `/api/files/download?path=${ encodeURIComponent ( path ) } ` }
0 commit comments