From 5c6daab8ad7ba732cfb5eab1abb3c05e5ec3458f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:20:52 +0000 Subject: [PATCH 1/4] Initial plan From b5945cfd2ffab7ed43818570f050ce8dd046b11a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:27:11 +0000 Subject: [PATCH 2/4] Plan: add pan support to ImageEnlarger Co-authored-by: PythonSmall-Q <106425289+PythonSmall-Q@users.noreply.github.com> --- XMOJ.user.js | 160 +++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 148 insertions(+), 12 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index f4459add..f0b4a62b 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -5613,6 +5613,7 @@ int main() align-items: center; justify-content: center; overflow: auto; + position: relative; } .xmoj-image-modal-image { @@ -5662,11 +5663,46 @@ int main() font-weight: bold; cursor: pointer; transition: color 0.2s ease; + z-index: 1; } .xmoj-image-modal-close:hover { color: #ccc; } + + .xmoj-image-modal-nav { + position: absolute; + top: 50%; + transform: translateY(-50%); + background: rgba(0, 0, 0, 0.5); + color: white; + border: none; + padding: 20px 12px; + cursor: pointer; + font-size: 28px; + transition: background-color 0.2s ease; + user-select: none; + -webkit-user-select: none; + } + + .xmoj-image-modal-nav:hover { + background: rgba(0, 0, 0, 0.8); + } + + .xmoj-image-modal-nav:disabled { + opacity: 0.3; + cursor: default; + } + + .xmoj-image-modal-nav-prev { + left: 0; + border-radius: 0 4px 4px 0; + } + + .xmoj-image-modal-nav-next { + right: 0; + border-radius: 4px 0 0 4px; + } `; document.head.appendChild(EnlargerStyle); @@ -5685,6 +5721,21 @@ int main() let ModalContent = document.createElement("div"); ModalContent.className = "xmoj-image-modal-content"; + + let PrevBtn = document.createElement("button"); + PrevBtn.className = "xmoj-image-modal-nav xmoj-image-modal-nav-prev"; + PrevBtn.type = "button"; + PrevBtn.setAttribute("aria-label", "上一张"); + PrevBtn.innerHTML = "❮"; + ModalContent.appendChild(PrevBtn); + + let NextBtn = document.createElement("button"); + NextBtn.className = "xmoj-image-modal-nav xmoj-image-modal-nav-next"; + NextBtn.type = "button"; + NextBtn.setAttribute("aria-label", "下一张"); + NextBtn.innerHTML = "❯"; + ModalContent.appendChild(NextBtn); + let ModalImage = document.createElement("img"); ModalImage.className = "xmoj-image-modal-image"; ModalContent.appendChild(ModalImage); @@ -5716,11 +5767,15 @@ int main() ImageModal.appendChild(Toolbar); document.body.appendChild(ImageModal); - // Zoom level state + // Zoom level and navigation state let CurrentZoom = 1; const ZoomStep = 0.1; const MinZoom = 0.1; const MaxZoom = 5; + let ImageList = []; + let CurrentImageIndex = -1; + let TouchStartX = 0; + let TouchStartY = 0; // Function to update image size let UpdateImageSize = () => { @@ -5728,11 +5783,38 @@ int main() ModalImage.style.transition = "transform 0.2s ease"; }; + // Function to update prev/next button state + let UpdateNavButtons = () => { + let HasMultiple = ImageList.length > 1; + PrevBtn.style.display = HasMultiple ? "" : "none"; + NextBtn.style.display = HasMultiple ? "" : "none"; + PrevBtn.disabled = CurrentImageIndex <= 0; + NextBtn.disabled = CurrentImageIndex >= ImageList.length - 1; + }; + + // Function to navigate to a specific image by index + let NavigateTo = (index) => { + if (index < 0 || index >= ImageList.length) return; + CurrentImageIndex = index; + CurrentZoom = 1; + ModalImage.src = ImageList[CurrentImageIndex]; + UpdateNavButtons(); + UpdateImageSize(); + }; + // Function to open modal - let OpenImageModal = (imgSrc) => { + let OpenImageModal = (imgElement) => { + let PreviewImages = [...document.querySelectorAll("img.xmoj-image-preview")]; + ImageList = PreviewImages.map(img => img.currentSrc || img.src).filter(src => src); + CurrentImageIndex = PreviewImages.indexOf(imgElement); + if (CurrentImageIndex === -1) { + ImageList = [(imgElement.currentSrc || imgElement.src)]; + CurrentImageIndex = 0; + } CurrentZoom = 1; - ModalImage.src = imgSrc; + ModalImage.src = ImageList[CurrentImageIndex]; ImageModal.classList.add("show"); + UpdateNavButtons(); UpdateImageSize(); }; @@ -5760,8 +5842,44 @@ int main() ZoomInBtn.click(); } else if (e.key === "-") { ZoomOutBtn.click(); + } else if (e.key === "ArrowLeft") { + NavigateTo(CurrentImageIndex - 1); + } else if (e.key === "ArrowRight") { + NavigateTo(CurrentImageIndex + 1); + } + } + }); + + // Touch swipe for image navigation + ModalContent.addEventListener("touchstart", (e) => { + TouchStartX = e.changedTouches[0].screenX; + TouchStartY = e.changedTouches[0].screenY; + }, { passive: true }); + + ModalContent.addEventListener("touchend", (e) => { + let TouchEndX = e.changedTouches[0].screenX; + let TouchEndY = e.changedTouches[0].screenY; + let DeltaX = TouchEndX - TouchStartX; + let DeltaY = TouchEndY - TouchStartY; + const SwipeThreshold = 50; + if (Math.abs(DeltaX) > SwipeThreshold && Math.abs(DeltaX) > Math.abs(DeltaY)) { + if (DeltaX < 0) { + NavigateTo(CurrentImageIndex + 1); + } else { + NavigateTo(CurrentImageIndex - 1); } } + }, { passive: true }); + + // Navigation button clicks + PrevBtn.addEventListener("click", (e) => { + e.stopPropagation(); + NavigateTo(CurrentImageIndex - 1); + }); + + NextBtn.addEventListener("click", (e) => { + e.stopPropagation(); + NavigateTo(CurrentImageIndex + 1); }); // Zoom controls @@ -5780,14 +5898,32 @@ int main() UpdateImageSize(); }); - // Save/Download image - SaveBtn.addEventListener("click", () => { - let Link = document.createElement("a"); - Link.href = ModalImage.src; - Link.download = ModalImage.src.split("/").pop() || "image.png"; - document.body.appendChild(Link); - Link.click(); - document.body.removeChild(Link); + // Save/Download image using fetch to trigger actual download instead of redirecting + SaveBtn.addEventListener("click", async () => { + let src = ModalImage.src; + let filename = src.split("/").pop().split("?")[0] || "image.png"; + try { + let response = await fetch(src); + let blob = await response.blob(); + saveAs(blob, filename); + } catch (e) { + GM_xmlhttpRequest({ + method: "GET", + url: src, + responseType: "blob", + onload: (resp) => { + saveAs(resp.response, filename); + }, + onerror: () => { + let Link = document.createElement("a"); + Link.href = src; + Link.download = filename; + document.body.appendChild(Link); + Link.click(); + document.body.removeChild(Link); + } + }); + } }); // Apply to all images on the page @@ -5805,7 +5941,7 @@ int main() } img.addEventListener("click", (e) => { e.stopPropagation(); - OpenImageModal(effectiveSrc); + OpenImageModal(img); }); } }; From 279dada58511ef0d1e44a1a994f177c34087a1cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:29:40 +0000 Subject: [PATCH 3/4] Add drag-to-pan for zoomed images in ImageEnlarger modal Co-authored-by: PythonSmall-Q <106425289+PythonSmall-Q@users.noreply.github.com> --- XMOJ.user.js | 85 +++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index f0b4a62b..599daeef 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -5612,7 +5612,7 @@ int main() display: flex; align-items: center; justify-content: center; - overflow: auto; + overflow: hidden; position: relative; } @@ -5774,13 +5774,24 @@ int main() const MaxZoom = 5; let ImageList = []; let CurrentImageIndex = -1; + let PanX = 0; + let PanY = 0; + let IsDragging = false; + let DragStartX = 0; + let DragStartY = 0; + let DragStartPanX = 0; + let DragStartPanY = 0; + let IsTouchPanning = false; let TouchStartX = 0; let TouchStartY = 0; + let TouchPanStartPanX = 0; + let TouchPanStartPanY = 0; - // Function to update image size + // Function to update image transform (zoom + pan) let UpdateImageSize = () => { - ModalImage.style.transform = `scale(${CurrentZoom})`; - ModalImage.style.transition = "transform 0.2s ease"; + ModalImage.style.transform = `translate(${PanX}px, ${PanY}px) scale(${CurrentZoom})`; + ModalImage.style.transition = IsDragging ? "none" : "transform 0.2s ease"; + ModalImage.style.cursor = CurrentZoom > 1 ? "grab" : ""; }; // Function to update prev/next button state @@ -5797,6 +5808,8 @@ int main() if (index < 0 || index >= ImageList.length) return; CurrentImageIndex = index; CurrentZoom = 1; + PanX = 0; + PanY = 0; ModalImage.src = ImageList[CurrentImageIndex]; UpdateNavButtons(); UpdateImageSize(); @@ -5812,6 +5825,8 @@ int main() CurrentImageIndex = 0; } CurrentZoom = 1; + PanX = 0; + PanY = 0; ModalImage.src = ImageList[CurrentImageIndex]; ImageModal.classList.add("show"); UpdateNavButtons(); @@ -5850,15 +5865,35 @@ int main() } }); - // Touch swipe for image navigation + // Touch events: pan when zoomed, swipe to navigate when at zoom level 1 ModalContent.addEventListener("touchstart", (e) => { - TouchStartX = e.changedTouches[0].screenX; - TouchStartY = e.changedTouches[0].screenY; + if (e.touches.length !== 1) return; + TouchStartX = e.touches[0].clientX; + TouchStartY = e.touches[0].clientY; + if (CurrentZoom > 1) { + IsTouchPanning = true; + TouchPanStartPanX = PanX; + TouchPanStartPanY = PanY; + } else { + IsTouchPanning = false; + } }, { passive: true }); + ModalContent.addEventListener("touchmove", (e) => { + if (!IsTouchPanning || e.touches.length !== 1) return; + PanX = TouchPanStartPanX + (e.touches[0].clientX - TouchStartX); + PanY = TouchPanStartPanY + (e.touches[0].clientY - TouchStartY); + UpdateImageSize(); + e.preventDefault(); + }, { passive: false }); + ModalContent.addEventListener("touchend", (e) => { - let TouchEndX = e.changedTouches[0].screenX; - let TouchEndY = e.changedTouches[0].screenY; + if (IsTouchPanning) { + IsTouchPanning = false; + return; + } + let TouchEndX = e.changedTouches[0].clientX; + let TouchEndY = e.changedTouches[0].clientY; let DeltaX = TouchEndX - TouchStartX; let DeltaY = TouchEndY - TouchStartY; const SwipeThreshold = 50; @@ -5871,6 +5906,32 @@ int main() } }, { passive: true }); + // Mouse drag to pan when zoomed + ModalImage.addEventListener("mousedown", (e) => { + if (CurrentZoom <= 1) return; + IsDragging = true; + DragStartX = e.clientX; + DragStartY = e.clientY; + DragStartPanX = PanX; + DragStartPanY = PanY; + ModalImage.style.cursor = "grabbing"; + e.preventDefault(); + }); + + document.addEventListener("mousemove", (e) => { + if (!IsDragging) return; + PanX = DragStartPanX + (e.clientX - DragStartX); + PanY = DragStartPanY + (e.clientY - DragStartY); + UpdateImageSize(); + }); + + document.addEventListener("mouseup", () => { + if (IsDragging) { + IsDragging = false; + ModalImage.style.cursor = CurrentZoom > 1 ? "grab" : ""; + } + }); + // Navigation button clicks PrevBtn.addEventListener("click", (e) => { e.stopPropagation(); @@ -5895,15 +5956,19 @@ int main() ResetZoomBtn.addEventListener("click", () => { CurrentZoom = 1; + PanX = 0; + PanY = 0; UpdateImageSize(); }); // Save/Download image using fetch to trigger actual download instead of redirecting SaveBtn.addEventListener("click", async () => { let src = ModalImage.src; - let filename = src.split("/").pop().split("?")[0] || "image.png"; + let urlPath = src.split("?")[0]; + let filename = urlPath.split("/").pop() || "image.png"; try { let response = await fetch(src); + if (!response.ok) throw new Error("Failed to fetch image"); let blob = await response.blob(); saveAs(blob, filename); } catch (e) { From 6f310f392574e215b87df20312c0fc038e2f0242 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 8 Mar 2026 14:40:43 +0000 Subject: [PATCH 4/4] Fix image viewer: drag/pan, mouse wheel zoom, and download Co-authored-by: PythonSmall-Q <106425289+PythonSmall-Q@users.noreply.github.com> --- XMOJ.user.js | 71 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 43 insertions(+), 28 deletions(-) diff --git a/XMOJ.user.js b/XMOJ.user.js index 599daeef..49538eed 100644 --- a/XMOJ.user.js +++ b/XMOJ.user.js @@ -5791,7 +5791,9 @@ int main() let UpdateImageSize = () => { ModalImage.style.transform = `translate(${PanX}px, ${PanY}px) scale(${CurrentZoom})`; ModalImage.style.transition = IsDragging ? "none" : "transform 0.2s ease"; - ModalImage.style.cursor = CurrentZoom > 1 ? "grab" : ""; + let CursorStyle = CurrentZoom > 1 ? "grab" : ""; + ModalImage.style.cursor = CursorStyle; + ModalContent.style.cursor = CursorStyle; }; // Function to update prev/next button state @@ -5907,14 +5909,16 @@ int main() }, { passive: true }); // Mouse drag to pan when zoomed - ModalImage.addEventListener("mousedown", (e) => { + ModalContent.addEventListener("mousedown", (e) => { if (CurrentZoom <= 1) return; + if (e.target.tagName.toUpperCase() === "BUTTON") return; IsDragging = true; DragStartX = e.clientX; DragStartY = e.clientY; DragStartPanX = PanX; DragStartPanY = PanY; ModalImage.style.cursor = "grabbing"; + ModalContent.style.cursor = "grabbing"; e.preventDefault(); }); @@ -5928,10 +5932,20 @@ int main() document.addEventListener("mouseup", () => { if (IsDragging) { IsDragging = false; - ModalImage.style.cursor = CurrentZoom > 1 ? "grab" : ""; + let CursorStyle = CurrentZoom > 1 ? "grab" : ""; + ModalImage.style.cursor = CursorStyle; + ModalContent.style.cursor = CursorStyle; } }); + // Mouse wheel to zoom in/out + 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(); @@ -5961,34 +5975,35 @@ int main() UpdateImageSize(); }); - // Save/Download image using fetch to trigger actual download instead of redirecting - SaveBtn.addEventListener("click", async () => { + // Save/Download image: fetch via GM_xmlhttpRequest to bypass CORS, then use blob URL for reliable download + SaveBtn.addEventListener("click", () => { let src = ModalImage.src; let urlPath = src.split("?")[0]; let filename = urlPath.split("/").pop() || "image.png"; - try { - let response = await fetch(src); - if (!response.ok) throw new Error("Failed to fetch image"); - let blob = await response.blob(); - saveAs(blob, filename); - } catch (e) { - GM_xmlhttpRequest({ - method: "GET", - url: src, - responseType: "blob", - onload: (resp) => { - saveAs(resp.response, filename); - }, - onerror: () => { - let Link = document.createElement("a"); - Link.href = src; - Link.download = filename; - document.body.appendChild(Link); - Link.click(); - document.body.removeChild(Link); - } - }); - } + GM_xmlhttpRequest({ + method: "GET", + url: src, + responseType: "blob", + onload: (resp) => { + 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); + }, + onerror: () => { + let Link = document.createElement("a"); + Link.href = src; + Link.download = filename; + Link.target = "_blank"; + document.body.appendChild(Link); + Link.click(); + document.body.removeChild(Link); + } + }); }); // Apply to all images on the page