add thumbhash use `github.com/galdor/go-thumbhash` and `github.com/evanw/thumbhash`
database:
add `thumbhash` column for `files` table
image:
add `EncodeThumbHash()` function use `go.n16f.net/thumbhash`
router:
add `/api/task/files/thumbhash` endpoint to check thumbhask
task:
add `CheckThumbHash()` function
web/scripts:
add thumbhash logic for `load_*` scripts
add `thumbhash.js` from `https://github.com/evanw/thumbhash/blob/main/js/thumbhash.js`, edited
web/styles/gallery:
add thumbhash transition when thumbnail loaded
adjust style for wrapped imgs
web/templates:
add `role="main"` for gallery and artwork id pages
render thumbhash script for gallery pages
186 lines
4.6 KiB
JavaScript
186 lines
4.6 KiB
JavaScript
let gallery;
|
|
let sentinel;
|
|
let counter;
|
|
let select;
|
|
let observer;
|
|
|
|
let offset = 0;
|
|
let loading = false;
|
|
let hasMore = true;
|
|
|
|
const urlParams = new URLSearchParams(window.location.search);
|
|
|
|
let height = urlParams.get("height") || (window.innerWidth > 1600 ? 320 : 220);
|
|
let sort = urlParams.get("sort") || "time";
|
|
let tags = urlParams.get("tags") || "";
|
|
const limit = window.innerWidth > 1600 ? 50 : 20;
|
|
|
|
function resetGallery() {
|
|
imgviewer.resetList();
|
|
gallery.replaceChildren();
|
|
height = urlParams.get("height") || (window.innerWidth > 1600 ? 320 : 220);
|
|
offset = 0;
|
|
hasMore = true;
|
|
loadMore();
|
|
}
|
|
|
|
async function loadMore() {
|
|
if (loading || !hasMore) return;
|
|
|
|
let url = `/api/files?offset=${offset}&limit=${limit}&sort=${sort}`
|
|
observer.unobserve(sentinel);
|
|
loading = true;
|
|
|
|
if (tags) url += `&tags=${tags}`
|
|
|
|
try {
|
|
const res = await fetch(url);
|
|
const data = await res.json();
|
|
|
|
switch (res.status) {
|
|
case 200:
|
|
break;
|
|
case 401:
|
|
alert("Session expired");
|
|
return;
|
|
case 500:
|
|
if (data.error) {
|
|
hasMore = false;
|
|
alert(`${data.msg}: ${data.error}`);
|
|
return
|
|
}
|
|
}
|
|
|
|
appendFiles(data.files);
|
|
|
|
hasMore = data.offset + data.files.length < data.total;
|
|
offset += data.files.length;
|
|
counter.textContent = data.total;
|
|
if (!sort) sort = data.sort;
|
|
} catch (err) {
|
|
console.error(err);
|
|
hasMore = false;
|
|
} finally {
|
|
loading = false;
|
|
|
|
// 加载完成下一帧重新观察,强制刷新交叉状态
|
|
requestAnimationFrame(() => {
|
|
observer.observe(sentinel);
|
|
});
|
|
}
|
|
}
|
|
|
|
function appendFiles(files) {
|
|
imgviewer.appendFiles(files);
|
|
const fragment = document.createDocumentFragment();
|
|
|
|
for (const file of files) {
|
|
const jWidth = file.width * height / file.height;
|
|
const jPadding = file.height / file.width * 100;
|
|
const wrapper = document.createElement("div");
|
|
wrapper.style.width = jWidth + "px";
|
|
wrapper.style.flexGrow = jWidth;
|
|
|
|
const i = document.createElement("i");
|
|
i.style.paddingBottom = jPadding + "%";
|
|
if (sort == "size" || sort == "size-down") {
|
|
const span = document.createElement("span");
|
|
const p = document.createElement("p");
|
|
p.textContent = `${file.ext} | ${file.thumbed ? "☇ " : ""}${file.size_str}`;
|
|
span.appendChild(p);
|
|
i.appendChild(span);
|
|
}
|
|
wrapper.appendChild(i);
|
|
|
|
const imgs = document.createElement("div");
|
|
imgs.id = "imgs";
|
|
|
|
const img = document.createElement("img");
|
|
img.loading = "lazy";
|
|
img.decoding = "async";
|
|
img.tabIndex = "0";
|
|
if (file.thumbed) {
|
|
img.src = `/thumbnails/${file.id_str}.jpg`;
|
|
img.dataset.highres = `/files/${file.id_str}.${file.ext}`;
|
|
} else {
|
|
img.src = `/files/${file.id_str}.${file.ext}`;
|
|
}
|
|
|
|
let artwork = file.edges.artwork;
|
|
img.alt = `trpic_artwork_${artwork.id}_${file.id_str}`;
|
|
img.dataset.id_str = file.id_str;
|
|
img.dataset.artwork_id = artwork.id;
|
|
|
|
imgs.appendChild(img);
|
|
|
|
if (file.thumbhash) {
|
|
const thumb = document.createElement("img");
|
|
thumb.id = "thumbhash";
|
|
thumb.src = thumbHashToDataURL(file.thumbhash);
|
|
imgs.appendChild(thumb);
|
|
|
|
img.onload = () => {
|
|
img.onload = null;
|
|
thumb.style.opacity = 0;
|
|
setInterval(() => {
|
|
thumb.remove();
|
|
}, 1000)
|
|
};
|
|
}
|
|
|
|
wrapper.appendChild(imgs);
|
|
imgviewer.bindClickEvents(img);
|
|
fragment.appendChild(wrapper);
|
|
}
|
|
|
|
gallery.appendChild(fragment);
|
|
}
|
|
|
|
document.addEventListener("DOMContentLoaded", () => {
|
|
gallery = document.getElementById("gallery");
|
|
sentinel = document.getElementById("sentinel");
|
|
counter = document.getElementById("image-count");
|
|
select = document.getElementById('sort-select')
|
|
|
|
select.value = sort
|
|
|
|
select.addEventListener('change', () => {
|
|
urlParams.set("sort", sort = select.value);
|
|
history.replaceState(null, null, `/files?${urlParams.toString()}`);
|
|
resetGallery()
|
|
})
|
|
|
|
observer = new IntersectionObserver(
|
|
(entries) => {
|
|
if (entries[0].isIntersecting) {
|
|
loadMore();
|
|
}
|
|
},
|
|
{
|
|
root: null,
|
|
rootMargin: "800px", // 提前 800px 加载
|
|
threshold: 0,
|
|
}
|
|
);
|
|
|
|
observer.observe(sentinel);
|
|
});
|
|
|
|
document.addEventListener("error", (e) => {
|
|
let target = e.target;
|
|
const tagName = target.tagName || "";
|
|
const curTimes = Number(target.dataset.retryTimes) || 0;
|
|
if (tagName.toLowerCase() === "img") {
|
|
if (curTimes >= 5) {
|
|
target.alt = "loading failed";
|
|
} else {
|
|
setTimeout(() => {
|
|
target.dataset.retryTimes = curTimes + 1;
|
|
target.src = target.src;
|
|
}, 10000);
|
|
}
|
|
} else {
|
|
target = null;
|
|
}
|
|
}, true);
|