Commit 6f141ef1c56dc0114e8dbd0c460167fadb771293
1 parent
f19672ff
Show a first-frame thumbnail on each preview player
preload="none" left every row's player a plain black box until clicked, giving no way to eyeball which clip is which without playing it. Switch to preload="metadata" (browsers paint the first frame once metadata loads, without playing) so the row itself doubles as a thumbnail. Load it lazily via IntersectionObserver rather than src'ing all 361 rows on page load — /lib/video/:id has no local cache and re-proxies to H3 Console on every hit, so firing that many requests at once would hammer it; only rows actually scrolled into view pull their frame. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011DUiYs6UjxDMkfvnjtsKFo
Showing
1 changed file
with
18 additions
and
2 deletions
admin.html
| @@ -182,6 +182,17 @@ function videoHref(c) { | @@ -182,6 +182,17 @@ function videoHref(c) { | ||
| 182 | return `/lib/video/${c.id}`; | 182 | return `/lib/video/${c.id}`; |
| 183 | } | 183 | } |
| 184 | 184 | ||
| 185 | +const previewObserver = new IntersectionObserver( | ||
| 186 | + (entries) => { | ||
| 187 | + for (const e of entries) { | ||
| 188 | + if (!e.isIntersecting) continue; | ||
| 189 | + e.target.src = e.target.dataset.src; | ||
| 190 | + previewObserver.unobserve(e.target); | ||
| 191 | + } | ||
| 192 | + }, | ||
| 193 | + { rootMargin: "200px" } | ||
| 194 | +); | ||
| 195 | + | ||
| 185 | function render() { | 196 | function render() { |
| 186 | const rows = filteredSorted(); | 197 | const rows = filteredSorted(); |
| 187 | const channelTotal = library.filter(inChannel).length; | 198 | const channelTotal = library.filter(inChannel).length; |
| @@ -202,7 +213,7 @@ function render() { | @@ -202,7 +213,7 @@ function render() { | ||
| 202 | <td class="name"><input data-field="promptEn" value="${escapeHtml(c.promptEn || "")}" /></td> | 213 | <td class="name"><input data-field="promptEn" value="${escapeHtml(c.promptEn || "")}" /></td> |
| 203 | <td class="prompt"><textarea data-field="fullPrompt">${escapeHtml(c.fullPrompt || "")}</textarea></td> | 214 | <td class="prompt"><textarea data-field="fullPrompt">${escapeHtml(c.fullPrompt || "")}</textarea></td> |
| 204 | <td><input data-field="videoUrl" value="${escapeHtml(c.videoUrl || "")}" placeholder="留空=保持原有播放源" /></td> | 215 | <td><input data-field="videoUrl" value="${escapeHtml(c.videoUrl || "")}" placeholder="留空=保持原有播放源" /></td> |
| 205 | - <td class="preview"><video controls preload="none" src="${videoHref(c)}"></video></td> | 216 | + <td class="preview"><video controls preload="metadata" data-src="${videoHref(c)}"></video></td> |
| 206 | <td> | 217 | <td> |
| 207 | <select data-field="channel"> | 218 | <select data-field="channel"> |
| 208 | <option value="video" ${c.channel === "dance" ? "" : "selected"}>视频直播间</option> | 219 | <option value="video" ${c.channel === "dance" ? "" : "selected"}>视频直播间</option> |
| @@ -227,7 +238,7 @@ function render() { | @@ -227,7 +238,7 @@ function render() { | ||
| 227 | <td class="name">${escapeHtml(c.promptEn || "")}</td> | 238 | <td class="name">${escapeHtml(c.promptEn || "")}</td> |
| 228 | <td class="prompt" title="${escapeHtml(c.fullPrompt || "")}">${escapeHtml((c.fullPrompt || "").slice(0, 80))}${(c.fullPrompt || "").length > 80 ? "…" : ""}</td> | 239 | <td class="prompt" title="${escapeHtml(c.fullPrompt || "")}">${escapeHtml((c.fullPrompt || "").slice(0, 80))}${(c.fullPrompt || "").length > 80 ? "…" : ""}</td> |
| 229 | <td><a class="video-link" href="${videoHref(c)}" target="_blank">${videoHref(c)}</a></td> | 240 | <td><a class="video-link" href="${videoHref(c)}" target="_blank">${videoHref(c)}</a></td> |
| 230 | - <td class="preview"><video controls preload="none" src="${videoHref(c)}"></video></td> | 241 | + <td class="preview"><video controls preload="metadata" data-src="${videoHref(c)}"></video></td> |
| 231 | <td>${c.channel === "dance" ? "跳舞直播间" : "视频直播间"}</td> | 242 | <td>${c.channel === "dance" ? "跳舞直播间" : "视频直播间"}</td> |
| 232 | <td>${escapeHtml(traceability(c))}</td> | 243 | <td>${escapeHtml(traceability(c))}</td> |
| 233 | <td>${c.duration ?? "-"}s</td> | 244 | <td>${c.duration ?? "-"}s</td> |
| @@ -243,6 +254,11 @@ function render() { | @@ -243,6 +254,11 @@ function render() { | ||
| 243 | }) | 254 | }) |
| 244 | .join(""); | 255 | .join(""); |
| 245 | 256 | ||
| 257 | + // Loading preload="metadata" for all 361 rows at once would hammer the | ||
| 258 | + // /lib/video proxy (each clip is a fresh H3 Console fetch); only pull a | ||
| 259 | + // row's first frame once it's actually scrolled into view. | ||
| 260 | + tbody.querySelectorAll("td.preview video[data-src]").forEach((v) => previewObserver.observe(v)); | ||
| 261 | + | ||
| 246 | tbody.querySelectorAll("[data-edit]").forEach((b) => b.addEventListener("click", () => { editingId = b.dataset.edit; render(); })); | 262 | tbody.querySelectorAll("[data-edit]").forEach((b) => b.addEventListener("click", () => { editingId = b.dataset.edit; render(); })); |
| 247 | tbody.querySelectorAll("[data-cancel]").forEach((b) => b.addEventListener("click", () => { editingId = null; render(); })); | 263 | tbody.querySelectorAll("[data-cancel]").forEach((b) => b.addEventListener("click", () => { editingId = null; render(); })); |
| 248 | tbody.querySelectorAll("[data-save]").forEach((b) => | 264 | tbody.querySelectorAll("[data-save]").forEach((b) => |