class MetaParser { static parse() { let file; const metas = document.querySelectorAll("meta[name]"); metas.forEach(meta => { const name = meta.getAttribute("name"); const data = this.metaToObject(meta); if (name === "file") { file = data; } }); return file; } static metaToObject(meta) { const obj = {}; for (const attr of meta.attributes) { let value = attr.value; if (value === "true") { value = true; } else if (value === "false") { value = false; } else if (Number.isSafeInteger(value) && !isNaN(value) && value.trim() !== "") { value = Number(value); } obj[attr.name] = value; } return obj; } } class Viewer { constructor(wrapper, content, header, image) { this.wrapper = wrapper; this.content = content; this.header = header; this.rawImage = image; this.wrapper.addEventListener('wheel', this.wheel, { passive: false }) this.wrapper.addEventListener('pointerdown', this.down) this.wrapper.addEventListener('pointermove', this.move) this.wrapper.addEventListener('pointerup', this.up) this.wrapper.addEventListener('pointercancel', this.up) this.state = { x: 0, y: 0, scale: 1 } this.tap = { startX: 0, startY: 0, moved: false, isMultiTouch: false } this.input = { pointers: new Map(), wheel: null, dirty: false } this.scale = { min: 0.5, max: 30 } this.prevPan = null this.prevPinch = null document.body.style.touchAction = "none"; this.loop(); } down = (e) => { this.wrapper.style.cursor = "grabbing" this.wrapper.setPointerCapture(e.pointerId) this.input.pointers.set(e.pointerId, { x: e.clientX, y: e.clientY, }) if (this.input.pointers.size === 1) { this.tap.startX = e.clientX this.tap.startY = e.clientY this.tap.moved = false this.tap.isMultiTouch = false } else { this.tap.isMultiTouch = true } this.input.dirty = true } move = (e) => { const p = this.input.pointers.get(e.pointerId) if (!p) return p.x = e.clientX p.y = e.clientY this.tap.moved = Math.hypot( e.clientX - this.tap.startX, e.clientY - this.tap.startY ) > 5 this.input.dirty = true } up = (e) => { this.wrapper.style.cursor = "" this.input.pointers.delete(e.pointerId) if (this.input.pointers.size === 0 && !this.tap.moved && !this.tap.isMultiTouch) { this.rawImage.style.opacity = this.rawImage.style.opacity === "0" ? "1" : "0"; this.header.style.display = this.rawImage.style.opacity === "0" ? "none" : "flex"; } this.input.dirty = true } wheel = (e) => { e.preventDefault() this.input.wheel = { x: e.clientX, y: e.clientY, delta: e.deltaY, } this.input.dirty = true } loop = () => { this.update() requestAnimationFrame(this.loop); } update() { if (!this.input.dirty) return this.input.dirty = false const pts = [...this.input.pointers.values()] if (this.input.wheel) { const { x, y, delta } = this.input.wheel const scaleFactor = Math.exp(-delta * 0.001) this.zoomAt(x, y, scaleFactor) this.input.wheel = null } if (pts.length === 1) { this.pan(pts[0]) } else if (pts.length >= 2) { this.pinch(pts[0], pts[1]) } if (pts.length < 2) this.prevPinch = null if (pts.length !== 1) this.prevPan = null this.render() } pan(p) { if (!this.prevPan) { this.prevPan = { ...p } return } this.state.x += p.x - this.prevPan.x this.state.y += p.y - this.prevPan.y this.prevPan = { ...p } } pinch(p1, p2) { const center = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2, } const dist = Math.hypot(p1.x - p2.x, p1.y - p2.y) if (!this.prevPinch) { this.prevPinch = { center, dist } return } const scaleFactor = dist / this.prevPinch.dist this.zoomAt(center.x, center.y, scaleFactor) this.state.x += center.x - this.prevPinch.center.x this.state.y += center.y - this.prevPinch.center.y this.prevPinch = { center, dist } } zoomAt(cx, cy, scaleFactor) { const rect = this.content.getBoundingClientRect() const dx = cx - (rect.left + rect.width / 2) const dy = cy - (rect.top + rect.height / 2) const prevScale = this.state.scale const newScale = Math.max(this.scale.min, Math.min(this.scale.max, prevScale * scaleFactor)) const ratio = newScale / prevScale this.state.x -= dx * (ratio - 1) this.state.y -= dy * (ratio - 1) this.state.scale = newScale } render() { const { x, y, scale } = this.state this.content.style.transform = `translate(${x}px, ${y}px) scale(${scale})` } reset() { this.state.x = 0 this.state.y = 0 this.state.scale = 1 this.render(); } } document.addEventListener('DOMContentLoaded', () => { let file = MetaParser.parse(), requesting = false; document.getElementById("preview").style.display = "block"; let header = document.getElementById("preview-header"); let qualitySel = document.getElementById("quality-selector"); qualitySel.addEventListener("change", switchQuality.bind(this)); let convertBtn = document.getElementById("convert-button"); convertBtn.addEventListener("click", useConvert.bind(this)); let saveBtn = document.getElementById("save-button"); saveBtn.addEventListener("click", saveConvert.bind(this)); let pixelatedBtn = document.getElementById("pixelated-button"); pixelatedBtn.addEventListener("click", switchPixelated.bind(this)); let backgroundBtn = document.getElementById("background-button"); backgroundBtn.addEventListener("click", switchBackground.bind(this)); let resetBtn = document.getElementById("reset-button"); resetBtn.addEventListener("click", resetImage.bind(this)); let previewView = document.getElementById("preview-view"); let imageWrapper = document.getElementById("image-wrapper"); let previewImg = document.getElementById("preview-img"); let originalImg = document.getElementById("original-img"); new Viewer(previewView, imageWrapper, header, previewImg); let imageSel = document.getElementById("image-selector"); imageSel.addEventListener("change", switchImage.bind(this)); if (imageSel.options.length == 0) { collectRaw(); } else { switchImage(); } document.addEventListener('keydown', (e) => { if (e.ctrlKey) { previewImg.style.opacity = 0 header.style.display = "none" } if (e.shiftKey) { previewView.style.imageRendering = "pixelated"; pixelatedBtn.style.backgroundColor = "#4d7543"; } let currentIndex = qualitySel.selectedIndex; const maxIndex = qualitySel.options.length - 1; switch (e.key) { case 'ArrowDown': e.preventDefault(); qualitySel.selectedIndex = currentIndex < maxIndex ? currentIndex + 1 : maxIndex; switchQuality(); break; case 'ArrowUp': e.preventDefault(); qualitySel.selectedIndex = currentIndex > 0 ? currentIndex - 1 : 0; switchQuality(); break; case 'Enter': e.preventDefault(); let q = qualitySel.value; if (document.getElementById("q" + q).dataset.converted) { if (confirm(`save image with ${q}% quality?`)) saveConvert(); } else { useConvert(); } } }); document.addEventListener('keyup', (e) => { switch (e.key) { case 'Control': previewImg.style.opacity = 1 header.style.display = "flex" break case 'Shift': previewView.style.imageRendering = "auto"; pixelatedBtn.style.backgroundColor = ""; } }); function useConvert() { if (requesting) { alert("Image is converting"); return; } // 开始转换后禁用原图选择器 imageSel.disabled = true; let q = qualitySel.value; let e = document.getElementById("q" + q); if (e.dataset.converted) { alert(`Image already converted with ${q}% quality`); return; } requesting = true; convertBtn.textContent = "converting" document.title = `${q}% converting 🔄` convertBtn.style.backgroundColor = "rgb(50, 80, 80)"; fetch(`/api/task/files/convert/${file.id}/webp/${q}?model=${imageSel.value}`, { method: "POST" }) .then(response => { response.json().then((resjson) => { switch (response.status) { case 200: e.dataset.converted = true; document.title = `${q}% - ${resjson.size_str_converted} ☑` e.textContent += ` - ${resjson.size_str_converted}`; previewImg.src = ``; previewImg.src = e.dataset.src = resjson.url; previewImg.style.opacity = 1 break; default: alert(`转换发生错误:${resjson.msg}: ${resjson.error}`); document.title = "failed ❌" } }); }) .finally(() => { convertBtn.textContent = "convert" convertBtn.style.backgroundColor = "#444"; requesting = false; }) .catch(error => { alert(`请求失败: ${error}`); }); } function saveConvert() { if (requesting) { alert("Request is already in progress"); return; } let q = qualitySel.value; let e = document.getElementById("q" + q); if (e.dataset.saved) { alert("already saved"); return } if (!e.dataset.converted) { alert(`need to convert first`); return } requesting = true; saveBtn.textContent = "saving" saveBtn.style.backgroundColor = "rgb(50, 80, 80)"; fetch(`/api/task/files/convert-done/${file.id}/webp/${q}?model=${imageSel.value}`, { method: "POST" }) .then(response => { response.json().then((resjson) => { switch (response.status) { case 200: e.dataset.saved = true; saveBtn.textContent = "saved" break; default: alert(`保存图片时发生错误:${resjson.msg}: ${resjson.error}`); } }); }) .finally(() => { saveBtn.style.backgroundColor = "#444"; requesting = false; }) .catch(error => { saveBtn.textContent = "save" alert(`请求失败: ${error}`); }); } function switchQuality() { previewImg.src = "" previewImg.style.opacity = 0 let e = document.getElementById("q" + qualitySel.value); if (e.dataset.converted) { previewImg.src = e.dataset.src; previewImg.style.opacity = 1 } } function switchImage() { originalImg.src = "" let img = imageSel.value; if (img == "orignal") { originalImg.src = `/files/${file.id}.${file.raw_ext}`; } else { originalImg.src = `/files/${file.id}_upscayl_${img}.png`; } } function switchPixelated() { if (previewView.style.imageRendering === "pixelated") { previewView.style.imageRendering = "auto"; pixelatedBtn.style.backgroundColor = ""; } else { previewView.style.imageRendering = "pixelated"; pixelatedBtn.style.backgroundColor = "#4d7543"; } } function switchBackground() { if (backgroundBtn.textContent == "black") { backgroundBtn.textContent = "white" backgroundBtn.style.color = "#000" backgroundBtn.style.background = "#ddd" document.body.style.background = "#fff" previewImg.style.background = "#fff" originalImg.style.background = "#fff" } else { backgroundBtn.textContent = "black" backgroundBtn.style.color = "" backgroundBtn.style.background = "" document.body.style.background = "#000" previewImg.style.background = "#000" originalImg.style.background = "#000" } } function resetImage() { if (requesting) { alert("another request is already in progress"); return; } requesting = true; resetBtn.textContent = "resetting" resetBtn.style.backgroundColor = "rgb(50, 80, 80)"; fetch(`/api/task/files/convert-reset/${file.id}`, { method: "POST" }) .then(response => { response.json().then((resjson) => { switch (response.status) { case 200: break; default: alert(`发生错误:${resjson.msg}: ${resjson.error}`); } }); }) .finally(() => { resetBtn.textContent = "reset" resetBtn.style.backgroundColor = "#444"; requesting = false; }) .catch(error => { alert(`请求失败: ${error}`); }); } function collectRaw() { if (confirm(`The original image could not be loaded. Need try download it to the server?`)) { fetch(`/api/task/files/convert-raw/${file.id}`, { method: "POST" }) .then(response => { response.json().then((resjson) => { switch (response.status) { case 200: originalImg.src = resjson.url; const opt = document.createElement("option"); opt.value = "orignal"; opt.textContent = `orignal - ${resjson.size_str}` imageSel.appendChild(opt); break; default: alert(`发生错误:${resjson.msg}: ${resjson.error}`); return } }); }) .catch(error => { alert(`请求失败: ${error}`); }); } } });