Fix image viewer: broken pan, missing wheel zoom, download navigates instead of saving#926
Conversation
Co-authored-by: PythonSmall-Q <106425289+PythonSmall-Q@users.noreply.github.com>
Co-authored-by: PythonSmall-Q <106425289+PythonSmall-Q@users.noreply.github.com>
Co-authored-by: PythonSmall-Q <106425289+PythonSmall-Q@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Fixes usability bugs in the ImageEnlarger modal viewer by improving interaction hit areas, restoring wheel zoom, adding image-to-image navigation, and making “保存图片” reliably trigger a download.
Changes:
- Move drag/pan handling to the modal container and add mouse/touch pan + swipe navigation.
- Add wheel zoom (clamped) and prev/next buttons + arrow-key navigation.
- Replace direct-link download with
GM_xmlhttpRequestblob download + fallback to opening in a new tab.
Comments suppressed due to low confidence (1)
XMOJ.user.js:5968
PanX/PanYpersist when zooming back down toCurrentZoom === 1via the wheel/zoom-out button, leaving the image offset while panning is disabled (since drag is gated onCurrentZoom > 1). Consider resettingPanX/PanYto 0 whenever zoom is clamped back to 1 (or below) so the image recenters when exiting zoom mode.
ModalContent.addEventListener("wheel", (e) => {
e.preventDefault();
let ZoomDelta = e.deltaY > 0 ? -ZoomStep : ZoomStep;
CurrentZoom = Math.max(MinZoom, Math.min(MaxZoom, CurrentZoom + ZoomDelta));
UpdateImageSize();
}, { passive: false });
// Navigation button clicks
PrevBtn.addEventListener("click", (e) => {
e.stopPropagation();
NavigateTo(CurrentImageIndex - 1);
});
NextBtn.addEventListener("click", (e) => {
e.stopPropagation();
NavigateTo(CurrentImageIndex + 1);
});
// Zoom controls
ZoomInBtn.addEventListener("click", () => {
CurrentZoom = Math.min(CurrentZoom + ZoomStep, MaxZoom);
UpdateImageSize();
});
ZoomOutBtn.addEventListener("click", () => {
CurrentZoom = Math.max(CurrentZoom - ZoomStep, MinZoom);
UpdateImageSize();
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| let UpdateImageSize = () => { | ||
| ModalImage.style.transform = `scale(${CurrentZoom})`; | ||
| ModalImage.style.transition = "transform 0.2s ease"; | ||
| ModalImage.style.transform = `translate(${PanX}px, ${PanY}px) scale(${CurrentZoom})`; |
There was a problem hiding this comment.
transform = translate(...) scale(...) scales the translation as zoom changes, so panning speed becomes multiplied by CurrentZoom (e.g., a 10px drag moves ~20px at 2x). To keep pan in screen pixels, apply transforms in the opposite order (scale then translate) or divide the translation by the zoom factor.
| ModalImage.style.transform = `translate(${PanX}px, ${PanY}px) scale(${CurrentZoom})`; | |
| ModalImage.style.transform = `scale(${CurrentZoom}) translate(${PanX}px, ${PanY}px)`; |
| let BlobUrl = URL.createObjectURL(resp.response); | ||
| let Link = document.createElement("a"); | ||
| Link.href = BlobUrl; | ||
| Link.download = filename; | ||
| document.body.appendChild(Link); | ||
| Link.click(); | ||
| document.body.removeChild(Link); | ||
| setTimeout(() => URL.revokeObjectURL(BlobUrl), 100); |
There was a problem hiding this comment.
GM_xmlhttpRequest calls onload even for non-2xx HTTP responses. The current implementation will still create a blob and prompt a download for 404/500 HTML error pages. Please check resp.status (and that resp.response is present) before downloading, and fall back to the onerror/new-tab behavior when the request fails.
| let BlobUrl = URL.createObjectURL(resp.response); | |
| let Link = document.createElement("a"); | |
| Link.href = BlobUrl; | |
| Link.download = filename; | |
| document.body.appendChild(Link); | |
| Link.click(); | |
| document.body.removeChild(Link); | |
| setTimeout(() => URL.revokeObjectURL(BlobUrl), 100); | |
| if (resp && resp.status >= 200 && resp.status < 300 && resp.response) { | |
| let BlobUrl = URL.createObjectURL(resp.response); | |
| let Link = document.createElement("a"); | |
| Link.href = BlobUrl; | |
| Link.download = filename; | |
| document.body.appendChild(Link); | |
| Link.click(); | |
| document.body.removeChild(Link); | |
| setTimeout(() => URL.revokeObjectURL(BlobUrl), 100); | |
| } else { | |
| let Link = document.createElement("a"); | |
| Link.href = src; | |
| Link.download = filename; | |
| Link.target = "_blank"; | |
| document.body.appendChild(Link); | |
| Link.click(); | |
| document.body.removeChild(Link); | |
| } |
| ModalContent.addEventListener("mousedown", (e) => { | ||
| if (CurrentZoom <= 1) return; | ||
| if (e.target.tagName.toUpperCase() === "BUTTON") return; | ||
| IsDragging = true; | ||
| DragStartX = e.clientX; | ||
| DragStartY = e.clientY; |
There was a problem hiding this comment.
The drag handler starts panning on any mouse button; right/middle click can unintentionally set IsDragging = true and suppress expected behavior. Gate drag start to the primary button only (e.g., e.button === 0).
Three bugs in the
ImageEnlargerimage viewer modal: dragging only worked within the pre-transform layout bounds of the image element, mouse wheel had no effect, and "保存图片" navigated to the image URL instead of triggering a download.Changes
Drag/pan area — moved
mousedownfromModalImagetoModalContent.transform: scale()doesn't resize the element's hit area, so dragging outside the original layout bounds was silently dropped. Attaching to the full-screen container fixes this. Guard added to skip drag onBUTTONtargets (nav arrows). Cursor style now synced on bothModalImageandModalContent.Mouse wheel zoom — added
wheellistener onModalContent(passive: falseto suppress scroll). Scroll up = zoom in, scroll down = zoom out, clamped to[MinZoom, MaxZoom].Download — replaced
fetch→saveAs(FileSaver) withGM_xmlhttpRequest→URL.createObjectURL→<a download>click.GM_xmlhttpRequestbypasses CORS;downloadon ablob:URL is always same-origin so the save dialog reliably triggers. Blob URL revoked after 100 ms. Error fallback addstarget="_blank"to avoid same-tab navigation.✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.
Summary by cubic
Add a modal image viewer with on-screen prev/next controls, arrow-key and swipe navigation, drag-to-pan when zoomed, mouse wheel zoom, and a download that saves the file without leaving the page.
GM_xmlhttpRequest, save via blob URL with fallback to a new tab (no redirect).Written for commit 6f310f3. Summary will update on new commits.