// Modified from https://github.com/mrhuo/image-viewer class ImageViewer { constructor() { this.targetSelector = "#gallery img"; this.currentScale = 1.0; this.maxScale = 8; this.minScale = 0.5; this.isDragging = false; this.hasMoved = false; this.downX = 0; this.downY = 0; this.startX = 0; this.startY = 0; this.currentX = 0; this.currentY = 0; this.imageList = []; this.fromArtwork = false; this.artworkFileIndex = -1; this.currentIndex = -1; this.oldURI = ""; this.SettingSubmitting = false; this.preview = document.getElementById("preview"); this.previewView = document.getElementById("preview-view"); this.sidebar = document.getElementById("sidebar"); this.imageWrapper = document.getElementById("image-wrapper"); this.previewImg = document.getElementById("preview-img"); this.navControls = document.getElementById("nav-controls"); this.prevBtn = document.getElementById("prev-btn"); this.nextBtn = document.getElementById("next-btn"); this.infoBox = document.getElementById("info-box"); this.imageCaption = document.getElementById("image-caption"); this.imageIndex = document.getElementById("image-index"); this.imageInfo = document.getElementById("image-info"); this.downloadBtn = document.getElementById("download-btn"); this.deleteBtn = document.getElementById("delete-btn"); this.sidebarBtn = document.getElementById("sidebar-btn"); this.sidebarContent = document.getElementById("sidebar-content"); this.sidebarImageArtworkLink = document.getElementById("img-artwork-a"); this.sidebarImageAuthor = document.getElementById("img-author"); this.sidebarImageSource = document.getElementById("img-source"); this.sidebarImageTag = document.getElementById("img-tag"); this.sidebarImageInfo = document.getElementById("img-info"); this.sidebarImageSetting = document.getElementById("img-setting"); this.sidebarImageSettingSselect = document.getElementById("sensitive-select"); this.sidebarImageSettingPselect = document.getElementById("private-select"); this.sidebarImageSettingButton = document.getElementById("setting-button"); this.sidebarImageSettingPostInput = document.getElementById("post-input"); this.sidebarImageSettingPostButton = document.getElementById("post-button"); this.sidebarImageSettingUploadFileButton = document.getElementById("upload-file-button"); this.sidebarImageSettingUploadURLInput = document.getElementById("upload-url-input"); this.sidebarImageSettingUploadButton = document.getElementById("upload-button"); let on = false; let status = localStorage.getItem("sidebar_on") if (status) { if (status === "true" && window.innerWidth > 640) { on = true; } } else if (window.innerWidth > 1600) { on = true; } if (on) this.openSidebar(); if (document.cookie.includes("_trpic_user")) { this.deleteBtn.style.display = "flex"; this.sidebarImageSetting.style.display = "block"; } this.imageWrapper.addEventListener("wheel", this.handleWheel.bind(this)); this.imageWrapper.addEventListener("mousedown", this.startDrag.bind(this)); this.prevBtn.addEventListener("click", this.showPrevImage.bind(this)); this.nextBtn.addEventListener("click", this.showNextImage.bind(this)); document.addEventListener("keydown", this.handleKeyDown.bind(this)); this.downloadBtn.addEventListener("click", this.downloadImage.bind(this)); this.deleteBtn.addEventListener("click", this.deleteImage.bind(this)); this.sidebarBtn.addEventListener("click", this.toggleSidebar.bind(this)); this.sidebarImageSettingSselect.addEventListener("change", this.updateSettingButtonBackground.bind(this)); this.sidebarImageSettingPselect.addEventListener("change", this.updateSettingButtonBackground.bind(this)); this.sidebarImageSettingButton.addEventListener("click", this.patchArtwork.bind(this)); this.sidebarImageSettingPostButton.addEventListener("click", this.postSource.bind(this)); this.sidebarImageSettingUploadButton.addEventListener("click", this.postArtworkFile.bind(this)); } updateSettingButtonBackground() { this.sidebarImageSettingButton.style.backgroundColor = "#99af4c"; } postArtworkFile() { const link = this.sidebarImageSettingUploadURLInput.value; const files = this.sidebarImageSettingUploadFileButton.files; if (link == "" && files.length == 0) { alert("请填入链接或文件"); return; } this.SettingSubmitting = true; this.sidebarImageSettingUploadButton.style.backgroundColor = "rgb(50, 50, 50)"; let artwork; if (this.fromArtwork) { artwork = this.imageList[this.currentIndex]; } else { artwork = this.imageList[this.currentIndex].edges.artwork; } if (link) { const formData = new FormData(); formData.append("link", link); fetch(`/api/artworks/${artwork.id}/files`, { method: "POST", body: formData, }) .then(res => { res.json().then(resjson => { switch (res.status) { case 201: break; case 401: alert("提交失败:需要验证"); break; default: alert(`提交失败:${resjson.msg}: ${resjson.error}`); break; } }) }) .finally(() => { this.sidebarImageSettingUploadButton.style.backgroundColor = "rgb(50, 50, 50)"; this.sidebarImageSettingUploadURLInput.value = ""; this.sidebarImageSettingUploadFileButton.value = ""; this.SettingSubmitting = false; }) .catch(error => { alert(`请求失败: ${error}`); }); } else { for (const file of files) { const formData = new FormData(); formData.append("file", file); fetch(`/api/artworks/${artwork.id}/files`, { method: "POST", body: formData, }) .then(res => { res.json().then(resjson => { switch (res.status) { case 201: break; case 401: alert("提交失败:需要验证"); break; default: alert(`提交失败:${resjson.msg}: ${resjson.error}`); break; } }) }) .finally(() => { this.sidebarImageSettingUploadButton.style.backgroundColor = "rgb(50, 50, 50)"; this.sidebarImageSettingUploadURLInput.value = ""; this.sidebarImageSettingUploadFileButton.value = ""; this.SettingSubmitting = false; }) .catch(error => { alert(`请求失败: ${error}`); }); } } } postSource() { const link = this.sidebarImageSettingPostInput.value; if (link == "" || this.SettingSubmitting == true) return; this.SettingSubmitting = true; this.sidebarImageSettingPostButton.style.backgroundColor = "rgb(50, 80, 80)"; let artwork; if (this.fromArtwork) { artwork = this.imageList[this.currentIndex]; } else { artwork = this.imageList[this.currentIndex].edges.artwork; } const formData = new FormData(); formData.append("link", link); fetch(`/api/artworks/${artwork.id}/posts`, { method: "POST", body: formData, }) .then(res => { res.json().then(resjson => { switch (res.status) { case 200: artwork.edges.posts = resjson.edges.posts; artwork.edges.socials = resjson.edges.socials; artwork.edges.tags = resjson.edges.tags; this.updateInfoBar(); break; case 401: alert("提交失败:需要验证"); break; case 409: if (confirm("此来源已链接到另一个作品,前往查看?")) { window.location.href = `/artworks/${resjson.artwork_id}`; } break; default: alert(`提交失败:${resjson.msg}: ${resjson.error}`); break; } }) }) .finally(() => { this.sidebarImageSettingPostButton.style.backgroundColor = "rgb(50, 50, 50)"; this.sidebarImageSettingPostInput.value = ""; this.SettingSubmitting = false; }) .catch(error => { alert(`请求失败: ${error}`); }); } patchArtwork() { if (this.SettingSubmitting) return; this.SettingSubmitting = true; let artwork; if (this.fromArtwork) { artwork = this.imageList[this.currentIndex]; } else { artwork = this.imageList[this.currentIndex].edges.artwork; } let Svalue = this.sidebarImageSettingSselect.value; if (Svalue < 0 || Svalue > 5) { this.SettingSubmitting = false; return; } let Pvalue = this.sidebarImageSettingPselect.value; const formData = new FormData(); formData.append("private", Pvalue); formData.append("sensitive", Svalue); this.sidebarImageSettingButton.style.backgroundColor = "rgb(50, 80, 80)"; fetch(`/api/artworks/${artwork.id}`, { method: "PATCH", body: formData, }) .then(res => { res.json().then(resjson => { switch (res.status) { case 200: artwork.sensitive = resjson.sensitive ? resjson.sensitive : 0; artwork.private = resjson.private ? true : false; this.updateInfoBar(); break; case 401: alert("提交失败:需要验证"); break; case 409: if (confirm("此来源已链接到另一个作品,前往查看?")) { window.location.href = `/artworks/${resjson.artwork_id}`; } break; default: alert(`提交失败:${resjson.msg}: ${resjson.error}`); break; } }) }) .finally(() => { this.SettingSubmitting = false; setInterval(() => { this.sidebarImageSettingButton.style.backgroundColor = "rgb(50, 50, 50)"; }, 1000) }) .catch(error => { alert(`请求失败: ${error}`); }); } appendFiles(files) { if (files.length === 0) return; if (this.fromArtwork) { this.imageList = []; this.artworkFileIndex = 0; this.currentIndex = 0; } this.imageList.push(...files); this.fromArtwork = false; } appendArtworks(artworks) { if (artworks.length === 0) return; if (!this.fromArtwork) { this.imageList = []; this.currentIndex = 0; } this.imageList.push(...artworks); this.fromArtwork = true; } resetList() { this.imageList = []; this.currentIndex = 0; } deleteArtwork() { let artwork_id; if (this.fromArtwork) { artwork_id = this.imageList[this.currentIndex].id; } else { artwork_id = this.imageList[this.currentIndex].edges.artwork.id; } fetch(`/api/artworks/${artwork_id}`, { method: "DELETE", }) .then((response) => { if (response.ok) { setTimeout(() => { this.closePreview(); resetGallery(); }, 1000); } else { response.json().then((resjson) => { alert(`Failed to delete artwork: ${resjson.msg}`); }); } }) .catch((err) => { console.error(err); alert(`Failed to delete artwork: ${err}`); }); } deleteImage() { let file; if (this.fromArtwork) { file = this.imageList[this.currentIndex].edges.files[this.artworkFileIndex]; } else { file = this.imageList[this.currentIndex]; } if (!confirm(`Are you sure you want to delete this ${file.id_str} image?`)) return; fetch(`/api/files/${file.id_str}`, { method: "DELETE", }) .then((response) => { if (response.ok) { setTimeout(() => { this.closePreview(); resetGallery(); }, 1000); } else { response.json().then((resjson) => { if (resjson.code == 403) { if (confirm(`This is last image of this artwork, are you want to delete this artwork?`)) this.deleteArtwork(); return } alert(`Failed to delete image: ${resjson.msg}`); }); } }) .catch((err) => { console.error(err); alert(`Failed to delete image: ${err}`); }); } downloadImage() { let file; if (this.fromArtwork) { file = this.imageList[this.currentIndex].edges.files[this.artworkFileIndex]; } else { file = this.imageList[this.currentIndex]; } const link = document.createElement("a"); const filename = `${ file.id_str }.${ file.ext }` link.download = `trpic_${filename}`; link.href = `/files/${filename}`; link.click(); link.remove(); } openSidebar() { localStorage.setItem("sidebar_on", true); this.sidebar.style.width = "30vw"; this.sidebar.classList.add("open"); this.navControls.classList.add("min-hide"); this.sidebarBtn.style.backgroundColor = "rgba(105, 105, 105, 0.3)"; } closeSidebar() { localStorage.setItem("sidebar_on", false); this.sidebar.style.width = "0px"; this.sidebar.classList.remove("open"); this.navControls.classList.remove("min-hide"); this.sidebarBtn.style.backgroundColor = ""; } toggleSidebar() { if (this.sidebar.classList.contains("open")) { this.closeSidebar(); } else { this.openSidebar(); } } bindDragEvents() { this.imageWrapper.addEventListener("mousemove", this.doDrag.bind(this)); this.imageWrapper.addEventListener("mouseup", this.endDrag.bind(this)); this.imageWrapper.addEventListener("mouseleave", this.endDrag.bind(this)); } unbindDragEvents() { this.imageWrapper.removeEventListener("mousemove", this.doDrag.bind(this)); this.imageWrapper.removeEventListener("mouseup", this.endDrag.bind(this)); this.imageWrapper.removeEventListener("mouseleave",this.endDrag.bind(this)); } resetTransform() { this.currentScale = 1.0; this.currentX = 0; this.currentY = 0; this.updateImageTransform(); } updateImageTransform() { this.previewImg.style.transform = ` translate(${this.currentX}px, ${this.currentY}px) scale(${this.currentScale}) `; } showImageByIndex(index, prev) { if (index < 0 || index >= this.imageList.length) { return; } if (index === this.imageList.length - 7 && index > prev) { loadMore(); } this.currentIndex = index; let file; if (this.fromArtwork) { file = this.imageList[this.currentIndex].edges.files[this.artworkFileIndex]; } else { file = this.imageList[this.currentIndex]; } this.resetTransform(); const highResSrc = `/files/${file.id_str}.${file.ext}`; const lowResSrc = `/thumbnails/${file.id_str}.jpg`; this.previewImg.src = (file.thumbed && !file.highres_loaded) ? lowResSrc : highResSrc; this.updateInfoBar(); this.updateNavButtons(); this.previewImg.onload = () => { // 如果有高分辨率图片,异步加载并在完成后替换 if (file.thumbed) { this.loadHighResImage(file, highResSrc); } }; this.previewImg.onerror = () => { this.updateInfoBar("Failed to load image"); }; this.infoBox.style.opacity = 0; this.preview.style.display = "flex"; document.body.style.overflow = "hidden"; gallery.style.opacity = 0.3; } loadHighResImage(file, highResSrc) { this.imageIndex.textContent += " loading..."; const listIndex = this.currentIndex; const fileIndex = this.artworkFileIndex; const highResImg = new Image(); highResImg.onload = () => { this.previewImg.onload = null; // Check if the image index has changed if (this.currentIndex != listIndex || this.artworkFileIndex != fileIndex) return; // High-resolution image loaded, replace current image this.previewImg.src = highResSrc; file.highres_loaded = "true"; this.updateInfoBar(); }; highResImg.onerror = (err) => { if (this.currentIndex != listIndex || this.artworkFileIndex != fileIndex) return; this.updateInfoBar(); alert(`Failed to load high res image: ${err}`) }; highResImg.src = highResSrc; } updateInfoBar() { if (this.fromArtwork) { this.updateInfoBarArtwork(); } else { this.updateInfoBarFile(); } } updateInfoBarArtwork() { const artwork = this.imageList[this.currentIndex]; this.imageIndex.textContent = `${this.currentIndex + 1}${artwork.edges.files.length > 1 ? ` - ${this.artworkFileIndex + 1}` : ""} / ${this.imageList.length}`; this.sidebarImageAuthor.replaceChildren(); if (artwork.edges.socials) { artwork.edges.socials.forEach((social, index) => { const author = document.createElement("p"); if (index > 0) { author.style.borderTop = "1px solid #777"; } const authorLink = document.createElement("a"); authorLink.textContent = `${social.platform}/${social.display_name}`; authorLink.href = social.url; authorLink.target = "_blank"; author.appendChild(authorLink); this.sidebarImageAuthor.appendChild(author); }) } this.sidebarImageSource.replaceChildren(); if (artwork.edges.posts) { artwork.edges.posts.forEach((post, index) => { const source = document.createElement("div"); if (index > 0) { source.style.borderTop = "1px solid #777"; } const link = document.createElement("a"); link.textContent = `${post.platform}/${post.post_id}`; link.href = post.url; link.target = "_blank"; source.appendChild(link); if (post.time) { const time = document.createElement("p"); time.classList.add("time"); time.textContent = post.time.substring(0, 19); source.appendChild(time); } if (post.title) { const title = document.createElement("p"); title.classList.add("title"); title.textContent = post.title; source.appendChild(title); } if (post.text) { const text = document.createElement("p"); text.classList.add("desc"); text.textContent = post.text; // text.innerHTML = post.text; source.appendChild(text); } this.sidebarImageSource.appendChild(source); }) } this.sidebarImageTag.replaceChildren(); if (artwork.edges.tags) { artwork.edges.tags.forEach(tag => { const tagLink = document.createElement("a"); tagLink.textContent = `#${tag.text}`; tagLink.href = `/artworks/?tags=${tag.id}`; this.sidebarImageTag.appendChild(tagLink); }) } const file = artwork.edges.files[this.artworkFileIndex]; const fileRes = document.createElement("p"); fileRes.textContent = `${file.width}*${file.height}px`; const fileInfo = document.createElement("p"); fileInfo.textContent = `${file.size_str} · ${file.ext}`; const id = document.createElement("p"); id.id = "img-id"; id.textContent = `${file.id_str} L${artwork.sensitive ? artwork.sensitive : 0} ${artwork.private ? " (private)" : ""}`; this.sidebarImageInfo.replaceChildren(fileRes, fileInfo, id); if (document.cookie.includes("_trpic_user")) { const a = document.createElement("a"); a.href = `/convert/${file.id_str}`; a.target = "_blank"; a.textContent = `${file.quality ? `${file.quality}% ` : ""}Convert`; this.sidebarImageInfo.appendChild(a); } this.sidebarImageSettingSselect.value = artwork.sensitive ? artwork.sensitive : 0; this.sidebarImageSettingPselect.value = Boolean(artwork.private); this.sidebarImageSettingPselect.style.backgroundColor = artwork.private ? "rgb(80, 70, 50)" : "rgb(50, 80, 50)"; this.sidebarImageArtworkLink.textContent = this.sidebarImageArtworkLink.href = `/artworks/${artwork.id}`; history.replaceState(null, null, `/artworks/${artwork.id}${this.artworkFileIndex > 0 ? `/photo/${this.artworkFileIndex}` : ""}`); } updateInfoBarFile() { const file = this.imageList[this.currentIndex]; const artwork = file.edges.artwork this.imageIndex.textContent = `${this.currentIndex + 1} / ${this.imageList.length}`; this.sidebarImageAuthor.replaceChildren(); if (artwork.edges.socials) { artwork.edges.socials.forEach((social, index) => { const author = document.createElement("p"); if (index > 0) { author.style.borderTop = "1px solid #777"; } const authorLink = document.createElement("a"); authorLink.textContent = `${social.platform}/${social.display_name}`; authorLink.href = social.url; authorLink.target = "_blank"; author.appendChild(authorLink); this.sidebarImageAuthor.appendChild(author); }) } this.sidebarImageSource.replaceChildren(); if (artwork.edges.posts) { artwork.edges.posts.forEach((post, index) => { const source = document.createElement("div"); if (index > 0) { source.style.borderTop = "1px solid #777"; } const link = document.createElement("a"); link.textContent = `${post.platform}/${post.post_id}`; link.href = post.url; link.target = "_blank"; source.appendChild(link); if (post.time) { const time = document.createElement("p"); time.classList.add("time"); time.textContent = post.time.substring(0, 19); source.appendChild(time); } if (post.title) { const title = document.createElement("p"); title.textContent = post.title; source.appendChild(title); } if (post.text) { const text = document.createElement("p"); text.classList.add("desc"); text.textContent = post.text; // text.innerHTML = post.text; source.appendChild(text); } this.sidebarImageSource.appendChild(source); }) } this.sidebarImageTag.replaceChildren(); if (artwork.edges.tags) { artwork.edges.tags.forEach(tag => { const tagLink = document.createElement("a"); tagLink.textContent = `#${tag.text}`; tagLink.href = `/artworks/?tags=${tag.id}`; this.sidebarImageTag.appendChild(tagLink); }) } const fileRes = document.createElement("p"); fileRes.textContent = `${file.width}*${file.height}px`; const fileInfo = document.createElement("p"); fileInfo.textContent = `${file.size_str} · ${file.ext}`; const id = document.createElement("p"); id.id = "img-id"; id.textContent = `${file.id_str} L${artwork.sensitive ? artwork.sensitive : 0 } ${artwork.private ? " (private)" : ""}`; this.sidebarImageInfo.replaceChildren(fileRes, fileInfo, id); if (document.cookie.includes("_trpic_user")) { const a = document.createElement("a"); a.href = `/convert/${file.id_str}`; a.target = "_blank"; a.textContent = `${file.quality ? `${file.quality}% ` : ""}Convert`; this.sidebarImageInfo.appendChild(a); } this.sidebarImageSettingSselect.value = artwork.sensitive ? artwork.sensitive : 0; this.sidebarImageSettingPselect.value = Boolean(artwork.private); this.sidebarImageSettingPselect.style.backgroundColor = artwork.private ? "rgb(80, 70, 50)" : "rgb(50, 80, 50)"; this.sidebarImageArtworkLink.textContent = this.sidebarImageArtworkLink.href = `/artworks/${artwork.id}`; history.replaceState(null, null, `/artworks/${artwork.id}`); } updateNavButtons() { let hasPrev = false; let hasNext = false; if (this.fromArtwork) { hasPrev = this.artworkFileIndex > 0 || this.currentIndex > 0; hasNext = this.artworkFileIndex < this.imageList[this.currentIndex].edges.files.length - 1 || this.currentIndex < this.imageList.length - 1; } else { hasPrev = this.currentIndex > 0; hasNext = this.currentIndex < this.imageList.length - 1; } this.prevBtn.style.opacity = hasPrev ? "1" : "0"; this.prevBtn.style.pointerEvents = hasPrev ? "auto" : "none"; this.nextBtn.style.opacity = hasNext ? "1" : "0"; this.nextBtn.style.pointerEvents = hasNext ? "auto" : "none"; } openPreview(e, img) { if (img.tagName != "IMG") return; e.stopPropagation(); let index; if (this.fromArtwork) { index = this.imageList.findIndex((i) => i.id == img.dataset.artwork_id); this.artworkFileIndex = this.imageList[index].edges.files.findIndex((i) => i.id_str == img.dataset.id_str); } else { index = this.imageList.findIndex((i) => i.id_str === img.dataset.id_str); } if (index !== -1) { this.oldURI = window.location.pathname + window.location.search; if (window.innerWidth <= 640) this.closeSidebar(); this.showImageByIndex(index, 0); } } closePreview() { this.currentIndex = -1; this.infoBox.style.opacity = 1; this.preview.style.display = "none"; document.body.style.overflow = ""; gallery.style.opacity = 1; history.replaceState(null, null, this.oldURI); } showNextImage() { if (this.fromArtwork) { if (this.artworkFileIndex < this.imageList[this.currentIndex].edges.files.length - 1) { this.artworkFileIndex++; this.showImageByIndex(this.currentIndex, this.currentIndex); return; } if (this.currentIndex < this.imageList.length - 1) { this.artworkFileIndex = 0; this.showImageByIndex(this.currentIndex + 1, this.currentIndex); } } else { if (this.currentIndex < this.imageList.length - 1) { this.showImageByIndex(this.currentIndex + 1, this.currentIndex); } } } showPrevImage() { if (this.fromArtwork) { if (this.artworkFileIndex > 0) { this.artworkFileIndex--; this.showImageByIndex(this.currentIndex, this.currentIndex); return; } if (this.currentIndex > 0) { this.artworkFileIndex = this.imageList[this.currentIndex - 1].edges.files.length - 1; this.showImageByIndex(this.currentIndex - 1, this.currentIndex); } } else { if (this.currentIndex > 0) { this.showImageByIndex(this.currentIndex - 1, this.currentIndex); } } } handleKeyDown(e) { if (e.key == "Enter") { this.openPreview(e, document.activeElement); return; } if (this.preview.style.display !== "flex") return; if (document.activeElement === this.sidebarImageSettingPostInput || document.activeElement === this.sidebarImageSettingUploadURLInput) return; switch (e.key) { case "Escape": this.closePreview(); break; case "ArrowRight": this.showNextImage(); break; case "ArrowLeft": this.showPrevImage(); break; case "b": if (e.ctrlKey) { this.toggleSidebar(); } break; } } handleWheel(e) { e.preventDefault(); const delta = e.deltaY > 0 ? this.currentScale < 2 ? -0.1 : -0.2 : this.currentScale < 2 ? 0.1 : 0.2; const newScale = parseFloat((this.currentScale + delta).toFixed(10)); if (newScale >= this.minScale && newScale <= this.maxScale) { const ratio = newScale / this.currentScale; const rect = this.imageWrapper.getBoundingClientRect(); const offsetX = e.clientX - (rect.left + rect.width / 2); const offsetY = e.clientY - (rect.top + rect.height / 2); this.currentX = offsetX * (1 - ratio) + this.currentX * ratio; this.currentY = offsetY * (1 - ratio) + this.currentY * ratio; this.currentScale = newScale; this.updateImageTransform(); } } startDrag(e) { if (e.button !== 0) return; // Only left button e.preventDefault(); e.stopPropagation(); this.isDragging = true; this.hasMoved = false; this.startX = e.clientX - this.currentX; this.startY = e.clientY - this.currentY; this.downX = e.clientX; this.downY = e.clientY; this.imageWrapper.classList.add("dragging"); this.bindDragEvents(); } doDrag(e) { if (!this.isDragging) return; e.preventDefault(); e.stopPropagation(); const dx = e.clientX - this.downX; const dy = e.clientY - this.downY; if (Math.abs(dx) > 2 || Math.abs(dy) > 2) { this.hasMoved = true; } this.currentX = e.clientX - this.startX; this.currentY = e.clientY - this.startY; this.updateImageTransform(); } endDrag(e) { if (!this.isDragging) return; if (e) { e.stopPropagation(); } this.isDragging = false; this.imageWrapper.classList.remove("dragging"); this.unbindDragEvents(); if (!this.hasMoved) { this.closePreview(); } } bindClickEvents(img) { img.addEventListener("click", (e) => this.openPreview(e, img)); img.style.cursor = "zoom-in"; } } let imgviewer; document.addEventListener('DOMContentLoaded', () => { imgviewer = new ImageViewer({}) });