@@ -65,12 +65,14 @@
const getSvgIcon = ( iconType : string ) : string => {
const icons : {[ key : string ] : string } = {
pdf : `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:16px;height:16px;"><path stroke-linecap="round" stroke-linejoin="round" stroke- width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6M9 16h6"></path ></svg>` ,
doc : `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:16px;height:16px;"><path stroke-linecap="round" stroke-linejoin="round" stroke- width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z "></path ></svg>` ,
sheet : `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:16px;height:16px;"><path stroke-linecap="round" stroke-linejoin="round" stroke- width="2" d="M9 17v-2m3 2v-4m3 4v-6m2-10H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V5a2 2 0 00-2-2z "></path ></svg>` ,
presentation : `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:16px;height:16px;"><path stroke-linecap="round" stroke-linejoin="round" stroke- width="2" d="M7 4v16m10 0V4m3-2H4a2 2 0 00-2 2v16a2 2 0 002 2h16a2 2 0 002-2V4a2 2 0 00-2-2z "></path ></svg>` ,
image : `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:16px;height:16px;"><path stroke-linecap="round" stroke-linejoin="round" stroke- width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path ></svg>` ,
file : `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:16px;height:16px;"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path ></svg>` ,
pdf : `<svg viewBox="0 0 24 24" fill="none" style="width:16px;height:16px;"><rect fill="#D32F2F" width="24 " height="24" rx="2"/><text x="12" y="16" font-size="10" font-weight="bold" fill="white" text-anchor="middle">PDF</text ></svg>` ,
doc : `<svg viewBox="0 0 24 24" fill="none" style="width:16px;height:16px;"><rect fill="#2B579A" width="24 " height="24" rx="2"/><text x="12" y="16" font-size="10" font-weight="bold" fill="white" text-anchor="middle ">W </text ></svg>` ,
sheet : `<svg viewBox="0 0 24 24" fill="none" style="width:16px;height:16px;"><rect fill="#217346" width="24 " height="24" rx="2"/><text x="12" y="16" font-size="10" font-weight="bold" fill="white" text-anchor="middle ">X </text ></svg>` ,
presentation : `<svg viewBox="0 0 24 24" fill="none" style="width:16px;height:16px;"><rect fill="#D35230" width="24 " height="24" rx="2"/><text x="12" y="16" font-size="10" font-weight="bold" fill="white" text-anchor="middle ">P </text ></svg>` ,
image : `<svg fill="none" stroke="currentColor" viewBox="0 0 24 24" style="width:16px;height:16px;color:#6b7280;"><rect x="2" y="2" width="20 " height="20" rx="2"/><circle cx="8" cy="8" r="2"/><path d="M21 15l-5-5L5 21"/ ></svg>` ,
zip : `<svg viewBox="0 0 24 24" fill="currentColor" style="width:16px;height:16px;color:#6b7280;"><path d="M1 9 3H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-2zm0 16H5V5h14v14zm-5-9h-3v4h3v-4zm0-3h-3v2h3V7z"/ ></svg>` ,
text : `<svg viewBox="0 0 24 24" fill="currentColor" style="width:16px;height:16px;color:#6b7280;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/><path d="M16 13H8M16 17H8M10 9H8"/></svg>` ,
file : `<svg viewBox="0 0 24 24" fill="currentColor" style="width:16px;height:16px;color:#6b7280;"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6z"/></svg>` ,
};
return icons [ iconType ] || icons . file ;
};
@@ -91,6 +93,399 @@
}
};
const canPreviewInline = ( name : string , contentType : string = '' ) : boolean => {
const ext = ( name || '' ). toLowerCase (). split ( '.' ). pop ();
const type = ( contentType || '' ). toLowerCase ();
return ext === 'pdf' || type . includes ( 'pdf' ) ||
[ 'doc' , 'docx' ]. includes ( ext as string ) || type . includes ( 'word' ) ||
[ 'jpg' , 'jpeg' , 'png' , 'gif' , 'svg' , 'bmp' ]. includes ( ext as string ) || type . includes ( 'image' );
};
const renderPdfPage = async () => {
if ( ! currentPdf || ! currentPdf . doc ) return ;
const pageObj = await currentPdf . doc . getPage ( currentPdf . page );
const viewport = pageObj . getViewport ({ scale : currentPdf.scale });
const canvas = document . getElementById ( 'pdfCanvas' ) as HTMLCanvasElement ;
if ( ! canvas ) return ;
const context = canvas . getContext ( '2d' );
if ( ! context ) return ;
canvas . height = viewport . height ;
canvas . width = viewport . width ;
await pageObj . render ({
canvasContext : context ,
viewport : viewport ,
}). promise ;
};
const renderImageOnCanvas = () => {
if ( ! currentPdf ? . imageElement ) return ;
const canvas = document . getElementById ( 'pdfCanvas' ) as HTMLCanvasElement ;
if ( ! canvas ) return ;
const ctx = canvas . getContext ( '2d' );
if ( ! ctx ) return ;
const scale = currentPdf . scale ;
canvas . width = ( currentPdf . imageWidth || 0 ) * scale ;
canvas . height = ( currentPdf . imageHeight || 0 ) * scale ;
ctx . drawImage ( currentPdf . imageElement , 0 , 0 , canvas . width , canvas . height );
};
const updateZoomLabel = () => {
const label = document . getElementById ( 'pdfZoomLabel' );
if ( label && currentPdf ) {
label . textContent = ` ${ Math . round ( currentPdf . scale * 100 ) } %` ;
}
};
const setupCanvasEventHandlers = () => {
const canvas = document . getElementById ( 'pdfCanvas' ) as HTMLCanvasElement ;
if ( ! canvas ) return ;
// Zoom buttons
const zoomOutBtn = document . getElementById ( 'pdfZoomOut' );
const zoomResetBtn = document . getElementById ( 'pdfZoomReset' );
const zoomInBtn = document . getElementById ( 'pdfZoomIn' );
if ( zoomOutBtn ) {
zoomOutBtn . addEventListener ( 'click' , async () => {
if ( ! currentPdf ) return ;
currentPdf . scale = Math . max ( MIN_PDF_SCALE , currentPdf . scale - 0.1 );
pdfPan = { x : 0 , y : 0 , dragging : false , startX : 0 , startY : 0 , baseX : 0 , baseY : 0 };
canvas . style . transform = 'translate(0px, 0px)' ;
if ( currentPdf . isImage ) {
renderImageOnCanvas ();
} else {
await renderPdfPage ();
}
updateZoomLabel ();
});
}
if ( zoomResetBtn ) {
zoomResetBtn . addEventListener ( 'click' , async () => {
if ( ! currentPdf ) return ;
currentPdf . scale = DEFAULT_PDF_SCALE ;
pdfPan = { x : 0 , y : 0 , dragging : false , startX : 0 , startY : 0 , baseX : 0 , baseY : 0 };
canvas . style . transform = 'translate(0px, 0px)' ;
if ( currentPdf . isImage ) {
renderImageOnCanvas ();
} else {
await renderPdfPage ();
}
updateZoomLabel ();
});
}
if ( zoomInBtn ) {
zoomInBtn . addEventListener ( 'click' , async () => {
if ( ! currentPdf ) return ;
currentPdf . scale = Math . min ( MAX_PDF_SCALE , currentPdf . scale + 0.1 );
pdfPan = { x : 0 , y : 0 , dragging : false , startX : 0 , startY : 0 , baseX : 0 , baseY : 0 };
canvas . style . transform = 'translate(0px, 0px)' ;
if ( currentPdf . isImage ) {
renderImageOnCanvas ();
} else {
await renderPdfPage ();
}
updateZoomLabel ();
});
}
// Page navigation buttons (if multi-page)
const prevBtn = document . getElementById ( 'pdfPrev' );
const nextBtn = document . getElementById ( 'pdfNext' );
if ( prevBtn ) {
prevBtn . addEventListener ( 'click' , async () => {
if ( ! currentPdf || currentPdf . page <= 1 ) return ;
currentPdf . page -- ;
pdfPan = { x : 0 , y : 0 , dragging : false , startX : 0 , startY : 0 , baseX : 0 , baseY : 0 };
canvas . style . transform = 'translate(0px, 0px)' ;
await renderPdfPage ();
document . getElementById ( 'pdfPageNum' ) ! . textContent = String ( currentPdf . page );
});
}
if ( nextBtn ) {
nextBtn . addEventListener ( 'click' , async () => {
if ( ! currentPdf || currentPdf . page >= currentPdf . pages ) return ;
currentPdf . page ++ ;
pdfPan = { x : 0 , y : 0 , dragging : false , startX : 0 , startY : 0 , baseX : 0 , baseY : 0 };
canvas . style . transform = 'translate(0px, 0px)' ;
await renderPdfPage ();
document . getElementById ( 'pdfPageNum' ) ! . textContent = String ( currentPdf . page );
});
}
// Canvas pan (mouse drag)
canvas . addEventListener ( 'mousedown' , ( e ) => {
pdfPan . dragging = true ;
pdfPan . startX = e . clientX ;
pdfPan . startY = e . clientY ;
canvas . style . cursor = 'grabbing' ;
});
canvas . addEventListener ( 'mousemove' , ( e ) => {
if ( ! pdfPan . dragging ) return ;
const dx = e . clientX - pdfPan . startX ;
const dy = e . clientY - pdfPan . startY ;
pdfPan . x = pdfPan . baseX + dx ;
pdfPan . y = pdfPan . baseY + dy ;
canvas . style . transform = `translate( ${ pdfPan . x } px, ${ pdfPan . y } px)` ;
});
canvas . addEventListener ( 'mouseup' , () => {
if ( pdfPan . dragging ) {
pdfPan . baseX = pdfPan . x ;
pdfPan . baseY = pdfPan . y ;
pdfPan . dragging = false ;
canvas . style . cursor = 'grab' ;
}
});
canvas . addEventListener ( 'mouseleave' , () => {
if ( pdfPan . dragging ) {
pdfPan . baseX = pdfPan . x ;
pdfPan . baseY = pdfPan . y ;
pdfPan . dragging = false ;
canvas . style . cursor = 'grab' ;
}
});
// Pinch zoom (touch)
let lastDistance = 0 ;
canvas . addEventListener ( 'touchstart' , ( e ) => {
if ( e . touches . length === 2 ) {
const dx = e . touches [ 0 ]. clientX - e . touches [ 1 ]. clientX ;
const dy = e . touches [ 0 ]. clientY - e . touches [ 1 ]. clientY ;
lastDistance = Math . sqrt ( dx * dx + dy * dy );
} else if ( e . touches . length === 1 ) {
pdfPan . dragging = true ;
pdfPan . startX = e . touches [ 0 ]. clientX ;
pdfPan . startY = e . touches [ 0 ]. clientY ;
}
});
canvas . addEventListener ( 'touchmove' , async ( e ) => {
if ( e . touches . length === 2 ) {
e . preventDefault ();
const dx = e . touches [ 0 ]. clientX - e . touches [ 1 ]. clientX ;
const dy = e . touches [ 0 ]. clientY - e . touches [ 1 ]. clientY ;
const distance = Math . sqrt ( dx * dx + dy * dy );
if ( lastDistance > 0 ) {
const scaleDelta = ( distance - lastDistance ) / 100 ;
const newScale = currentPdf ? currentPdf . scale + scaleDelta : DEFAULT_PDF_SCALE ;
if ( currentPdf ) {
currentPdf . scale = Math . max ( MIN_PDF_SCALE , Math . min ( MAX_PDF_SCALE , newScale ));
if ( currentPdf . isImage ) {
renderImageOnCanvas ();
} else {
await renderPdfPage ();
}
updateZoomLabel ();
}
}
lastDistance = distance ;
} else if ( e . touches . length === 1 && pdfPan . dragging ) {
const dx = e . touches [ 0 ]. clientX - pdfPan . startX ;
const dy = e . touches [ 0 ]. clientY - pdfPan . startY ;
pdfPan . x = pdfPan . baseX + dx ;
pdfPan . y = pdfPan . baseY + dy ;
const canvas = document . getElementById ( 'pdfCanvas' ) as HTMLCanvasElement ;
if ( canvas ) {
canvas . style . transform = `translate( ${ pdfPan . x } px, ${ pdfPan . y } px)` ;
}
}
});
canvas . addEventListener ( 'touchend' , () => {
pdfPan . baseX = pdfPan . x ;
pdfPan . baseY = pdfPan . y ;
pdfPan . dragging = false ;
lastDistance = 0 ;
});
// Wheel zoom (mouse scroll)
canvas . addEventListener ( 'wheel' , async ( e ) => {
e . preventDefault ();
if ( ! currentPdf ) return ;
const zoomDelta = e . deltaY > 0 ? - 0.1 : 0.1 ;
currentPdf . scale = Math . max ( MIN_PDF_SCALE , Math . min ( MAX_PDF_SCALE , currentPdf . scale + zoomDelta ));
if ( currentPdf . isImage ) {
renderImageOnCanvas ();
} else {
await renderPdfPage ();
}
updateZoomLabel ();
}, { passive : false });
};
// Setup canvas event handlers for zoom, pan, and pinch
const setupPdfControls = () => {
const zoomInBtn = document . getElementById ( 'pdfZoomInBtn' ) as HTMLButtonElement ;
const zoomOutBtn = document . getElementById ( 'pdfZoomOutBtn' ) as HTMLButtonElement ;
const zoomResetBtn = document . getElementById ( 'pdfZoomResetBtn' ) as HTMLButtonElement ;
const canvas = document . getElementById ( 'pdfCanvas' ) as HTMLCanvasElement ;
if ( ! canvas ) return ;
const applyPan = () => {
canvas . style . transform = `translate( ${ pdfPan . x } px, ${ pdfPan . y } px)` ;
};
const activePointers = new Map < number , { x : number ; y : number }>();
let pinchStartDistance : number | null = null ;
let pinchStartScale : number | null = null ;
const distanceBetween = () => {
if ( activePointers . size < 2 ) return null ;
const pts = Array . from ( activePointers . values ());
const a = pts [ 0 ];
const b = pts [ 1 ];
const dx = a . x - b . x ;
const dy = a . y - b . y ;
return Math . hypot ( dx , dy );
};
// Zoom button handlers
if ( zoomInBtn ) {
zoomInBtn . addEventListener ( 'click' , async () => {
if ( ! currentPdf ) return ;
currentPdf . scale = Math . min ( currentPdf . scale + 0.1 , MAX_PDF_SCALE );
if ( currentPdf . isImage ) {
renderImageOnCanvas ();
} else {
await renderPdfPage ();
}
});
}
if ( zoomOutBtn ) {
zoomOutBtn . addEventListener ( 'click' , async () => {
if ( ! currentPdf ) return ;
currentPdf . scale = Math . max ( currentPdf . scale - 0.1 , MIN_PDF_SCALE );
if ( currentPdf . isImage ) {
renderImageOnCanvas ();
} else {
await renderPdfPage ();
}
});
}
if ( zoomResetBtn ) {
zoomResetBtn . addEventListener ( 'click' , async () => {
if ( ! currentPdf ) return ;
currentPdf . scale = currentPdf . isImage ? 1 : DEFAULT_PDF_SCALE ;
pdfPan = { x : 0 , y : 0 , dragging : false , startX : 0 , startY : 0 , baseX : 0 , baseY : 0 };
canvas . style . transform = 'translate(0px, 0px)' ;
if ( currentPdf . isImage ) {
renderImageOnCanvas ();
} else {
await renderPdfPage ();
}
});
}
// Canvas event handlers
canvas . style . touchAction = 'none' ;
canvas . addEventListener ( 'wheel' , async ( e ) => {
if ( ! currentPdf ) return ;
e . preventDefault ();
const delta = e . deltaY ;
const step = 0.1 ;
if ( delta < 0 ) {
currentPdf . scale = Math . min ( currentPdf . scale + step , MAX_PDF_SCALE );
} else {
currentPdf . scale = Math . max ( currentPdf . scale - step , MIN_PDF_SCALE );
}
if ( currentPdf . isImage ) {
renderImageOnCanvas ();
} else {
await renderPdfPage ();
}
}, { passive : false });
canvas . addEventListener ( 'pointerdown' , ( e ) => {
if ( ! currentPdf ) return ;
activePointers . set ( e . pointerId , { x : e.clientX , y : e.clientY });
if ( activePointers . size === 2 ) {
pinchStartDistance = distanceBetween ();
pinchStartScale = currentPdf . scale ;
pdfPan . dragging = false ;
} else if ( activePointers . size === 1 ) {
pdfPan . dragging = true ;
pdfPan . startX = e . clientX ;
pdfPan . startY = e . clientY ;
pdfPan . baseX = pdfPan . x ;
pdfPan . baseY = pdfPan . y ;
canvas . style . cursor = 'grabbing' ;
}
canvas . setPointerCapture ( e . pointerId );
});
canvas . addEventListener ( 'pointermove' , ( e ) => {
if ( ! currentPdf ) return ;
if ( activePointers . has ( e . pointerId )) {
activePointers . set ( e . pointerId , { x : e.clientX , y : e.clientY });
}
if ( activePointers . size >= 2 && pinchStartDistance ) {
const dist = distanceBetween ();
if ( dist ) {
const scale = pinchStartScale ! * ( dist / pinchStartDistance );
currentPdf . scale = Math . min ( Math . max ( scale , MIN_PDF_SCALE ), MAX_PDF_SCALE );
if ( currentPdf . isImage ) {
renderImageOnCanvas ();
} else {
renderPdfPage ();
}
}
return ;
}
if ( ! pdfPan . dragging ) return ;
const dx = e . clientX - pdfPan . startX ;
const dy = e . clientY - pdfPan . startY ;
pdfPan . x = pdfPan . baseX + dx ;
pdfPan . y = pdfPan . baseY + dy ;
applyPan ();
});
const endPan = ( e : PointerEvent ) => {
if ( activePointers . has ( e . pointerId )) {
activePointers . delete ( e . pointerId );
}
if ( activePointers . size < 2 ) {
pinchStartDistance = null ;
pinchStartScale = null ;
}
if ( activePointers . size === 0 ) {
pdfPan . dragging = false ;
canvas . style . cursor = 'grab' ;
}
try {
canvas . releasePointerCapture ( e . pointerId );
} catch {}
};
canvas . addEventListener ( 'pointerup' , endPan );
canvas . addEventListener ( 'pointercancel' , endPan );
};
// Search and filters
let searchQuery = '' ;
let showFiltersPanel = false ;
@@ -111,6 +506,15 @@
let showFileViewer = false ;
let currentViewerFile : any = null ;
let graphToken = '' ;
// PDF/Image viewer state
let currentPreviewUrl : string | null = null ;
let currentPdf : { doc : any ; page : number ; pages : number ; scale : number ; isImage? : boolean ; imageElement? : HTMLImageElement ; imageWidth? : number ; imageHeight? : number } | null = null ;
let pdfPan = { x : 0 , y : 0 , dragging : false , startX : 0 , startY : 0 , baseX : 0 , baseY : 0 };
let showPdfViewer = false ;
const DEFAULT_PDF_SCALE = 0.45 ;
const MIN_PDF_SCALE = 0.3 ;
const MAX_PDF_SCALE = 3.0 ;
let graphTokenExpiry : number | null = null ; // Expiration timestamp in ms
let graphTokenRefreshTimer : NodeJS.Timeout | null = null ; // Background refresh timer
@@ -487,11 +891,108 @@
currentViewerFile = file ;
showFileViewer = true ;
showDetailModal = false ;
// Try to preview in canvas if supported
const ext = ( file . name || '' ). toLowerCase (). split ( '.' ). pop ();
if ( canPreviewInline ( file . name , file . contentType || '' )) {
loadFilePreview ( file );
}
}
async function loadFilePreview ( file : any ) {
if ( ! file . url ) return ;
try {
let fetchUrl = '' ;
// Use backend proxy endpoints instead of direct SharePoint URLs
const ext = ( file . name || '' ). toLowerCase (). split ( '.' ). pop ();
const isWordDoc = [ 'doc' , 'docx' ]. includes ( ext as string ) || file . contentType ? . includes ( 'word' );
const isPdf = ext === 'pdf' || file . contentType ? . includes ( 'pdf' );
if ( file . driveId && file . id ) {
// Use proxy endpoints with Graph API
if ( isWordDoc ) {
fetchUrl = `/api/file-pdf?driveId= ${ encodeURIComponent ( file . driveId ) } &itemId= ${ encodeURIComponent ( file . id ) } ` ;
} else {
fetchUrl = `/api/file-content?driveId= ${ encodeURIComponent ( file . driveId ) } &itemId= ${ encodeURIComponent ( file . id ) } ` ;
}
} else {
// Fallback to direct URL (for non-SharePoint files)
fetchUrl = file . url ;
}
const response = await fetch ( fetchUrl , {
headers : graphToken ? { 'x-graph-token' : graphToken } : {},
});
if ( ! response . ok ) throw new Error ( `Failed to fetch file: ${ response . status } ` );
const blob = await response . blob ();
if ( currentPreviewUrl ) {
URL . revokeObjectURL ( currentPreviewUrl );
}
currentPreviewUrl = URL . createObjectURL ( blob );
const isImage = [ 'jpg' , 'jpeg' , 'png' , 'gif' , 'svg' , 'bmp' ]. includes ( ext as string ) || file . contentType ? . includes ( 'image' );
if (( isPdf || isWordDoc ) && ( window as any ). pdfjsLib ) {
const data = await blob . arrayBuffer ();
const pdfjsLib = ( window as any ). pdfjsLib ;
pdfjsLib . GlobalWorkerOptions . workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js' ;
const doc = await pdfjsLib . getDocument ({ data }). promise ;
currentPdf = {
doc ,
page : 1 ,
pages : doc.numPages ,
scale : DEFAULT_PDF_SCALE ,
};
pdfPan = { x : 0 , y : 0 , dragging : false , startX : 0 , startY : 0 , baseX : 0 , baseY : 0 };
showPdfViewer = true ;
// Render first page and setup controls
await new Promise ( resolve => setTimeout ( resolve , 100 ));
await renderPdfPage ();
updateZoomLabel ();
setupCanvasEventHandlers ();
} else if ( isImage ) {
const img = new Image ();
img . onload = async () => {
currentPdf = {
doc : null ,
page : 1 ,
pages : 1 ,
scale : 1 ,
isImage : true ,
imageElement : img ,
imageWidth : img.width ,
imageHeight : img.height ,
};
pdfPan = { x : 0 , y : 0 , dragging : false , startX : 0 , startY : 0 , baseX : 0 , baseY : 0 };
showPdfViewer = true ;
await new Promise ( resolve => setTimeout ( resolve , 100 ));
renderImageOnCanvas ();
updateZoomLabel ();
setupCanvasEventHandlers ();
};
img . src = currentPreviewUrl ;
}
} catch ( err ) {
console . error ( 'Preview load error:' , err );
}
}
function closeFileViewer() {
showFileViewer = false ;
showPdfViewer = false ;
currentViewerFile = null ;
currentPdf = null ;
if ( currentPreviewUrl ) {
URL . revokeObjectURL ( currentPreviewUrl );
currentPreviewUrl = null ;
}
showDetailModal = true ;
}
@@ -967,8 +1468,50 @@
</ div >
<!-- Viewer Content -->
< div style = "flex: 1; overflow: auto; background: #000; display: flex; align-items: center; justify-content: center;" >
{ #if currentViewerFile . url }
< div style = "flex: 1; overflow: auto; background: #000; display: flex; align-items: center; justify-content: center; flex-direction: column; " >
{ #if showPdfViewer && currentPdf }
<!-- PDF/Image Controls -->
< div style = "background: #1f2937; color: white; width: 100%; padding: 8px 12px; display: flex; align-items: center; gap: 12px; border-bottom: 1px solid #374151; flex-shrink: 0;" >
< div style = "display: flex; align-items: center; gap: 6px;" >
< button
id = "pdfZoomOutBtn"
style = "padding: 4px 8px; background: #6b7280; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: bold;"
>
−
</ button >
< button
id = "pdfZoomResetBtn"
style = "padding: 4px 8px; background: #6b7280; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: bold;"
>
{ Math . round ( currentPdf . scale * 100 )} %
</ button >
< button
id = "pdfZoomInBtn"
style = "padding: 4px 8px; background: #6b7280; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 11px; font-weight: bold;"
>
+
</ button >
< span style = "font-size: 11px; color: #d1d5db; margin-left: 6px;" >
{ Math . round ( currentPdf . scale * 100 )} %
</ span >
</ div >
{ #if ! currentPdf . isImage && currentPdf . pages > 1 }
< div style = "display: flex; align-items: center; gap: 6px; margin-left: 12px;" >
< button style = "padding: 4px 8px; background: #6b7280; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 11px;" > ◀</ button >
< button style = "padding: 4px 8px; background: #6b7280; color: white; border: none; border-radius: 4px; cursor: pointer; font-size: 11px;" > ▶</ button >
< span style = "font-size: 11px; color: #d1d5db;" > Page < span > { currentPdf . page } </ span > / < span > { currentPdf . pages } </ span ></ span >
</ div >
{ /if }
</ div >
<!-- Canvas Viewer -->
< div style = "flex: 1; overflow: hidden; background: #111; display: flex; align-items: center; justify-content: center; width: 100%;" >
< canvas
id = "pdfCanvas"
style = "max-width: 100%; max-height: 100%; object-fit: contain; background: white; cursor: grab; touch-action: none;"
></ canvas >
</ div >
{ :else if currentViewerFile . url }
< iframe
src = { currentViewerFile . url }
style="width: 100 %; height: 100 %; border: none ;"