From e89f60f8a51b4214d0b9087ab29084fa8a155424 Mon Sep 17 00:00:00 2001 From: aewing Date: Fri, 23 Jan 2026 13:14:29 +0000 Subject: [PATCH] WIP: Add canvas zoom/pan/pinch event handlers - handlers deployed but zoom interaction not working yet, will debug later --- Mode2Svelte/src/routes/+layout.svelte | 1 + Mode2Svelte/src/routes/+page.svelte | 559 +++++++++++++++++- .../src/routes/api/file-content/+server.ts | 62 ++ .../src/routes/api/file-pdf/+server.ts | 61 ++ 4 files changed, 675 insertions(+), 8 deletions(-) create mode 100644 Mode2Svelte/src/routes/api/file-content/+server.ts create mode 100644 Mode2Svelte/src/routes/api/file-pdf/+server.ts diff --git a/Mode2Svelte/src/routes/+layout.svelte b/Mode2Svelte/src/routes/+layout.svelte index 96f5db4..0b0b350 100644 --- a/Mode2Svelte/src/routes/+layout.svelte +++ b/Mode2Svelte/src/routes/+layout.svelte @@ -7,6 +7,7 @@ + {@render children()} diff --git a/Mode2Svelte/src/routes/+page.svelte b/Mode2Svelte/src/routes/+page.svelte index 5672368..2cda3ac 100644 --- a/Mode2Svelte/src/routes/+page.svelte +++ b/Mode2Svelte/src/routes/+page.svelte @@ -65,12 +65,14 @@ const getSvgIcon = (iconType: string): string => { const icons: {[key: string]: string} = { - pdf: ``, - doc: ``, - sheet: ``, - presentation: ``, - image: ``, - file: ``, + pdf: `PDF`, + doc: `W`, + sheet: `X`, + presentation: `P`, + image: ``, + zip: ``, + text: ``, + file: ``, }; 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(); + 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 @@ -
- {#if currentViewerFile.url} +
+ {#if showPdfViewer && currentPdf} + +
+
+ + + + + {Math.round(currentPdf.scale * 100)}% + +
+ {#if !currentPdf.isImage && currentPdf.pages > 1} +
+ + + Page {currentPdf.page} / {currentPdf.pages} +
+ {/if} +
+ + +
+ +
+ {:else if currentViewerFile.url}