From 2dd0985ea9e815e9b38f8a4e0ffe3edb45fe86a0 Mon Sep 17 00:00:00 2001 From: Yuri Karamian Date: Mon, 3 Aug 2026 16:56:38 +0200 Subject: [PATCH] Add Android image pinch zoom (Closes #115) --- src/lib/components/Editor.svelte | 39 ++- src/lib/components/ImageViewer.svelte | 385 ++++++++++++++++++++++++++ 2 files changed, 417 insertions(+), 7 deletions(-) create mode 100644 src/lib/components/ImageViewer.svelte diff --git a/src/lib/components/Editor.svelte b/src/lib/components/Editor.svelte index 8c3e742..49b5cc0 100644 --- a/src/lib/components/Editor.svelte +++ b/src/lib/components/Editor.svelte @@ -55,6 +55,7 @@ import { relativePath } from '$lib/utils/paths'; import GraphView from './GraphView.svelte'; import TagSuggestInput from './TagSuggestInput.svelte'; + import ImageViewer from './ImageViewer.svelte'; import { isMobile, isAndroid } from '$lib/platform'; import ResizeHandle from './ResizeHandle.svelte'; @@ -471,7 +472,8 @@ let tableContextMenu = $state<{ x: number; y: number; hasStyling: boolean } | null>(null); let tablePickerOpen = $state(false); let tablePickerHover = $state({ rows: 0, cols: 0 }); - let imageToolbar = $state<{ pos: number; x: number; y: number; size: string; src: string } | null>(null); + let imageToolbar = $state<{ pos: number; x: number; y: number; size: string; src: string; alt: string } | null>(null); + let imageViewer = $state<{ src: string; alt: string } | null>(null); let copyToast = $state<'copying' | 'done' | null>(null); $effect(() => { @@ -4828,11 +4830,12 @@ const node = editor.state.doc.nodeAt(pos); const currentSize = node?.attrs.size || 'full'; const imgSrc = node?.attrs.src || (target as HTMLImageElement).src || ''; - const toolbarW = isMobile ? 130 : 250; - const toolbarH = 38; - const x = Math.min(event.clientX, window.innerWidth - toolbarW - 8); - const y = Math.min(event.clientY, window.innerHeight - toolbarH - 8); - imageToolbar = { pos, x, y, size: currentSize, src: imgSrc }; + const imgAlt = node?.attrs.alt || (target as HTMLImageElement).alt || ''; + const toolbarW = isAndroid ? 188 : isMobile ? 130 : 250; + const toolbarH = isAndroid ? 48 : 38; + const x = Math.max(8, Math.min(event.clientX, window.innerWidth - toolbarW - 8)); + const y = Math.max(8, Math.min(event.clientY, window.innerHeight - toolbarH - 8)); + imageToolbar = { pos, x, y, size: currentSize, src: imgSrc, alt: imgAlt }; // Move cursor after the image to clear ProseMirror's node selection highlight const afterPos = pos + (node?.nodeSize || 1); editor.chain().setTextSelection(afterPos).run(); @@ -4842,6 +4845,12 @@ imageToolbar = null; } + function openImageViewer() { + if (!imageToolbar) return; + imageViewer = { src: imageToolbar.src, alt: imageToolbar.alt }; + imageToolbar = null; + } + function setImageSize(size: string) { if (!editor || !imageToolbar) return; const { pos } = imageToolbar; @@ -7345,10 +7354,16 @@
(imageToolbar = null)}> -
e.stopPropagation()}> +
e.stopPropagation()}> + {#if isAndroid} + + + {/if} {#if !isMobile && !imageToolbar.src.startsWith('imgproxy:') && !imageToolbar.src.startsWith('http://imgproxy.localhost')}
{/if} +{#if imageViewer} + (imageViewer = null)} /> +{/if} + {#if copyToast}
{#if copyToast === 'copying'} @@ -10003,6 +10022,12 @@ display: block; } + .img-toolbar.mobile-viewer-toolbar button { + min-width: 40px; + min-height: 40px; + padding: 8px 12px; + } + .copy-toast { position: fixed; bottom: 24px; diff --git a/src/lib/components/ImageViewer.svelte b/src/lib/components/ImageViewer.svelte new file mode 100644 index 0000000..3a903ea --- /dev/null +++ b/src/lib/components/ImageViewer.svelte @@ -0,0 +1,385 @@ + + + + +