refac
This commit is contained in:
@@ -477,6 +477,17 @@
|
|||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
{#if filePdfData !== null}
|
||||||
|
<Tooltip content={$i18n.t('Reset view')}>
|
||||||
|
<button
|
||||||
|
class="shrink-0 p-1 rounded hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-400 dark:text-gray-500 hover:text-gray-600 dark:hover:text-gray-400"
|
||||||
|
on:click={() => filePreviewRef?.resetPdfView()}
|
||||||
|
aria-label={$i18n.t('Reset view')}
|
||||||
|
>
|
||||||
|
<Reset className="size-3.5" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
{#if (isMarkdown || isCsv) && fileContent !== null && !editing}
|
{#if (isMarkdown || isCsv) && fileContent !== null && !editing}
|
||||||
<Tooltip content={showRaw ? $i18n.t('Preview') : $i18n.t('Source')}>
|
<Tooltip content={showRaw ? $i18n.t('Preview') : $i18n.t('Source')}>
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -6,6 +6,8 @@
|
|||||||
import Spinner from '../../common/Spinner.svelte';
|
import Spinner from '../../common/Spinner.svelte';
|
||||||
import PDFViewer from '../../common/PDFViewer.svelte';
|
import PDFViewer from '../../common/PDFViewer.svelte';
|
||||||
|
|
||||||
|
let pdfViewerRef: PDFViewer;
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
const i18n = getContext('i18n');
|
||||||
|
|
||||||
export let selectedFile: string | null = null;
|
export let selectedFile: string | null = null;
|
||||||
@@ -135,13 +137,17 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const resetPdfView = () => {
|
||||||
|
pdfViewerRef?.resetView();
|
||||||
|
};
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
disposePanzoom();
|
disposePanzoom();
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
class="flex-1 {fileImageUrl !== null || filePdfData !== null
|
class="flex-1 {fileImageUrl !== null
|
||||||
? 'overflow-hidden'
|
? 'overflow-hidden'
|
||||||
: 'overflow-y-auto'} min-h-0 relative h-full"
|
: 'overflow-y-auto'} min-h-0 relative h-full"
|
||||||
>
|
>
|
||||||
@@ -158,7 +164,7 @@
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if filePdfData !== null}
|
{:else if filePdfData !== null}
|
||||||
<PDFViewer data={filePdfData} className="w-full h-full" />
|
<PDFViewer bind:this={pdfViewerRef} data={filePdfData} className="w-full h-full" />
|
||||||
{:else if fileContent !== null}
|
{:else if fileContent !== null}
|
||||||
{#if isMarkdown && !showRaw}
|
{#if isMarkdown && !showRaw}
|
||||||
<div class="prose dark:prose-invert max-w-full text-sm p-3">
|
<div class="prose dark:prose-invert max-w-full text-sm p-3">
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, onDestroy, getContext } from 'svelte';
|
import { onMount, onDestroy } from 'svelte';
|
||||||
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
import pdfWorkerUrl from 'pdfjs-dist/build/pdf.worker.mjs?url';
|
||||||
import panzoom, { type PanZoom } from 'panzoom';
|
import panzoom, { type PanZoom } from 'panzoom';
|
||||||
import Spinner from './Spinner.svelte';
|
import Spinner from './Spinner.svelte';
|
||||||
import Tooltip from './Tooltip.svelte';
|
|
||||||
import Reset from '../icons/Reset.svelte';
|
|
||||||
|
|
||||||
const i18n = getContext('i18n');
|
|
||||||
|
|
||||||
export let url: string | null = null;
|
export let url: string | null = null;
|
||||||
export let data: ArrayBuffer | Uint8Array | null = null;
|
export let data: ArrayBuffer | Uint8Array | null = null;
|
||||||
@@ -18,6 +14,7 @@
|
|||||||
let error = '';
|
let error = '';
|
||||||
let pdfDoc: any = null;
|
let pdfDoc: any = null;
|
||||||
let pzInstance: PanZoom | null = null;
|
let pzInstance: PanZoom | null = null;
|
||||||
|
let zoomLevel = 1;
|
||||||
|
|
||||||
const initPanzoom = () => {
|
const initPanzoom = () => {
|
||||||
if (pzInstance) {
|
if (pzInstance) {
|
||||||
@@ -27,15 +24,50 @@
|
|||||||
pzInstance = panzoom(sceneElement, {
|
pzInstance = panzoom(sceneElement, {
|
||||||
bounds: true,
|
bounds: true,
|
||||||
boundsPadding: 0.1,
|
boundsPadding: 0.1,
|
||||||
zoomSpeed: 0.065
|
zoomSpeed: 0.065,
|
||||||
|
beforeWheel: (e) => {
|
||||||
|
// Only zoom on pinch (ctrlKey / metaKey); let normal scroll pass through
|
||||||
|
if (!e.ctrlKey && !e.metaKey) {
|
||||||
|
return true; // returning true cancels the panzoom wheel handling
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
beforeMouseDown: (e) => {
|
||||||
|
// Only allow drag-to-pan when zoomed in (not at default scale)
|
||||||
|
const transform = pzInstance?.getTransform();
|
||||||
|
if (transform && Math.abs(transform.scale - 1) < 0.01) {
|
||||||
|
return true; // cancel panzoom mouse handling at 1x — allow text selection / normal interaction
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pzInstance.on('zoom', () => {
|
||||||
|
zoomLevel = pzInstance?.getTransform()?.scale ?? 1;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const resetView = () => {
|
const zoomIn = () => {
|
||||||
|
if (!pzInstance || !outerContainer) return;
|
||||||
|
const cx = outerContainer.clientWidth / 2;
|
||||||
|
const cy = outerContainer.clientHeight / 2;
|
||||||
|
pzInstance.zoomTo(cx, cy, 1.25); // +25%
|
||||||
|
zoomLevel = pzInstance.getTransform().scale;
|
||||||
|
};
|
||||||
|
|
||||||
|
const zoomOut = () => {
|
||||||
|
if (!pzInstance || !outerContainer) return;
|
||||||
|
const cx = outerContainer.clientWidth / 2;
|
||||||
|
const cy = outerContainer.clientHeight / 2;
|
||||||
|
pzInstance.zoomTo(cx, cy, 0.8); // -20% (inverse of 1.25)
|
||||||
|
zoomLevel = pzInstance.getTransform().scale;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resetView = () => {
|
||||||
if (pzInstance) {
|
if (pzInstance) {
|
||||||
pzInstance.moveTo(0, 0);
|
pzInstance.moveTo(0, 0);
|
||||||
pzInstance.zoomAbs(0, 0, 1);
|
pzInstance.zoomAbs(0, 0, 1);
|
||||||
|
zoomLevel = 1;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,20 +77,24 @@
|
|||||||
// Clear previous canvases
|
// Clear previous canvases
|
||||||
sceneElement.innerHTML = '';
|
sceneElement.innerHTML = '';
|
||||||
|
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
|
||||||
for (let i = 1; i <= pdfDoc.numPages; i++) {
|
for (let i = 1; i <= pdfDoc.numPages; i++) {
|
||||||
const page = await pdfDoc.getPage(i);
|
const page = await pdfDoc.getPage(i);
|
||||||
const viewport = page.getViewport({ scale: 1 });
|
const viewport = page.getViewport({ scale: 1 });
|
||||||
|
|
||||||
// Scale to fit container width
|
// Scale to fit container width
|
||||||
const containerWidth = outerContainer?.clientWidth || 800;
|
const containerWidth = outerContainer?.clientWidth || 800;
|
||||||
const scale = containerWidth / viewport.width;
|
const cssScale = containerWidth / viewport.width;
|
||||||
const scaledViewport = page.getViewport({ scale });
|
const renderScale = cssScale * dpr;
|
||||||
|
const scaledViewport = page.getViewport({ scale: renderScale });
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = scaledViewport.width;
|
canvas.width = scaledViewport.width;
|
||||||
canvas.height = scaledViewport.height;
|
canvas.height = scaledViewport.height;
|
||||||
canvas.style.width = '100%';
|
// CSS size stays at the CSS-pixel dimensions for layout
|
||||||
canvas.style.height = 'auto';
|
canvas.style.width = `${Math.round(cssScale * viewport.width)}px`;
|
||||||
|
canvas.style.height = `${Math.round(cssScale * viewport.height)}px`;
|
||||||
canvas.style.display = 'block';
|
canvas.style.display = 'block';
|
||||||
|
|
||||||
if (i > 1) {
|
if (i > 1) {
|
||||||
@@ -119,31 +155,68 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="relative overflow-hidden {className}" bind:this={outerContainer}>
|
<div class="relative {className}">
|
||||||
<div bind:this={sceneElement} class="w-full">
|
<div class="overflow-y-auto h-full" bind:this={outerContainer}>
|
||||||
{#if loading}
|
<div bind:this={sceneElement} class="w-full">
|
||||||
<div class="flex items-center justify-center h-full">
|
{#if loading}
|
||||||
<Spinner className="size-5" />
|
<div class="flex items-center justify-center h-full">
|
||||||
</div>
|
<Spinner className="size-5" />
|
||||||
{/if}
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
{#if error}
|
{#if error}
|
||||||
<div class="flex items-center justify-center h-full text-sm text-red-500">
|
<div class="flex items-center justify-center h-full text-sm text-red-500">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if !loading && !error && pdfDoc}
|
{#if !loading && !error && pdfDoc}
|
||||||
<div class="absolute top-2 right-2 z-10">
|
<div
|
||||||
<Tooltip content={$i18n.t('Reset view')}>
|
class="absolute bottom-3 left-1/2 -translate-x-1/2 z-10 flex items-center gap-0.5 rounded-lg bg-white/90 dark:bg-gray-850/90 backdrop-blur-sm shadow-lg border border-gray-200/60 dark:border-gray-700/60 px-1 py-0.5"
|
||||||
<button
|
>
|
||||||
class="p-1.5 rounded-lg bg-white/80 dark:bg-gray-850/80 backdrop-blur-sm shadow-sm hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400"
|
<button
|
||||||
on:click={resetView}
|
class="p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400"
|
||||||
|
on:click={zoomOut}
|
||||||
|
aria-label="Zoom out"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-3.5"
|
||||||
>
|
>
|
||||||
<Reset className="size-4" />
|
<path
|
||||||
</button>
|
fill-rule="evenodd"
|
||||||
</Tooltip>
|
d="M4 10a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H4.75A.75.75 0 0 1 4 10Z"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="px-1.5 py-1 min-w-[3rem] text-center text-[11px] font-medium text-gray-500 dark:text-gray-400 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition tabular-nums"
|
||||||
|
on:click={resetView}
|
||||||
|
aria-label="Reset zoom"
|
||||||
|
>
|
||||||
|
{Math.round(zoomLevel * 100)}%
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="p-1.5 rounded-md hover:bg-gray-100 dark:hover:bg-gray-800 transition text-gray-500 dark:text-gray-400"
|
||||||
|
on:click={zoomIn}
|
||||||
|
aria-label="Zoom in"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 20 20"
|
||||||
|
fill="currentColor"
|
||||||
|
class="size-3.5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M10.75 4.75a.75.75 0 0 0-1.5 0v4.5h-4.5a.75.75 0 0 0 0 1.5h4.5v4.5a.75.75 0 0 0 1.5 0v-4.5h4.5a.75.75 0 0 0 0-1.5h-4.5v-4.5Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user