let gallery; let sentinel; 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") || "", or_tags = urlParams.get("or_tags") || "", not_tags = urlParams.get("not_tags") || ""; let socials = urlParams.get("socials") || "", or_socials = urlParams.get("or_socials") || "", not_socials = urlParams.get("not_socials") || ""; let platforms = urlParams.get("platforms") || "", or_platforms = urlParams.get("or_platforms") || "", not_platforms = urlParams.get("not_platforms") || ""; let formats = urlParams.get("formats") || "", or_formats = urlParams.get("or_formats") || "", not_formats = urlParams.get("not_formats") || ""; const limit = window.innerWidth > 1200 ? 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}` if (or_tags) url += `&or_tags=${or_tags}` if (not_tags) url += `¬_tags=${not_tags}` if (socials) url += `&socials=${socials}` if (or_socials) url += `&or_socials=${or_socials}` if (not_socials) url += `¬_socials=${not_socials}` if (platforms) url += `&platforms=${platforms}` if (or_platforms) url += `&or_platforms=${or_platforms}` if (not_platforms) url += `¬_platforms=${not_platforms}` if (formats) url += `&formats=${formats}` if (or_formats) url += `&or_formats=${or_formats}` if (not_formats) url += `¬_formats=${not_formats}` try { const res = await fetch(url); const data = await res.json(); switch (res.status) { case 200: break; case 401: alert(`Session expired: ${data.msg}: ${data.error}`); return; case 500: if (data.error) { hasMore = false; alert(`${data.msg}: ${data.error}`); return } } appendFiles(data.files); hasMore = sort != "random" && data.files.length == limit; offset += data.files.length; 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.className = "info"; i.style.paddingBottom = jPadding + "%"; wrapper.appendChild(i); const imgs = document.createElement("div"); imgs.className = "imgs"; const img = document.createElement("img"); if (gallery.childElementCount < 50) { img.fetchPriority = "high"; } else { img.loading = "lazy"; } img.decoding = "async"; img.tabIndex = "0"; img.src = file.thumbed ? `/thumbnails/${file.id}.jpg` : `/files/${file.id}.${file.ext}`; let artwork = file.edges.artwork; img.id = img.alt = `trpic_artwork_${artwork.id}_${file.id}`; img.dataset.id = file.id; img.dataset.artwork_id = artwork.id; imgs.appendChild(img); if (file.thumbhash) { const thumb = document.createElement("img"); thumb.alt = "thumbhash"; thumb.classList.add("thumbhash"); thumb.src = thumbHashToDataURL(file.thumbhash); imgs.appendChild(thumb); if (artwork.sensitive > 1) { img.tabIndex = "-1"; const m = document.createElement("a"); m.tabIndex = "0"; m.textContent = "spoiler"; m.href = `javascript:`; m.classList.add(artwork.sensitive > 2 ? "sensitive" : "nsfw"); imgs.appendChild(m); m.addEventListener("click", (e) => { thumb.style.opacity = m.style.opacity = 0; m.style.pointerEvents = "none"; if (e.ctrlKey) { thumb.remove(); m.remove(); img.focus({ preventScroll: true }); img.tabIndex = "0"; } else { setTimeout(() => { thumb.style.opacity = m.style.opacity = ""; m.style.pointerEvents = ""; m.tabIndex = "0"; img.blur(); img.tabIndex = "-1"; }, 15000) m.tabIndex = "-1"; img.tabIndex = "0"; img.focus({ preventScroll: true }); } }) } else { img.onload = () => { img.onload = null; thumb.style.opacity = 0; setTimeout(() => { thumb.remove(); }, 1000) }; } } wrapper.appendChild(imgs); imgviewer.bindClickEvents(img); fragment.appendChild(wrapper); if (artwork.edges.socials) { const tr = document.createElement("p"); tr.className = "top-right"; tr.textContent = artwork.edges.socials[0].display_name; i.appendChild(tr); } const br = document.createElement("p"); br.className = "bottom-right"; switch (sort) { case "size": case "size-down": br.textContent = `${file.ext} | ${file.size_str}`; break; case "resolution": case "resolution-down": br.textContent = `${file.width}x${file.height}px`; break; } if (br.textContent) { i.appendChild(br); } } gallery.appendChild(fragment); } document.addEventListener("DOMContentLoaded", () => { gallery = document.getElementById("gallery"); sentinel = document.getElementById("sentinel"); 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);