676 lines
20 KiB
JavaScript
676 lines
20 KiB
JavaScript
class MetaParser {
|
|
static parse() {
|
|
const result = {
|
|
artwork: null,
|
|
socials: [],
|
|
files: [],
|
|
posts: [],
|
|
tags: []
|
|
};
|
|
const metas = document.querySelectorAll("meta[name]");
|
|
|
|
metas.forEach(meta => {
|
|
const name = meta.getAttribute("name");
|
|
const data = this.metaToObject(meta);
|
|
if (name === "artwork") {
|
|
result.artwork = data;
|
|
} else if (name.startsWith("social-")) {
|
|
result.socials.push(data);
|
|
} else if (name.startsWith("file-")) {
|
|
result.files.push(data);
|
|
} else if (name.startsWith("post-")) {
|
|
result.posts.push(data);
|
|
} else if (name.startsWith("tag-")) {
|
|
result.tags.push(data);
|
|
}
|
|
});
|
|
return result;
|
|
}
|
|
|
|
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) {
|
|
this.wrapper = wrapper;
|
|
this.content = content;
|
|
|
|
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.input = { pointers: new Map(), wheel: null, dirty: false }
|
|
this.scale = { min: 0.5, max: 8 }
|
|
this.prevPan = null
|
|
this.prevPinch = null
|
|
|
|
document.body.style.touchAction = "none";
|
|
this.loop();
|
|
}
|
|
|
|
down = (e) => {
|
|
this.wrapper.style.cursor = "grabbing"
|
|
this.content.style.willChange = "transform"
|
|
this.wrapper.setPointerCapture(e.pointerId)
|
|
this.input.pointers.set(e.pointerId, {
|
|
x: e.clientX,
|
|
y: e.clientY,
|
|
})
|
|
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.input.dirty = true
|
|
}
|
|
|
|
up = (e) => {
|
|
this.wrapper.style.cursor = ""
|
|
this.content.style.willChange = ""
|
|
this.input.pointers.delete(e.pointerId)
|
|
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', () => {
|
|
const pathIndex = parseInt(window.location.pathname.split("/photo/").pop());
|
|
|
|
let data = MetaParser.parse(),
|
|
index = pathIndex ? pathIndex : 0;
|
|
|
|
let preview = document.getElementById("preview");
|
|
preview.style.display = "flex";
|
|
|
|
let imageWrapper = document.getElementById("image-wrapper");
|
|
let previewImg = document.getElementById("preview-img");
|
|
|
|
let viewer = new Viewer(imageWrapper, previewImg);
|
|
|
|
let prevBtn = document.getElementById("prev-btn");
|
|
prevBtn.addEventListener("click", showPrevImage.bind(this));
|
|
let nextBtn = document.getElementById("next-btn");
|
|
nextBtn.addEventListener("click", showNextImage.bind(this));
|
|
|
|
document.addEventListener("keydown", handleKeyDown.bind(this));
|
|
|
|
let navControls = document.getElementById("nav-controls");
|
|
let downloadBtn = document.getElementById("download-btn");
|
|
downloadBtn.addEventListener("click", downloadImage.bind(this));
|
|
let deleteBtn = document.getElementById("delete-btn");
|
|
deleteBtn.addEventListener("click", deleteImage.bind(this));
|
|
|
|
let sidebar = document.getElementById("sidebar");
|
|
let sidebarBtn = document.getElementById("sidebar-btn");
|
|
sidebarBtn.addEventListener("click", toggleSidebar.bind(this));
|
|
|
|
let sidebarImageAuthor = document.getElementById("img-author");
|
|
let sidebarImageSource = document.getElementById("img-source");
|
|
let sidebarImageTag = document.getElementById("img-tag");
|
|
let sidebarImageInfo = document.getElementById("img-info");
|
|
let sidebarImageSetting = document.getElementById("img-setting");
|
|
let sidebarImageSettingSselect = document.getElementById("sensitive-select");
|
|
let sidebarImageSettingPselect = document.getElementById("private-select");
|
|
let sidebarImageSettingButton = document.getElementById("setting-button");
|
|
sidebarImageSettingButton.addEventListener("click", patchArtwork.bind(this));
|
|
let sidebarImageSettingPostInput = document.getElementById("post-input");
|
|
let sidebarImageSettingPostButton = document.getElementById("post-button");
|
|
sidebarImageSettingPostButton.addEventListener("click", postSource.bind(this));
|
|
let sidebarImageSettingUploadFileButton = document.getElementById("upload-file-button");
|
|
let sidebarImageSettingUploadURLInput = document.getElementById("upload-url-input");
|
|
let sidebarImageSettingUploadButton = document.getElementById("upload-button");
|
|
sidebarImageSettingUploadButton.addEventListener("click", postArtworkFile.bind(this));
|
|
|
|
let SettingSubmitting = false;
|
|
|
|
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) openSidebar();
|
|
|
|
if (document.cookie.includes("_trpic_user")) {
|
|
deleteBtn.style.display = "flex";
|
|
sidebarImageSetting.style.display = "block";
|
|
}
|
|
|
|
prevBtn.style.opacity = 0;
|
|
nextBtn.style.opacity = 0;
|
|
|
|
showImage();
|
|
|
|
function postArtworkFile() {
|
|
const link = sidebarImageSettingUploadURLInput.value;
|
|
const files = sidebarImageSettingUploadFileButton.files;
|
|
|
|
if (link == "" && files.length == 0) {
|
|
alert("请填入链接或文件");
|
|
return;
|
|
}
|
|
|
|
SettingSubmitting = true;
|
|
sidebarImageSettingUploadButton.style.backgroundColor = "rgb(50, 50, 50)";
|
|
|
|
if (link) {
|
|
const formData = new FormData();
|
|
formData.append("link", link);
|
|
fetch(`/api/artworks/${data.artwork.id}/files`, {
|
|
method: "POST",
|
|
body: formData,
|
|
})
|
|
.then(res => {
|
|
res.json().then(resjson => {
|
|
switch (res.status) {
|
|
case 201:
|
|
data.files = resjson.edges.files;
|
|
showImage();
|
|
break;
|
|
case 401:
|
|
alert("提交失败:需要验证");
|
|
break;
|
|
default:
|
|
alert(`提交失败:${resjson.msg}: ${resjson.error}`);
|
|
break;
|
|
}
|
|
})
|
|
})
|
|
.finally(() => {
|
|
sidebarImageSettingUploadButton.style.backgroundColor = "rgb(50, 50, 50)";
|
|
sidebarImageSettingUploadURLInput.value = "";
|
|
sidebarImageSettingUploadFileButton.value = "";
|
|
SettingSubmitting = false;
|
|
})
|
|
.catch(error => {
|
|
alert(`请求失败: ${error}`);
|
|
});
|
|
} else {
|
|
for (const file of files) {
|
|
const formData = new FormData();
|
|
formData.append("file", file);
|
|
fetch(`/api/artworks/${data.artwork.id}/files`, {
|
|
method: "POST",
|
|
body: formData,
|
|
})
|
|
.then(res => {
|
|
res.json().then(resjson => {
|
|
switch (res.status) {
|
|
case 201:
|
|
data.files = resjson.edges.files;
|
|
showImage();
|
|
break;
|
|
case 401:
|
|
alert("提交失败:需要验证");
|
|
break;
|
|
default:
|
|
alert(`提交失败:${resjson.msg}: ${resjson.error}`);
|
|
break;
|
|
}
|
|
})
|
|
})
|
|
.finally(() => {
|
|
sidebarImageSettingUploadButton.style.backgroundColor = "rgb(50, 50, 50)";
|
|
sidebarImageSettingUploadURLInput.value = "";
|
|
sidebarImageSettingUploadFileButton.value = "";
|
|
SettingSubmitting = false;
|
|
})
|
|
.catch(error => {
|
|
alert(`请求失败: ${error}`);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
function postSource() {
|
|
const link = sidebarImageSettingPostInput.value;
|
|
if (link == "" || SettingSubmitting == true) return;
|
|
SettingSubmitting = true;
|
|
sidebarImageSettingPostButton.style.backgroundColor = "rgb(50, 80, 80)";
|
|
|
|
const formData = new FormData();
|
|
formData.append("link", link);
|
|
|
|
fetch(`/api/artworks/${data.artwork.id}/posts`, {
|
|
method: "POST",
|
|
body: formData,
|
|
})
|
|
.then(res => {
|
|
res.json().then(resjson => {
|
|
switch (res.status) {
|
|
case 200:
|
|
data.posts = resjson.edges.posts;
|
|
data.socials = resjson.edges.socials;
|
|
data.tags = resjson.edges.tags;
|
|
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(() => {
|
|
sidebarImageSettingPostButton.style.backgroundColor = "rgb(50, 50, 50)";
|
|
sidebarImageSettingPostInput.value = "";
|
|
SettingSubmitting = false;
|
|
})
|
|
.catch(error => {
|
|
alert(`请求失败: ${error}`);
|
|
});
|
|
}
|
|
|
|
function patchArtwork() {
|
|
if (SettingSubmitting) return;
|
|
SettingSubmitting = true;
|
|
|
|
let Svalue = sidebarImageSettingSselect.value;
|
|
if (Svalue < 0 || Svalue > 5) {
|
|
SettingSubmitting = false;
|
|
return;
|
|
}
|
|
let Pvalue = sidebarImageSettingPselect.value;
|
|
|
|
const formData = new FormData();
|
|
formData.append("private", Pvalue);
|
|
formData.append("sensitive", Svalue);
|
|
|
|
sidebarImageSettingButton.style.backgroundColor = "rgb(50, 80, 80)";
|
|
fetch(`/api/artworks/${data.artwork.id}`, {
|
|
method: "PATCH",
|
|
body: formData,
|
|
})
|
|
.then(res => {
|
|
res.json().then(resjson => {
|
|
switch (res.status) {
|
|
case 200:
|
|
data.artwork.sensitive = resjson.sensitive ? resjson.sensitive : 0;
|
|
data.artwork.private = resjson.private ? true : false;
|
|
updateInfoBar();
|
|
break;
|
|
case 401:
|
|
alert("提交失败:需要验证");
|
|
break;
|
|
default:
|
|
alert(`提交失败:${resjson.msg}: ${resjson.error}`);
|
|
break;
|
|
}
|
|
})
|
|
})
|
|
.finally(() => {
|
|
SettingSubmitting = false;
|
|
setInterval(() => {
|
|
sidebarImageSettingButton.style.backgroundColor = "rgb(50, 50, 50)";
|
|
}, 1000)
|
|
})
|
|
.catch(error => {
|
|
alert(`请求失败: ${error}`);
|
|
});
|
|
}
|
|
|
|
function showImage() {
|
|
previewImg.style.opacity = 1;
|
|
viewer.reset();
|
|
let image = data.files[index];
|
|
previewImg.src = `/files/${image.id_str}.${image.ext}`;
|
|
updateNavButtons();
|
|
updateInfoBar();
|
|
|
|
previewImg.onerror = () => {
|
|
alert("Failed to load image");
|
|
};
|
|
}
|
|
|
|
function showPrevImage() {
|
|
if (index > 0) {
|
|
index = index - 1;
|
|
showImage();
|
|
}
|
|
}
|
|
|
|
function showNextImage() {
|
|
if (index < data.files.length - 1) {
|
|
index = index + 1;
|
|
showImage();
|
|
}
|
|
}
|
|
|
|
function handleKeyDown(e) {
|
|
if (document.activeElement === sidebarImageSettingPostInput || document.activeElement === sidebarImageSettingUploadURLInput) return;
|
|
|
|
switch (e.key) {
|
|
case "Escape":
|
|
if (sidebar.classList.contains("open")) {
|
|
closeSidebar();
|
|
} else {
|
|
window.history.back();
|
|
}
|
|
break;
|
|
case "ArrowRight":
|
|
showNextImage();
|
|
break;
|
|
case "ArrowLeft":
|
|
showPrevImage();
|
|
break;
|
|
case "b":
|
|
if (e.ctrlKey) {
|
|
toggleSidebar();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
function updateNavButtons() {
|
|
prevBtn.style.opacity = index <= 0 ? "0" : "1";
|
|
prevBtn.style.pointerEvents = index <= 0 ? "none" : "auto";
|
|
|
|
nextBtn.style.opacity = index >= data.files.length - 1 ? "0" : "1";
|
|
nextBtn.style.pointerEvents = index >= data.files.length - 1 ? "none" : "auto";
|
|
|
|
history.replaceState(null, null, `/artworks/${data.artwork.id}${index > 0 ? `/photo/${index}` : ""}`);
|
|
}
|
|
|
|
function updateInfoBar() {
|
|
sidebarImageAuthor.replaceChildren();
|
|
data.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);
|
|
sidebarImageAuthor.appendChild(author);
|
|
});
|
|
|
|
sidebarImageSource.replaceChildren();
|
|
data.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);
|
|
}
|
|
|
|
sidebarImageSource.appendChild(source);
|
|
})
|
|
|
|
sidebarImageTag.replaceChildren();
|
|
data.tags.forEach(tag => {
|
|
const tagLink = document.createElement("a");
|
|
tagLink.textContent = `#${tag.text}`;
|
|
tagLink.href = `/artworks/?tags=${tag.id}`;
|
|
tagLink.target = "_blank";
|
|
sidebarImageTag.appendChild(tagLink);
|
|
})
|
|
|
|
const file = data.files[index];
|
|
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${data.artwork.sensitive} ${data.artwork.private ? " (private)" : ""}`;
|
|
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`;
|
|
sidebarImageInfo.appendChild(a);
|
|
}
|
|
|
|
sidebarImageSettingSselect.value = data.artwork.sensitive ? data.artwork.sensitive : 0;
|
|
sidebarImageSettingPselect.value = Boolean(data.artwork.private);
|
|
sidebarImageSettingPselect.style.backgroundColor = data.artwork.private ? "rgb(80, 70, 50)" : "rgb(50, 80, 50)";
|
|
}
|
|
|
|
function openSidebar() {
|
|
localStorage.setItem("sidebar_on", true);
|
|
sidebar.style.width = "30vw";
|
|
sidebar.classList.add("open");
|
|
navControls.classList.add("min-hide");
|
|
sidebarBtn.style.backgroundColor = "rgba(105, 105, 105, 0.3)";
|
|
}
|
|
|
|
function closeSidebar() {
|
|
localStorage.setItem("sidebar_on", false);
|
|
sidebar.style.width = "0px";
|
|
sidebar.classList.remove("open");
|
|
navControls.classList.remove("min-hide");
|
|
sidebarBtn.style.backgroundColor = "";
|
|
}
|
|
|
|
function toggleSidebar() {
|
|
if (sidebar.classList.contains("open")) {
|
|
closeSidebar();
|
|
} else {
|
|
openSidebar();
|
|
}
|
|
}
|
|
|
|
function downloadImage() {
|
|
const file = data.files[index];
|
|
const post = data.posts[0];
|
|
const link = document.createElement("a");
|
|
link.download = `${post.platform}_${post.post_id}_trpic_${file.id_str}.${file.ext}`;
|
|
link.href = `/files/${file.id_str}.${file.ext}`;
|
|
link.click();
|
|
link.remove();
|
|
}
|
|
|
|
function deleteImage() {
|
|
const file = data.files[index];
|
|
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) {
|
|
data.files.splice(index, 1);
|
|
index = 0;
|
|
showImage();
|
|
} 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?`)) deleteArtwork();
|
|
return
|
|
}
|
|
alert(`Failed to delete image: ${resjson.msg}`);
|
|
});
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
alert(`Failed to delete image: ${err}`);
|
|
});
|
|
}
|
|
|
|
function deleteArtwork() {
|
|
fetch(`/api/artworks/${data.artwork.id}`, {
|
|
method: "DELETE",
|
|
})
|
|
.then((response) => {
|
|
if (response.ok) {
|
|
window.location.href = "/";
|
|
} else {
|
|
response.json().then((resjson) => {
|
|
alert(`Failed to delete artwork: ${resjson.msg}`);
|
|
});
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error(err);
|
|
alert(`Failed to delete artwork: ${err}`);
|
|
});
|
|
}
|
|
});
|