From 43f46b545f3fa9ab0959ca4e32ddef9fb461cb89 Mon Sep 17 00:00:00 2001 From: Vladimir Poluliashenko Date: Mon, 8 Jun 2026 12:21:04 +0100 Subject: [PATCH 1/8] 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 f5156c24b..6795fbd05 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "electron-squirrel-startup": "^1.0.1", "howler": "^2.2.4", "mri": "^1.2.0", + "panzoom": "^9.4.4", "pinia": "^3.0.4", "semver": "^7.7.4", "unzip-crx-3": "^0.2.0", @@ -5889,6 +5890,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", @@ -6218,6 +6228,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, @@ -13640,6 +13656,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", @@ -14300,6 +14322,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", @@ -18800,6 +18833,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, @@ -22979,6 +23018,14 @@ "integrity": "sha512-d9dYqZTS90WLiU0I5c6DHj/HcKkF8ZyGN3G5x8wSbslulz70KOxaqCT0hQCo9KOyhVqzqGojvNdJXoTumZOtcw==", "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", @@ -23199,6 +23246,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 @@ -28224,6 +28276,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", @@ -28652,6 +28709,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", @@ -31684,6 +31751,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 cf05ab02f..376c0729a 100644 --- a/package.json +++ b/package.json @@ -69,6 +69,7 @@ "electron-squirrel-startup": "^1.0.1", "howler": "^2.2.4", "mri": "^1.2.0", + "panzoom": "^9.4.4", "pinia": "^3.0.4", "semver": "^7.7.4", "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 4753e49028baf7606d96f01c08940db1de1b539d Mon Sep 17 00:00:00 2001 From: Vladimir Poluliashenko Date: Wed, 17 Jun 2026 19:47:30 +0100 Subject: [PATCH 2/8] 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 28ce6faae9224dfa4eb88af504e9086d0bdaa7f6 Mon Sep 17 00:00:00 2001 From: Vladimir Poluliashenko Date: Wed, 17 Jun 2026 20:20:23 +0100 Subject: [PATCH 3/8] 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)