From 54d2137fac66eaf8d153ad95ef518f451a595e78 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 19 Oct 2026 15:44:56 +0000 Subject: [PATCH] ventures: refresh havre from havre-open --- ventures/havre/app.js | 31 ++++++++++++++++++++++++++++++- ventures/havre/style.css | 2 ++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/ventures/havre/app.js b/ventures/havre/app.js index a7efb3f..9fe300a 100644 --- a/ventures/havre/app.js +++ b/ventures/havre/app.js @@ -913,12 +913,41 @@ function renderGallery() { $("#gal-empty").textContent = "no photos or clips here yet."; $("#gal-grid").replaceChildren(...items.map(({ m, t }) => { const md = m.media, clip = md.kind === "clip"; - const pic = mediaEl(clip ? "video" : "img", md.id, clip ? { muted: true, playsinline: true, preload: "metadata" } : { alt: md.shot || (md.kind === "photo-in" ? "your photo" : "a photo"), decoding: "async" }, md.kind === "clip" ? "clip" : "photo"); + const pic = clip ? clipStill(md) : mediaEl("img", md.id, { alt: md.shot || (md.kind === "photo-in" ? "your photo" : "a photo"), decoding: "async" }, "photo"); const when = m.at ? T.ago(m.at) : ""; return h("li", {}, h("button", { type: "button", title: md.shot || null, onclick: () => jumpTo(t, m) }, pic, h("span", { class: "cap", text: [clip ? "clip" : md.kind === "photo-in" ? "yours" : "photo", when, galScope === "all" && T.titleOf(t, t.msgs, 24)].filter(Boolean).join(" · ") }))); })); } +/** a clip as a still: a frame taken from it, kept for the session, under a play mark */ +const stills = new Map(); +function clipStill(md) { + const img = h("img", { alt: md.shot ? "A clip: " + md.shot : "A clip", decoding: "async" }); + const box = h("span", { class: "still" }, img, h("span", { class: "play", "aria-hidden": "true", text: "▶" })); + if (stills.has(md.id)) { img.src = stills.get(md.id); return box; } + M.url(md.id).then((u) => { + if (!u) return; + const v = document.createElement("video"); + v.muted = true; v.playsInline = true; v.preload = "auto"; v.src = u; + const grab = () => { + try { + const w = v.videoWidth, hgt = v.videoHeight; + if (!w || !hgt) return; + const s = 320 / Math.max(w, hgt), cv = document.createElement("canvas"); + cv.width = Math.round(w * s); cv.height = Math.round(hgt * s); + cv.getContext("2d").drawImage(v, 0, 0, cv.width, cv.height); + const data = cv.toDataURL("image/jpeg", 0.8); + stills.set(md.id, data); + img.src = data; + } catch { /* no still: the play mark on its own */ } + v.removeAttribute("src"); v.load(); + }; + v.addEventListener("seeked", grab, { once: true }); + v.addEventListener("loadeddata", () => { v.currentTime = Math.min(0.5, (v.duration || 1) / 2); }, { once: true }); + v.load(); + }); + return box; +} /** go to a photo or clip in its chat */ function jumpTo(t, m) { $("#gallerydlg").close(); diff --git a/ventures/havre/style.css b/ventures/havre/style.css index 7f7feca..d8aabbd 100644 --- a/ventures/havre/style.css +++ b/ventures/havre/style.css @@ -228,6 +228,8 @@ input:disabled, textarea:disabled, select:disabled { background: #f2f2f2; color: .gal button { display: block; width: 100%; padding: 0; border: 0; background: none; color: inherit; text-align: left; cursor: pointer; } .gal img, .gal video { display: block; width: 100%; aspect-ratio: 1; object-fit: cover; border: 1px solid var(--ink); background: var(--ink); } .gal button:hover img, .gal button:hover video, .gal button:focus-visible img, .gal button:focus-visible video { outline: 3px solid var(--hi); } +.gal .still { position: relative; display: block; } +.gal .play { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); padding: 6px 10px; background: var(--paper); color: var(--ink); border: 1px solid var(--ink); font: 14px/1 var(--mono); } .gal .cap { display: block; margin-top: 4px; font: 11px/1.4 var(--mono); overflow-wrap: anywhere; } /* edit / delete under a message: shown on hover or focus; always, faintly, where there's no hover (phones) */ .msgtools { grid-column: 2; font: 12px/1.4 var(--mono); opacity: 0; }