WIP: Add canvas zoom/pan/pinch event handlers - handlers deployed but zoom interaction not working yet, will debug later

This commit is contained in:
2026-01-23 13:14:29 +00:00
parent 135087e096
commit e89f60f8a5
4 changed files with 675 additions and 8 deletions
+1
View File
@@ -7,6 +7,7 @@
<svelte:head> <svelte:head>
<link rel="icon" href={favicon} /> <link rel="icon" href={favicon} />
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.min.js"></script>
</svelte:head> </svelte:head>
{@render children()} {@render children()}
+551 -8
View File
@@ -65,12 +65,14 @@
const getSvgIcon = (iconType: string): string => { const getSvgIcon = (iconType: string): string => {
const icons: {[key: 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>`, 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 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>`, 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 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>`, 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 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>`, 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;"><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>`, 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>`,
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>`, zip: `<svg viewBox="0 0 24 24" fill="currentColor" style="width:16px;height:16px;color:#6b7280;"><path d="M19 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; 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 // Search and filters
let searchQuery = ''; let searchQuery = '';
let showFiltersPanel = false; let showFiltersPanel = false;
@@ -111,6 +506,15 @@
let showFileViewer = false; let showFileViewer = false;
let currentViewerFile: any = null; let currentViewerFile: any = null;
let graphToken = ''; 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 graphTokenExpiry: number | null = null; // Expiration timestamp in ms
let graphTokenRefreshTimer: NodeJS.Timeout | null = null; // Background refresh timer let graphTokenRefreshTimer: NodeJS.Timeout | null = null; // Background refresh timer
@@ -487,11 +891,108 @@
currentViewerFile = file; currentViewerFile = file;
showFileViewer = true; showFileViewer = true;
showDetailModal = false; 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() { function closeFileViewer() {
showFileViewer = false; showFileViewer = false;
showPdfViewer = false;
currentViewerFile = null; currentViewerFile = null;
currentPdf = null;
if (currentPreviewUrl) {
URL.revokeObjectURL(currentPreviewUrl);
currentPreviewUrl = null;
}
showDetailModal = true; showDetailModal = true;
} }
@@ -967,8 +1468,50 @@
</div> </div>
<!-- Viewer Content --> <!-- Viewer Content -->
<div style="flex: 1; overflow: auto; background: #000; display: flex; align-items: center; justify-content: center;"> <div style="flex: 1; overflow: auto; background: #000; display: flex; align-items: center; justify-content: center; flex-direction: column;">
{#if currentViewerFile.url} {#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 <iframe
src={currentViewerFile.url} src={currentViewerFile.url}
style="width: 100%; height: 100%; border: none;" style="width: 100%; height: 100%; border: none;"
@@ -0,0 +1,62 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';
/**
* Proxy endpoint to fetch file content from SharePoint
* Bypasses CORS restrictions by using backend authentication
*/
export const GET: RequestHandler = async ({ request, url }) => {
try {
// Get Graph token from request header or environment
const headerToken = request.headers.get('x-graph-token') || '';
const envToken = process.env.GRAPH_TOKEN || process.env.MS_GRAPH_TOKEN || '';
const token = headerToken || envToken;
if (!token) {
return json({ error: 'GRAPH_TOKEN missing' }, { status: 500 });
}
const driveId = url.searchParams.get('driveId');
const itemId = url.searchParams.get('itemId');
if (!driveId || !itemId) {
return json({ error: 'driveId and itemId required' }, { status: 400 });
}
// Fetch from Microsoft Graph API
const graphUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content`;
const res = await fetch(graphUrl, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
const text = await res.text();
console.error(`[file-content] Graph API error: ${res.status} - ${text}`);
return json({ error: `Graph ${res.status}: ${text}` }, { status: 502 });
}
// Get the content type and disposition headers
const contentType = res.headers.get('content-type') || 'application/octet-stream';
const contentDisposition = res.headers.get('content-disposition') || '';
const headers: Record<string, string> = {
'Content-Type': contentType,
'Cache-Control': 'public, max-age=3600',
};
if (contentDisposition) {
headers['Content-Disposition'] = contentDisposition;
}
// Return the file as a response
const buffer = await res.arrayBuffer();
return new Response(buffer, {
status: 200,
headers,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error('[file-content] Error:', message);
return json({ error: message }, { status: 500 });
}
};
@@ -0,0 +1,61 @@
import { json } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';
/**
* Proxy endpoint to fetch and convert Word documents to PDF
* Uses Microsoft Graph API with ?format=pdf parameter
*/
export const GET: RequestHandler = async ({ request, url }) => {
try {
// Get Graph token from request header or environment
const headerToken = request.headers.get('x-graph-token') || '';
const envToken = process.env.GRAPH_TOKEN || process.env.MS_GRAPH_TOKEN || '';
const token = headerToken || envToken;
if (!token) {
return json({ error: 'GRAPH_TOKEN missing' }, { status: 500 });
}
const driveId = url.searchParams.get('driveId');
const itemId = url.searchParams.get('itemId');
if (!driveId || !itemId) {
return json({ error: 'driveId and itemId required' }, { status: 400 });
}
// Use Graph API to convert to PDF format
const graphUrl = `https://graph.microsoft.com/v1.0/drives/${driveId}/items/${itemId}/content?format=pdf`;
const res = await fetch(graphUrl, {
headers: { Authorization: `Bearer ${token}` },
});
if (!res.ok) {
const text = await res.text();
console.error(`[file-pdf] Conversion failed: ${res.status} - ${text}`);
return json({ error: `PDF conversion failed: ${res.status}` }, { status: 502 });
}
const headers: Record<string, string> = {
'Content-Type': 'application/pdf',
'Cache-Control': 'public, max-age=3600',
};
// Update filename to .pdf if present
const contentDisposition = res.headers.get('content-disposition');
if (contentDisposition) {
headers['Content-Disposition'] = contentDisposition.replace(/\.(docx?)/gi, '.pdf');
}
console.log(`[file-pdf] Successfully converted: driveId=${driveId}, itemId=${itemId}`);
const buffer = await res.arrayBuffer();
return new Response(buffer, {
status: 200,
headers,
});
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
console.error('[file-pdf] Error:', message);
return json({ error: message }, { status: 500 });
}
};