600 lines
17 KiB
JavaScript
600 lines
17 KiB
JavaScript
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(),
|
|
model = "",
|
|
remote = true,
|
|
upscaylURL = localStorage.getItem("upscayl_url"),
|
|
upscaylToken = localStorage.getItem("upscayl_token"),
|
|
requesting = false;
|
|
|
|
let modelSel = document.getElementById("model-selector");
|
|
modelSel.addEventListener("change", switchModel.bind(this));
|
|
let upscaylBtn = document.getElementById("upscayl-button");
|
|
upscaylBtn.addEventListener("click", useUpscayl.bind(this));
|
|
let uploadBtn = document.getElementById("upload-button");
|
|
uploadBtn.addEventListener("click", uploadImage.bind(this));
|
|
let goConvertBtn = document.getElementById("go-convert-button");
|
|
goConvertBtn.addEventListener("click", () => { window.location.href = `/convert/${file.id}`; });
|
|
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 removeBtn = document.getElementById("remove-button");
|
|
removeBtn.addEventListener("click", () => {
|
|
if (confirm("Remove upscayl-server API URL and Token?")) {
|
|
removeTokens();
|
|
window.location.reload();
|
|
}
|
|
});
|
|
|
|
if (modelSel.children.length == 1) {
|
|
remote = false;
|
|
uploadBtn.style.display = "";
|
|
|
|
for (; !upscaylURL;) upscaylURL = prompt("Enter upscayl-server API URL")
|
|
for (; !upscaylToken;) upscaylToken = prompt("Enter upscayl-server API Token")
|
|
|
|
if (upscaylURL || upscaylToken) removeBtn.style.display = "";
|
|
|
|
fetch(`${upscaylURL}/models`, { headers: { 'Authorization': upscaylToken } })
|
|
.then(res => {
|
|
res.json().then((resjson) => {
|
|
if (!res.ok) {
|
|
alert(`Error: ${resjson.error}`)
|
|
removeTokens();
|
|
} else {
|
|
localStorage.setItem("upscayl_url", upscaylURL)
|
|
localStorage.setItem("upscayl_token", upscaylToken)
|
|
|
|
resjson.models.forEach((m) => {
|
|
const opt = document.createElement("option");
|
|
opt.id = opt.value = opt.textContent = m;
|
|
modelSel.appendChild(opt);
|
|
})
|
|
}
|
|
});
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
alert(`Error while fetching models: ${err}`);
|
|
});
|
|
}
|
|
|
|
document.getElementById("preview").style.display = "block";
|
|
let header = document.getElementById("preview-header");
|
|
|
|
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);
|
|
|
|
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 = modelSel.selectedIndex;
|
|
const maxIndex = modelSel.options.length - 1;
|
|
|
|
switch (e.key) {
|
|
case 'ArrowDown':
|
|
e.preventDefault();
|
|
modelSel.selectedIndex = currentIndex < maxIndex ? currentIndex + 1 : maxIndex;
|
|
switchModel();
|
|
break;
|
|
case 'ArrowUp':
|
|
e.preventDefault();
|
|
modelSel.selectedIndex = currentIndex > 0 ? currentIndex - 1 : 0;
|
|
switchModel();
|
|
break;
|
|
case 'Enter':
|
|
e.preventDefault();
|
|
if (!remote && model && document.getElementById(model).dataset.converted) {
|
|
if (confirm(`upload ${model} image?`)) uploadImage();
|
|
} else {
|
|
useUpscayl();
|
|
}
|
|
}
|
|
});
|
|
|
|
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 = "";
|
|
}
|
|
});
|
|
|
|
let originalSize = document.getElementById("original-size");
|
|
if (originalSize.textContent == "") {
|
|
collectRaw();
|
|
} else {
|
|
originalImg.src = `/files/${file.id}.${file.raw_ext}`;
|
|
}
|
|
|
|
function removeTokens() {
|
|
localStorage.removeItem("upscayl_url")
|
|
localStorage.removeItem("upscayl_token")
|
|
}
|
|
|
|
function uploadImage() {
|
|
if (remote) {
|
|
// remote 模式不需要上传
|
|
alert("image already in the server");
|
|
return;
|
|
}
|
|
|
|
if (requesting) {
|
|
alert("need waiting for previous request to finish");
|
|
return;
|
|
}
|
|
|
|
if (!model) {
|
|
alert(`select a model and convert first`);
|
|
return
|
|
}
|
|
|
|
let e = document.getElementById(model);
|
|
|
|
if (e.dataset.uploaded) {
|
|
alert("already saved");
|
|
return
|
|
}
|
|
|
|
if (!e.dataset.converted) {
|
|
alert(`need to convert first`);
|
|
return
|
|
}
|
|
|
|
requesting = true;
|
|
uploadBtn.textContent = "uploading"
|
|
document.title = `${model} uploading ⬆`
|
|
uploadBtn.style.backgroundColor = "rgb(50, 80, 80)";
|
|
|
|
fetch(previewImg.src)
|
|
.then(f => {
|
|
return new Promise(async (resolve, reject) => {
|
|
const blob = await f.blob();
|
|
const formData = new FormData();
|
|
formData.append('file', blob, 'tmp.png');
|
|
|
|
let res = await fetch(`/api/task/files/upscayl/${file.id}/${model}`, { method: 'PUT', body: formData })
|
|
let resjson = await res.json()
|
|
switch (res.status) {
|
|
case 200:
|
|
e.dataset.uploaded = true;
|
|
uploadBtn.textContent = "uploaded"
|
|
document.title = `${model} uploaded ☑`
|
|
goConvertBtn.style.display = "";
|
|
break;
|
|
default:
|
|
alert(`保存 upscayl 图片时发生错误:${resjson.msg}: ${resjson.error}`);
|
|
document.title = "upload failed ❌"
|
|
reject(new Error(resjson.error));
|
|
}
|
|
resolve();
|
|
})
|
|
})
|
|
.finally(() => {
|
|
uploadBtn.style.backgroundColor = "#444";
|
|
requesting = false;
|
|
})
|
|
.catch(error => {
|
|
uploadBtn.textContent = "upload"
|
|
alert(`请求失败: ${error}`);
|
|
});
|
|
}
|
|
|
|
function useUpscayl() {
|
|
if (requesting) {
|
|
alert("need waiting for previous request to finish");
|
|
return;
|
|
}
|
|
|
|
if (!model) {
|
|
alert(`select a model to convert`);
|
|
return
|
|
}
|
|
|
|
let e = document.getElementById(model);
|
|
if (e.dataset.converted) {
|
|
alert(`already converted with ${model}`)
|
|
return;
|
|
}
|
|
|
|
requesting = true;
|
|
upscaylBtn.textContent = "waiting"
|
|
document.title = `${model} processing 🔄`
|
|
upscaylBtn.style.backgroundColor = "rgb(50, 80, 80)";
|
|
|
|
if (remote) {
|
|
fetch(`/api/task/files/upscayl/${file.id}/${model}`, { method: "POST" })
|
|
.then(response => {
|
|
response.json().then((resjson) => {
|
|
switch (response.status) {
|
|
case 200:
|
|
e.dataset.converted = true;
|
|
document.title = `${model} ☑`
|
|
e.textContent += ` - ${resjson.size_str}`
|
|
previewImg.src = ``;
|
|
previewImg.src = e.dataset.src = resjson.url;
|
|
goConvertBtn.style.display = "";
|
|
break;
|
|
default:
|
|
alert(`upscayl 发生错误:${resjson.msg}: ${resjson.error}`);
|
|
document.title = "upscayl failed ❌"
|
|
}
|
|
});
|
|
})
|
|
.finally(() => {
|
|
upscaylBtn.textContent = "upscayl"
|
|
upscaylBtn.style.backgroundColor = "#444";
|
|
requesting = false;
|
|
})
|
|
.catch(error => {
|
|
alert(`请求失败: ${error}`);
|
|
});
|
|
} else {
|
|
fetch(`/files/${file.id}.${file.raw_ext}`)
|
|
.then(res => {
|
|
return new Promise(async (resolve, reject) => {
|
|
const blob = await res.blob();
|
|
const formData = new FormData();
|
|
formData.append("model", model);
|
|
formData.append('file', blob, 'tmp.png');
|
|
let taskres = await fetch(`${upscaylURL}/tasks`, { method: "POST", headers: { 'Authorization': upscaylToken }, body: formData });
|
|
|
|
function poll(t) {
|
|
if (t.status === "done") {
|
|
e.dataset.converted = true;
|
|
document.title = `${model} ☑`
|
|
e.textContent += ` - ${t.size_str}`;
|
|
previewImg.src = ``;
|
|
previewImg.src = e.dataset.src = `${upscaylURL}${t.url}`;
|
|
uploadBtn.style.backgroundColor = "rgb(70, 120, 95)";
|
|
resolve();
|
|
} else if (t.status === "error") {
|
|
reject(new Error(t.error))
|
|
} else {
|
|
setTimeout(async () => {
|
|
poll(await (await fetch(`${upscaylURL}/tasks/${t.id}`)).json());
|
|
}, 2000);
|
|
}
|
|
}
|
|
|
|
poll(await taskres.json());
|
|
});
|
|
})
|
|
.finally(() => {
|
|
upscaylBtn.textContent = "upscayl"
|
|
upscaylBtn.style.backgroundColor = "#444";
|
|
requesting = false;
|
|
})
|
|
.catch(error => {
|
|
alert(`请求失败: ${error}`);
|
|
});
|
|
}
|
|
}
|
|
|
|
function switchModel() {
|
|
uploadBtn.textContent = "upload"
|
|
uploadBtn.style.backgroundColor = "";
|
|
previewImg.src = ""
|
|
previewImg.style.opacity = 0
|
|
model = modelSel.value;
|
|
if (model != "select model") {
|
|
let e = document.getElementById(model);
|
|
if (e.dataset.uploaded) {
|
|
uploadBtn.textContent = "uploaded"
|
|
} else if (e.dataset.converted) {
|
|
uploadBtn.style.backgroundColor = "rgb(70, 120, 95)";
|
|
}
|
|
previewImg.style.opacity = 1
|
|
previewImg.src = e.dataset.src ? e.dataset.src : "";
|
|
} else {
|
|
model = ""
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
if (!confirm(`Reset to original image?`)) 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;
|
|
originalSize.textContent = resjson.size_str;
|
|
break;
|
|
default:
|
|
alert(`发生错误:${resjson.msg}: ${resjson.error}`);
|
|
return
|
|
}
|
|
});
|
|
})
|
|
.catch(error => {
|
|
alert(`请求失败: ${error}`);
|
|
});
|
|
}
|
|
}
|
|
});
|