增加标准和小格子瀑布流显示 小格子模式支持获取图片和作者信息
This commit is contained in:
1 parent
80cd41fe03
commit
9e437d86b2
2 files changed
+534
-47
No files matched your search
@@ -811,6 +811,10 @@ async def random_image(
|
||||
"bookmark_count": getattr(image, "bookmark_count", None),
|
||||
"view_count": getattr(image, "view_count", None),
|
||||
"comment_count": getattr(image, "comment_count", None),
|
||||
"user": {
|
||||
"id": str(image.user_id) if image.user_id is not None else None,
|
||||
"name": image.user_name,
|
||||
},
|
||||
},
|
||||
"urls": {
|
||||
"proxy": f"/i/{image.id}.{image.ext}",
|
||||
|
||||
@@ -129,6 +129,38 @@ def _build_wtf_html(*, base_url: str) -> str:
|
||||
color: rgba(43, 29, 22, 0.90);
|
||||
}}
|
||||
.btn:hover {{ filter: brightness(0.98); }}
|
||||
|
||||
.seg {{
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--border);
|
||||
background: rgba(43, 29, 22, 0.06);
|
||||
border-radius: 999px;
|
||||
padding: 2px;
|
||||
gap: 2px;
|
||||
}}
|
||||
.segbtn {{
|
||||
cursor: pointer;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
border-radius: 999px;
|
||||
padding: 7px 10px;
|
||||
font-size: 12px;
|
||||
color: rgba(43, 29, 22, 0.78);
|
||||
user-select: none;
|
||||
white-space: nowrap;
|
||||
}}
|
||||
.segbtn:hover {{ filter: brightness(0.98); }}
|
||||
.segbtn.active {{
|
||||
background: var(--card-2);
|
||||
color: rgba(43, 29, 22, 0.92);
|
||||
box-shadow: 0 6px 18px rgba(40, 24, 16, 0.10);
|
||||
}}
|
||||
.segbtn:focus-visible {{
|
||||
outline: 2px solid rgba(162, 82, 44, 0.55);
|
||||
outline-offset: 2px;
|
||||
}}
|
||||
|
||||
.muted {{ color: var(--muted2); }}
|
||||
code {{
|
||||
font-family: var(--mono);
|
||||
@@ -173,6 +205,24 @@ def _build_wtf_html(*, base_url: str) -> str:
|
||||
.item.pending img {{
|
||||
aspect-ratio: var(--ar, 16 / 9);
|
||||
}}
|
||||
.meta {{
|
||||
display: none;
|
||||
padding: 8px 10px 10px;
|
||||
border-top: 1px solid rgba(58, 38, 26, 0.10);
|
||||
background: rgba(255, 250, 243, 0.92);
|
||||
color: rgba(67, 51, 44, 0.78);
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}}
|
||||
.meta a {{
|
||||
color: var(--link);
|
||||
text-decoration: none;
|
||||
font-family: var(--mono);
|
||||
font-size: 11px;
|
||||
}}
|
||||
.meta a:hover {{ text-decoration: underline; }}
|
||||
.item img {{
|
||||
width: 100%;
|
||||
height: auto;
|
||||
@@ -191,6 +241,119 @@ def _build_wtf_html(*, base_url: str) -> str:
|
||||
color: var(--muted2);
|
||||
font-size: 11px;
|
||||
}}
|
||||
|
||||
body.view-masonry .wrap {{
|
||||
max-width: none;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
}}
|
||||
body.view-masonry .top {{
|
||||
margin: 0 16px;
|
||||
}}
|
||||
@media (max-width: 520px) {{
|
||||
body.view-masonry .top {{ margin: 0 12px; }}
|
||||
}}
|
||||
body.view-masonry .feed {{
|
||||
margin-top: 14px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 0;
|
||||
align-items: flex-start;
|
||||
width: 100%;
|
||||
}}
|
||||
body.view-masonry .mcol {{
|
||||
flex: 1 1 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
line-height: 0;
|
||||
min-width: 0;
|
||||
}}
|
||||
body.view-masonry .item {{
|
||||
width: 100%;
|
||||
break-inside: avoid;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
contain-intrinsic-size: 620px;
|
||||
}}
|
||||
body.view-masonry .item.pending {{
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
box-shadow: none;
|
||||
}}
|
||||
body.view-masonry .item img {{
|
||||
border-radius: 0;
|
||||
transition: opacity 180ms ease;
|
||||
transform: none;
|
||||
}}
|
||||
body.view-masonry .item.loaded img {{
|
||||
transform: none;
|
||||
}}
|
||||
|
||||
body.view-tiles .feed {{
|
||||
margin-top: 14px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 16px;
|
||||
}}
|
||||
@media (max-width: 1100px) {{
|
||||
body.view-tiles .feed {{
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 14px;
|
||||
}}
|
||||
}}
|
||||
@media (max-width: 760px) {{
|
||||
body.view-tiles .feed {{
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}}
|
||||
}}
|
||||
@media (max-width: 520px) {{
|
||||
body.view-tiles .feed {{
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}}
|
||||
}}
|
||||
body.view-tiles .item {{
|
||||
border-radius: 14px;
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 10px 30px rgba(40, 24, 16, 0.06);
|
||||
transition: transform 120ms ease, box-shadow 180ms ease, border-color 180ms ease;
|
||||
}}
|
||||
body.view-tiles .item:hover {{
|
||||
transform: translateY(-1px);
|
||||
border-color: rgba(162, 82, 44, 0.30);
|
||||
box-shadow: 0 14px 36px rgba(40, 24, 16, 0.10);
|
||||
}}
|
||||
body.view-tiles .item:focus-within {{
|
||||
border-color: rgba(162, 82, 44, 0.42);
|
||||
}}
|
||||
body.view-tiles .item img {{
|
||||
aspect-ratio: var(--tile-ar, 16 / 9);
|
||||
object-fit: cover;
|
||||
transform: none;
|
||||
}}
|
||||
body.view-tiles .item.pending img {{
|
||||
aspect-ratio: var(--tile-ar, var(--ar, 16 / 9));
|
||||
}}
|
||||
body.view-tiles .meta {{
|
||||
display: flex;
|
||||
}}
|
||||
body.view-tiles .meta a {{
|
||||
padding: 2px 6px;
|
||||
border-radius: 8px;
|
||||
}}
|
||||
body.view-tiles .meta a:hover {{
|
||||
background: rgba(192, 112, 70, 0.14);
|
||||
text-decoration: none;
|
||||
}}
|
||||
body.view-tiles .meta a:focus-visible {{
|
||||
outline: 2px solid rgba(162, 82, 44, 0.55);
|
||||
outline-offset: 2px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -207,6 +370,11 @@ def _build_wtf_html(*, base_url: str) -> str:
|
||||
</div>
|
||||
<div class="row">
|
||||
<button class="btn" id="toggle">暂停加载</button>
|
||||
<div class="seg" id="viewSeg" role="tablist" aria-label="布局切换">
|
||||
<button class="segbtn" type="button" data-view="single" role="tab" aria-selected="true">单列</button>
|
||||
<button class="segbtn" type="button" data-view="masonry" role="tab" aria-selected="false">标准瀑布流</button>
|
||||
<button class="segbtn" type="button" data-view="tiles" role="tab" aria-selected="false">小格子</button>
|
||||
</div>
|
||||
<span class="muted" id="info"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -220,18 +388,142 @@ def _build_wtf_html(*, base_url: str) -> str:
|
||||
const sentinel = document.getElementById("sentinel");
|
||||
const info = document.getElementById("info");
|
||||
const toggle = document.getElementById("toggle");
|
||||
const viewSeg = document.getElementById("viewSeg");
|
||||
const viewButtons = Array.from(viewSeg.querySelectorAll("button[data-view]"));
|
||||
|
||||
const baseParams = new URLSearchParams(window.location.search);
|
||||
const qs = new URLSearchParams(window.location.search);
|
||||
const VIEW_KEY = "wtf_view";
|
||||
|
||||
const allItems = [];
|
||||
let masonryCols = [];
|
||||
let masonryHeights = [];
|
||||
let masonryCount = 0;
|
||||
|
||||
function normalizeView(raw) {{
|
||||
const v = String(raw || "").trim().toLowerCase();
|
||||
if (v === "masonry" || v === "mason" || v === "standard") return "masonry";
|
||||
if (v === "tiles" || v === "tile" || v === "grid" || v === "small") return "tiles";
|
||||
return "single";
|
||||
}}
|
||||
|
||||
function ratioToNumber(r) {{
|
||||
const s = String(r || "").replace(/\\s+/g, "");
|
||||
const m = s.match(/^(\\d+(?:\\.\\d+)?)\\/(\\d+(?:\\.\\d+)?)$/);
|
||||
if (!m) return 0.75;
|
||||
const w = Number(m[1]);
|
||||
const h = Number(m[2]);
|
||||
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) return 0.75;
|
||||
return h / w;
|
||||
}}
|
||||
|
||||
function desiredMasonryCount() {{
|
||||
const w = Math.max(0, document.documentElement.clientWidth || window.innerWidth || 0);
|
||||
if (w <= 520) return 1;
|
||||
if (w <= 980) return 2;
|
||||
return 3;
|
||||
}}
|
||||
|
||||
function masonryColWidth() {{
|
||||
const w = Math.max(320, feed.clientWidth || document.documentElement.clientWidth || window.innerWidth || 1000);
|
||||
return masonryCount > 0 ? (w / masonryCount) : w;
|
||||
}}
|
||||
|
||||
function masonryEstimateHeight(item) {{
|
||||
const ratio = Number(item && item.dataset ? item.dataset.ratio : NaN);
|
||||
const r = Number.isFinite(ratio) && ratio > 0 ? ratio : 0.75;
|
||||
return masonryColWidth() * r;
|
||||
}}
|
||||
|
||||
function rebuildLayout() {{
|
||||
try {{ feed.textContent = ""; }} catch (e) {{}}
|
||||
masonryCols = [];
|
||||
masonryHeights = [];
|
||||
masonryCount = 0;
|
||||
|
||||
if (viewMode === "masonry") {{
|
||||
masonryCount = desiredMasonryCount();
|
||||
masonryHeights = new Array(masonryCount).fill(0);
|
||||
for (let i = 0; i < masonryCount; i++) {{
|
||||
const col = document.createElement("div");
|
||||
col.className = "mcol";
|
||||
masonryCols.push(col);
|
||||
feed.appendChild(col);
|
||||
}}
|
||||
|
||||
for (const item of allItems) {{
|
||||
const h = masonryEstimateHeight(item);
|
||||
let best = 0;
|
||||
for (let i = 1; i < masonryHeights.length; i++) {{
|
||||
if (masonryHeights[i] < masonryHeights[best]) best = i;
|
||||
}}
|
||||
item.dataset.mcol = String(best);
|
||||
item.dataset.estH = String(h);
|
||||
masonryHeights[best] += h;
|
||||
masonryCols[best].appendChild(item);
|
||||
}}
|
||||
return;
|
||||
}}
|
||||
|
||||
for (const item of allItems) {{
|
||||
try {{
|
||||
delete item.dataset.mcol;
|
||||
delete item.dataset.estH;
|
||||
}} catch (e) {{}}
|
||||
feed.appendChild(item);
|
||||
}}
|
||||
}}
|
||||
|
||||
function mountNewItem(item) {{
|
||||
allItems.push(item);
|
||||
if (viewMode !== "masonry") {{
|
||||
feed.appendChild(item);
|
||||
return;
|
||||
}}
|
||||
|
||||
if (!masonryCols.length || masonryCount !== desiredMasonryCount()) {{
|
||||
rebuildLayout();
|
||||
return;
|
||||
}}
|
||||
|
||||
const h = masonryEstimateHeight(item);
|
||||
let best = 0;
|
||||
for (let i = 1; i < masonryHeights.length; i++) {{
|
||||
if (masonryHeights[i] < masonryHeights[best]) best = i;
|
||||
}}
|
||||
item.dataset.mcol = String(best);
|
||||
item.dataset.estH = String(h);
|
||||
masonryHeights[best] += h;
|
||||
masonryCols[best].appendChild(item);
|
||||
}}
|
||||
|
||||
function updateMasonryEstimate(item) {{
|
||||
if (viewMode !== "masonry") return;
|
||||
if (!item || !item.dataset) return;
|
||||
const idx = Number(item.dataset.mcol);
|
||||
if (!Number.isInteger(idx) || idx < 0 || idx >= masonryHeights.length) return;
|
||||
const oldH = Number(item.dataset.estH);
|
||||
const prev = Number.isFinite(oldH) && oldH > 0 ? oldH : 0;
|
||||
const next = masonryEstimateHeight(item);
|
||||
masonryHeights[idx] = Math.max(0, masonryHeights[idx] - prev + next);
|
||||
item.dataset.estH = String(next);
|
||||
}}
|
||||
|
||||
let storedView = "";
|
||||
try {{ storedView = localStorage.getItem(VIEW_KEY) || ""; }} catch (e) {{}}
|
||||
|
||||
const baseParams = new URLSearchParams(qs);
|
||||
baseParams.delete("view");
|
||||
baseParams.delete("format");
|
||||
baseParams.delete("redirect");
|
||||
baseParams.delete("t");
|
||||
if (!baseParams.has("adaptive")) baseParams.set("adaptive", "1");
|
||||
baseParams.set("redirect", "1");
|
||||
|
||||
const baseQuery = baseParams.toString();
|
||||
info.textContent = baseQuery ? `当前过滤: ?${{baseQuery}}` : "当前过滤: (无)";
|
||||
const showParams = new URLSearchParams(baseParams);
|
||||
const baseQuery = showParams.toString();
|
||||
info.textContent = baseQuery ? ("当前过滤: ?" + baseQuery) : "当前过滤: (无)";
|
||||
|
||||
let paused = false;
|
||||
let viewMode = normalizeView(qs.get("view") || storedView || "single");
|
||||
let seq = 0;
|
||||
let inflight = 0;
|
||||
let rendered = 0;
|
||||
@@ -242,24 +534,7 @@ def _build_wtf_html(*, base_url: str) -> str:
|
||||
return window.matchMedia && window.matchMedia("(max-width: 520px)").matches;
|
||||
}}
|
||||
|
||||
function cfg() {{
|
||||
const mobile = isMobile();
|
||||
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||||
const effectiveType = conn && conn.effectiveType ? String(conn.effectiveType) : "";
|
||||
const slow = effectiveType.includes("2g") || effectiveType.includes("3g");
|
||||
|
||||
const base = mobile
|
||||
? {{ initial: 12, step: 8, maxInflight: 12 }}
|
||||
: {{ initial: 18, step: 14, maxInflight: 22 }};
|
||||
|
||||
return {{
|
||||
initial: base.initial,
|
||||
step: base.step,
|
||||
maxInflight: slow ? Math.max(6, Math.floor(base.maxInflight * 0.6)) : base.maxInflight,
|
||||
}};
|
||||
}}
|
||||
|
||||
function pickSkeletonRatio() {{
|
||||
function pickRatioFromParams() {{
|
||||
const o = String(baseParams.get("orientation") || "").trim().toLowerCase();
|
||||
if (o === "square") return "1 / 1";
|
||||
if (o === "portrait") return "2 / 3";
|
||||
@@ -272,23 +547,149 @@ def _build_wtf_html(*, base_url: str) -> str:
|
||||
return isMobile() ? "3 / 4" : "16 / 9";
|
||||
}}
|
||||
|
||||
function buildUrl() {{
|
||||
function cfg() {{
|
||||
const mobile = isMobile();
|
||||
const conn = navigator.connection || navigator.mozConnection || navigator.webkitConnection;
|
||||
const effectiveType = conn && conn.effectiveType ? String(conn.effectiveType) : "";
|
||||
const slow = effectiveType.includes("2g") || effectiveType.includes("3g");
|
||||
|
||||
let initial = mobile ? 12 : 18;
|
||||
let step = mobile ? 8 : 14;
|
||||
let maxInflight = mobile ? 12 : 22;
|
||||
|
||||
if (viewMode === "masonry") {{
|
||||
initial = mobile ? 14 : 24;
|
||||
step = mobile ? 10 : 16;
|
||||
maxInflight = mobile ? 14 : 22;
|
||||
}} else if (viewMode === "tiles") {{
|
||||
initial = mobile ? 16 : 24;
|
||||
step = mobile ? 10 : 14;
|
||||
maxInflight = mobile ? 16 : 20;
|
||||
}}
|
||||
|
||||
if (slow) {{
|
||||
maxInflight = Math.max(6, Math.floor(maxInflight * 0.6));
|
||||
}}
|
||||
|
||||
return {{ initial: initial, step: step, maxInflight: maxInflight }};
|
||||
}}
|
||||
|
||||
function applyView(mode, persist) {{
|
||||
viewMode = normalizeView(mode);
|
||||
document.body.classList.toggle("view-single", viewMode === "single");
|
||||
document.body.classList.toggle("view-masonry", viewMode === "masonry");
|
||||
document.body.classList.toggle("view-tiles", viewMode === "tiles");
|
||||
|
||||
for (const b of viewButtons) {{
|
||||
const on = String(b.dataset.view || "") === viewMode;
|
||||
b.classList.toggle("active", on);
|
||||
b.setAttribute("aria-selected", on ? "true" : "false");
|
||||
}}
|
||||
|
||||
if (viewMode === "tiles") {{
|
||||
document.documentElement.style.setProperty("--tile-ar", pickRatioFromParams());
|
||||
}} else {{
|
||||
document.documentElement.style.removeProperty("--tile-ar");
|
||||
}}
|
||||
|
||||
if (persist) {{
|
||||
try {{ localStorage.setItem(VIEW_KEY, viewMode); }} catch (e) {{}}
|
||||
}}
|
||||
|
||||
rebuildLayout();
|
||||
|
||||
const c = cfg();
|
||||
target = Math.max(target, c.initial);
|
||||
ensure();
|
||||
updateSentinel();
|
||||
}}
|
||||
|
||||
for (const b of viewButtons) {{
|
||||
b.addEventListener("click", () => {{
|
||||
applyView(String(b.dataset.view || "single"), true);
|
||||
}});
|
||||
}}
|
||||
applyView(viewMode, false);
|
||||
|
||||
function buildRandomJsonUrl() {{
|
||||
const p = new URLSearchParams(baseParams);
|
||||
p.set("format", "simple_json");
|
||||
p.set("t", String(Date.now()) + "_" + String(seq++));
|
||||
return "/random?" + p.toString();
|
||||
}}
|
||||
|
||||
function updateSentinel() {{
|
||||
const status = paused ? "已暂停" : "加载中";
|
||||
sentinel.textContent = `${{status}} · 已显示 ${{rendered}} 张 · in-flight ${{inflight}}`;
|
||||
function buildProxyQuery() {{
|
||||
const qp = new URLSearchParams();
|
||||
const pc = String(baseParams.get("pixiv_cat") || "").trim();
|
||||
if (pc === "1") qp.set("pixiv_cat", "1");
|
||||
const mh = String(baseParams.get("pximg_mirror_host") || "").trim();
|
||||
if (mh) qp.set("pximg_mirror_host", mh);
|
||||
const s = qp.toString();
|
||||
return s ? ("?" + s) : "";
|
||||
}}
|
||||
|
||||
function appendItem(imgEl) {{
|
||||
const item = document.createElement("div");
|
||||
item.className = "item";
|
||||
item.appendChild(imgEl);
|
||||
feed.appendChild(item);
|
||||
requestAnimationFrame(() => item.classList.add("loaded"));
|
||||
async function fetchRandomData() {{
|
||||
const url = buildRandomJsonUrl();
|
||||
const resp = await fetch(url, {{ cache: "no-store" }});
|
||||
if (resp.status === 404) {{
|
||||
const e = new Error("NO_MATCH");
|
||||
e.name = "NO_MATCH";
|
||||
throw e;
|
||||
}}
|
||||
if (!resp.ok) {{
|
||||
throw new Error("HTTP_" + String(resp.status));
|
||||
}}
|
||||
const body = await resp.json();
|
||||
if (!body || body.ok !== true || !body.data || !body.data.urls || !body.data.image) {{
|
||||
throw new Error("BAD_BODY");
|
||||
}}
|
||||
return body.data;
|
||||
}}
|
||||
|
||||
function updateSentinel() {{
|
||||
const status = paused ? "已暂停" : "加载中";
|
||||
sentinel.textContent = status + " · 已显示 " + String(rendered) + " 张 · in-flight " + String(inflight);
|
||||
}}
|
||||
|
||||
function setMeta(meta, data) {{
|
||||
try {{
|
||||
meta.textContent = "";
|
||||
const image = data && data.image ? data.image : null;
|
||||
if (!image) return;
|
||||
|
||||
const illustId = String(image.illust_id || "").trim();
|
||||
const user = image.user || null;
|
||||
const userId = user && user.id ? String(user.id).trim() : "";
|
||||
const userName = user && user.name ? String(user.name).trim() : "";
|
||||
|
||||
const links = [];
|
||||
if (userId) {{
|
||||
const a = document.createElement("a");
|
||||
a.href = "https://www.pixiv.net/users/" + userId;
|
||||
a.target = "_blank";
|
||||
a.rel = "noreferrer noopener";
|
||||
a.textContent = "pixiv.net/users/" + userId;
|
||||
if (userName) a.title = userName;
|
||||
links.push(a);
|
||||
}}
|
||||
if (illustId) {{
|
||||
const a = document.createElement("a");
|
||||
a.href = "https://www.pixiv.net/artworks/" + illustId;
|
||||
a.target = "_blank";
|
||||
a.rel = "noreferrer noopener";
|
||||
a.textContent = "pixiv.net/artworks/" + illustId;
|
||||
links.push(a);
|
||||
}}
|
||||
|
||||
for (let i = 0; i < links.length; i++) {{
|
||||
if (i > 0) {{
|
||||
const sep = document.createElement("span");
|
||||
sep.textContent = " · ";
|
||||
meta.appendChild(sep);
|
||||
}}
|
||||
meta.appendChild(links[i]);
|
||||
}}
|
||||
}} catch (e) {{}}
|
||||
}}
|
||||
|
||||
function startOne() {{
|
||||
@@ -302,22 +703,29 @@ def _build_wtf_html(*, base_url: str) -> str:
|
||||
|
||||
const item = document.createElement("div");
|
||||
item.className = "item pending";
|
||||
item.style.setProperty("--ar", pickSkeletonRatio());
|
||||
const fallbackAr = pickRatioFromParams();
|
||||
item.style.setProperty("--ar", fallbackAr);
|
||||
item.dataset.ratio = String(ratioToNumber(fallbackAr));
|
||||
|
||||
const img = new Image();
|
||||
const img = document.createElement("img");
|
||||
img.decoding = "async";
|
||||
img.loading = "lazy";
|
||||
img.referrerPolicy = "no-referrer";
|
||||
img.alt = "";
|
||||
|
||||
const meta = document.createElement("div");
|
||||
meta.className = "meta";
|
||||
|
||||
item.appendChild(img);
|
||||
feed.appendChild(item);
|
||||
item.appendChild(meta);
|
||||
mountNewItem(item);
|
||||
|
||||
let tries = 0;
|
||||
const load = () => {{
|
||||
tries += 1;
|
||||
img.src = buildUrl();
|
||||
}};
|
||||
let done = false;
|
||||
|
||||
img.onload = () => {{
|
||||
const finishOk = () => {{
|
||||
if (done) return;
|
||||
done = true;
|
||||
inflight = Math.max(0, inflight - 1);
|
||||
rendered += 1;
|
||||
failStreak = 0;
|
||||
@@ -326,25 +734,97 @@ def _build_wtf_html(*, base_url: str) -> str:
|
||||
updateSentinel();
|
||||
ensure();
|
||||
}};
|
||||
img.onerror = () => {{
|
||||
if (tries < 3) {{
|
||||
setTimeout(load, 250 * tries);
|
||||
return;
|
||||
}}
|
||||
|
||||
const finishFail = (reason) => {{
|
||||
if (done) return;
|
||||
done = true;
|
||||
inflight = Math.max(0, inflight - 1);
|
||||
failStreak += 1;
|
||||
try {{
|
||||
const idx = allItems.indexOf(item);
|
||||
if (idx >= 0) allItems.splice(idx, 1);
|
||||
}} catch (e) {{}}
|
||||
try {{
|
||||
const colIdx = Number(item.dataset.mcol);
|
||||
const est = Number(item.dataset.estH);
|
||||
if (Number.isInteger(colIdx) && colIdx >= 0 && colIdx < masonryHeights.length && Number.isFinite(est) && est > 0) {{
|
||||
masonryHeights[colIdx] = Math.max(0, masonryHeights[colIdx] - est);
|
||||
}}
|
||||
}} catch (e) {{}}
|
||||
try {{ item.remove(); }} catch (e) {{}}
|
||||
updateSentinel();
|
||||
if (String(reason || "") === "NO_MATCH") {{
|
||||
paused = true;
|
||||
toggle.textContent = "继续加载";
|
||||
sentinel.textContent = "没有匹配结果:请放宽过滤条件后重试。";
|
||||
return;
|
||||
}}
|
||||
if (failStreak >= 8) {{
|
||||
paused = true;
|
||||
toggle.textContent = "继续加载";
|
||||
sentinel.textContent = "连续失败较多:可能网络不稳定或当前过滤无匹配,请放宽条件后重试。";
|
||||
sentinel.textContent = "连续失败较多:可能网络不稳定,请稍后再试或放宽条件。";
|
||||
return;
|
||||
}}
|
||||
ensure();
|
||||
}};
|
||||
|
||||
load();
|
||||
const attempt = async () => {{
|
||||
tries += 1;
|
||||
try {{
|
||||
const data = await fetchRandomData();
|
||||
setMeta(meta, data);
|
||||
try {{
|
||||
const iw = data && data.image ? Number(data.image.width) : NaN;
|
||||
const ih = data && data.image ? Number(data.image.height) : NaN;
|
||||
if (Number.isFinite(iw) && Number.isFinite(ih) && iw > 0 && ih > 0) {{
|
||||
img.setAttribute("width", String(iw));
|
||||
img.setAttribute("height", String(ih));
|
||||
item.style.setProperty("--ar", String(iw) + " / " + String(ih));
|
||||
item.dataset.ratio = String(ih / iw);
|
||||
updateMasonryEstimate(item);
|
||||
}}
|
||||
}} catch (e) {{}}
|
||||
const proxy = data && data.urls ? String(data.urls.proxy || "") : "";
|
||||
if (!proxy || proxy[0] !== "/") throw new Error("BAD_PROXY");
|
||||
img.src = proxy + buildProxyQuery();
|
||||
}} catch (e) {{
|
||||
if (done) return;
|
||||
const name = e && e.name ? String(e.name) : "";
|
||||
const msg = e && e.message ? String(e.message) : "";
|
||||
if (name === "NO_MATCH" || msg === "NO_MATCH") {{
|
||||
finishFail("NO_MATCH");
|
||||
return;
|
||||
}}
|
||||
if (tries < 3) {{
|
||||
setTimeout(() => attempt(), 250 * tries);
|
||||
return;
|
||||
}}
|
||||
finishFail(msg || name || "ERROR");
|
||||
}}
|
||||
}};
|
||||
|
||||
img.onload = () => {{
|
||||
try {{
|
||||
if ((!img.hasAttribute("width") || !img.hasAttribute("height")) && img.naturalWidth > 0 && img.naturalHeight > 0) {{
|
||||
img.setAttribute("width", String(img.naturalWidth));
|
||||
img.setAttribute("height", String(img.naturalHeight));
|
||||
item.style.setProperty("--ar", String(img.naturalWidth) + " / " + String(img.naturalHeight));
|
||||
item.dataset.ratio = String(img.naturalHeight / img.naturalWidth);
|
||||
updateMasonryEstimate(item);
|
||||
}}
|
||||
}} catch (e) {{}}
|
||||
finishOk();
|
||||
}};
|
||||
img.onerror = () => {{
|
||||
if (done) return;
|
||||
if (tries < 3) {{
|
||||
setTimeout(() => attempt(), 250 * tries);
|
||||
return;
|
||||
}}
|
||||
finishFail("IMAGE_ERROR");
|
||||
}};
|
||||
|
||||
attempt();
|
||||
}}
|
||||
|
||||
function ensure() {{
|
||||
@@ -402,6 +882,9 @@ def _build_wtf_html(*, base_url: str) -> str:
|
||||
window.addEventListener("resize", () => {{
|
||||
const c = cfg();
|
||||
target = Math.max(target, c.initial);
|
||||
if (viewMode === "masonry" && masonryCount !== desiredMasonryCount()) {{
|
||||
rebuildLayout();
|
||||
}}
|
||||
ensure();
|
||||
}});
|
||||
</script>
|
||||
|
||||
Reference in new issue
Block a user