From 1c7fe6394807a09e2e353bc6e5e91ebed8e74604 Mon Sep 17 00:00:00 2001 From: Vladimir Poluliashenko Date: Mon, 8 Jun 2026 12:21:04 +0100 Subject: [PATCH 1/9] feat(Viewer): add zoom and pan to image viewer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Install panzoom library (9.4.4, 6 kB) and apply it to the image viewer wrapper in ViewerHandlerImages.vue: - Scroll wheel to zoom in/out (1× – 8×) - Drag to pan when zoomed in - Double-click to zoom in 3× at cursor, double-click again to reset - Auto-center image when zoom returns to 1× - Pan blocked at minimum zoom to prevent accidental shifts - Cursor changes: zoom-in at 1×, grab/grabbing when zoomed Fixes #1535 Signed-off-by: Vladimir Poluliashenko Assisted-by: ClaudeCode:claude-sonnet-4-6 --- package-lock.json | 72 +++++++++ package.json | 1 + .../renderer/Viewer/ViewerHandlerImages.vue | 137 ++++++++++++++++-- 3 files changed, 200 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 306ade85a..4c0386397 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "electron-squirrel-startup": "^1.0.1", "howler": "^2.2.4", "mri": "^1.2.0", + "panzoom": "^9.4.4", "pinia": "^4.0.2", "semver": "^7.8.5", "unzip-crx-3": "^0.2.0", @@ -5144,6 +5145,15 @@ "dev": true, "license": "MIT" }, + "node_modules/amator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/amator/-/amator-1.1.0.tgz", + "integrity": "sha512-V5+aH8pe+Z3u/UG3L3pG3BaFQGXAyXHVQDroRwjPHdh08bcUEchAVsU1MCuJSCaU5o60wTK6KaE6te5memzgYw==", + "license": "MIT", + "dependencies": { + "bezier-easing": "^2.0.3" + } + }, "node_modules/ansi-escapes": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", @@ -5449,6 +5459,12 @@ "dev": true, "license": "MIT" }, + "node_modules/bezier-easing": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", + "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==", + "license": "MIT" + }, "node_modules/big.js": { "version": "5.2.2", "dev": true, @@ -12012,6 +12028,12 @@ "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==", "license": "MIT" }, + "node_modules/ngraph.events": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz", + "integrity": "sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw==", + "license": "BSD-3-Clause" + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -12646,6 +12668,17 @@ "version": "1.0.11", "license": "(MIT AND Zlib)" }, + "node_modules/panzoom": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/panzoom/-/panzoom-9.4.4.tgz", + "integrity": "sha512-r1KfkNZvsBw59IPq7Yy+GWZnZE1YCG/t7aG6caSkij/TBqdxTzmxNTm/lHf3h6qlVMFisIGIO+lgS2Ym23PIoA==", + "license": "MIT", + "dependencies": { + "amator": "^1.1.0", + "ngraph.events": "^1.2.2", + "wheel": "^1.0.0" + } + }, "node_modules/param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -16543,6 +16576,12 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/wheel": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wheel/-/wheel-1.0.0.tgz", + "integrity": "sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA==", + "license": "MIT" + }, "node_modules/which": { "version": "2.0.2", "dev": true, @@ -20051,6 +20090,14 @@ "integrity": "sha512-I8FjmltrfnDFoZedi5CG8DghVYNhzb/Ijluz7tCSJH0xpd0484Kowhbb1XDYOxfJpU1p5wnM2X54dA+IfGyD1g==", "dev": true }, + "amator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/amator/-/amator-1.1.0.tgz", + "integrity": "sha512-V5+aH8pe+Z3u/UG3L3pG3BaFQGXAyXHVQDroRwjPHdh08bcUEchAVsU1MCuJSCaU5o60wTK6KaE6te5memzgYw==", + "requires": { + "bezier-easing": "^2.0.3" + } + }, "ansi-escapes": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-5.0.0.tgz", @@ -20250,6 +20297,11 @@ "version": "0.6.1", "dev": true }, + "bezier-easing": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/bezier-easing/-/bezier-easing-2.1.0.tgz", + "integrity": "sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==" + }, "big.js": { "version": "5.2.2", "dev": true @@ -24596,6 +24648,11 @@ "resolved": "https://registry.npmjs.org/nested-property/-/nested-property-4.0.0.tgz", "integrity": "sha512-yFehXNWRs4cM0+dz7QxCd06hTbWbSkV0ISsqBfkntU6TOY4Qm3Q88fRRLOddkGh2Qq6dZvnKVAahfhjcUvLnyA==" }, + "ngraph.events": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ngraph.events/-/ngraph.events-1.4.0.tgz", + "integrity": "sha512-NeDGI4DSyjBNBRtA86222JoYietsmCXbs8CEB0dZ51Xeh4lhVl1y3wpWLumczvnha8sFQIW4E0vvVWwgmX2mGw==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -25008,6 +25065,16 @@ "pako": { "version": "1.0.11" }, + "panzoom": { + "version": "9.4.4", + "resolved": "https://registry.npmjs.org/panzoom/-/panzoom-9.4.4.tgz", + "integrity": "sha512-r1KfkNZvsBw59IPq7Yy+GWZnZE1YCG/t7aG6caSkij/TBqdxTzmxNTm/lHf3h6qlVMFisIGIO+lgS2Ym23PIoA==", + "requires": { + "amator": "^1.1.0", + "ngraph.events": "^1.2.2", + "wheel": "^1.0.0" + } + }, "param-case": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", @@ -27603,6 +27670,11 @@ "webidl-conversions": "^3.0.0" } }, + "wheel": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wheel/-/wheel-1.0.0.tgz", + "integrity": "sha512-XiCMHibOiqalCQ+BaNSwRoZ9FDTAvOsXxGHXChBugewDj7HC8VBIER71dEOiRH1fSdLbRCQzngKTSiZ06ZQzeA==" + }, "which": { "version": "2.0.2", "dev": true, diff --git a/package.json b/package.json index 813255d69..334715110 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "electron-squirrel-startup": "^1.0.1", "howler": "^2.2.4", "mri": "^1.2.0", + "panzoom": "^9.4.4", "pinia": "^4.0.2", "semver": "^7.8.5", "unzip-crx-3": "^0.2.0", diff --git a/src/talk/renderer/Viewer/ViewerHandlerImages.vue b/src/talk/renderer/Viewer/ViewerHandlerImages.vue index 2f3762d58..429221cf0 100644 --- a/src/talk/renderer/Viewer/ViewerHandlerImages.vue +++ b/src/talk/renderer/Viewer/ViewerHandlerImages.vue @@ -4,7 +4,8 @@ --> + + From 6683dfbf655d5f04e1823ccd844de3e21a090a3a Mon Sep 17 00:00:00 2001 From: Vladimir Poluliashenko Date: Wed, 17 Jun 2026 19:47:30 +0100 Subject: [PATCH 2/9] fix(Viewer): add JSDoc param types to image viewer functions Signed-off-by: Vladimir Poluliashenko Assisted-by: ClaudeCode:claude-sonnet-4-6 --- src/talk/renderer/Viewer/ViewerHandlerImages.vue | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/talk/renderer/Viewer/ViewerHandlerImages.vue b/src/talk/renderer/Viewer/ViewerHandlerImages.vue index 429221cf0..e836c1d89 100644 --- a/src/talk/renderer/Viewer/ViewerHandlerImages.vue +++ b/src/talk/renderer/Viewer/ViewerHandlerImages.vue @@ -68,16 +68,25 @@ function disposePanzoom() { grabbing.value = false } +/** + * @param {Function} handleLoadEnd - Callback to signal load completion + */ function onImageLoad(handleLoadEnd) { handleLoadEnd(false) initPanzoom() } +/** + * @param {Function} handleLoadEnd - Callback to signal load error + */ function onImageError(handleLoadEnd) { handleLoadEnd(true) disposePanzoom() } +/** + * @param {MouseEvent} event - The double-click event + */ function onDoubleClick(event) { if (!instance.value) { return From d5aedc88c6d7a65b83efe35324dfa16c92b3e0d4 Mon Sep 17 00:00:00 2001 From: Vladimir Poluliashenko Date: Wed, 17 Jun 2026 20:20:23 +0100 Subject: [PATCH 3/9] fix(Viewer): block pan at minimum zoom; apply cursor class to image element Signed-off-by: Vladimir Poluliashenko Assisted-by: ClaudeCode:claude-sonnet-4-6 --- src/talk/renderer/Viewer/ViewerHandlerImages.vue | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/talk/renderer/Viewer/ViewerHandlerImages.vue b/src/talk/renderer/Viewer/ViewerHandlerImages.vue index e836c1d89..95e385dd1 100644 --- a/src/talk/renderer/Viewer/ViewerHandlerImages.vue +++ b/src/talk/renderer/Viewer/ViewerHandlerImages.vue @@ -41,7 +41,10 @@ function initPanzoom() { minZoom: ZOOM_MIN, maxZoom: ZOOM_MAX, bounds: true, - boundsPadding: 0.1, + boundsPadding: 0, + beforeMouseDown() { + return scale.value <= ZOOM_MIN + }, }) instance.value.on('zoom', (pz) => { const transform = pz.getTransform() @@ -111,10 +114,11 @@ onBeforeUnmount(disposePanzoom)