app
Widget Registry
Public Made by Adomby adom
The reviewed registry of widgets renderable from plain markdown on any wiki page
← Commit history
adom-video: optional JSON manifest src (chapters/captions on private pages); fetch without credentials
2 files changed
+24−10
widgets.json+1@@ -162,6 +162,7 @@ "element": "adom-video", "attrs": [ "video",+ "src", "title", "poster", "chapters",
widgets/video-widget.js+23−10@@ -7,8 +7,9 @@ // captions (WebVTT or SRT, drawn by the player so + / - can resize them), speed and loop in a // settings menu, picture-in-picture, fullscreen. Self-contained: Shadow DOM, no dependencies. //-// Attributes: video (required; `src` also works outside the registry, but the wiki inlines a `src`-// file into the widget frame, so a registered tag must use `video`), title, poster, chapters (JSON [{t,title}] or a WebVTT chapter+// Attributes: video (required; never pass a video as `src`: the wiki inlines a registered tag's `src`+// file into the frame), src (optional JSON manifest {video, title, poster, chapters, captions}, which+// the wiki inlines, so chapters and captions work on private pages too), title, poster, chapters (JSON [{t,title}] or a WebVTT chapter // file), captions (.vtt or .srt), start (seconds or 1m30s), loop, muted, autoplay. // // Inside a wiki widget frame the browser blocks fullscreen (the frame carries no allow=@@ -271,14 +272,26 @@ // absolute URL for a wiki path (the frame is opaque-origin, so resolve against the real location) _abs(u) { try { return new URL(u, location.href).href; } catch { return u; } } + // a JSON manifest (the wiki inlines a registered tag's `src` file as a child script):+ // { "video": "...", "title", "poster", "chapters": [{t,title}] or a path, "captions": a path or WebVTT text }+ _manifest() {+ if (this._man !== undefined) return this._man;+ this._man = null;+ const el = this.querySelector('script[type="application/json"]');+ if (el) { try { const j = JSON.parse(el.textContent); if (j && typeof j === 'object' && !Array.isArray(j)) this._man = j; } catch {} }+ return this._man;+ }+ _opt(n) { const a = this.getAttribute(n); if (a != null && a !== '') return a; const m = this._manifest(); return m && m[n] != null ? m[n] : null; }+ _load() { this._loaded = true;- const src = this.getAttribute('video') || this.getAttribute('src');+ const m = this._manifest();+ const src = this.getAttribute('video') || (m && m.video) || (m ? null : this.getAttribute('src')); const v = this.v;- this.$('.top').textContent = this.getAttribute('title') || '';+ this.$('.top').textContent = this._opt('title') || ''; if (!src) return this._error('No video: add <code>video="/OWNER/SLUG/render/video.mp4"</code> to the tag.'); this.$('.err').classList.remove('on');- if (this.getAttribute('poster')) v.poster = this._abs(this.getAttribute('poster'));+ if (this._opt('poster')) v.poster = this._abs(this._opt('poster')); v.loop = this.hasAttribute('loop'); v.muted = this.hasAttribute('muted'); this._startAt = parseT(this.getAttribute('start') || (location.hash.match(/[#&]t=([^&]+)/) || [])[1]);@@ -295,13 +308,13 @@ _error(html) { const e = this.$('.err'); e.innerHTML = `<div>${html}</div>`; e.classList.add('on'); this.p.classList.remove('wait'); } async _fetchText(u) {- const r = await fetch(this._abs(u), { credentials: 'include' }).catch(() => null);+ const r = await fetch(this._abs(u)).catch(() => null); return r && r.ok ? r.text() : null; } async _chapters() {- const u = this.getAttribute('chapters'); this.chapters = [];+ const u = this._opt('chapters'); this.chapters = []; if (u) {- const txt = await this._fetchText(u);+ const txt = typeof u === 'string' ? await this._fetchText(u) : JSON.stringify(u); if (txt) { try { const j = JSON.parse(txt); this.chapters = (Array.isArray(j) ? j : j.chapters || []).map((c) => ({ t: parseT(c.t ?? c.time ?? c.start), title: String(c.title ?? c.name ?? '') })); } catch { this.chapters = parseCues(txt).map((c) => ({ t: c.start, title: c.text.replace(/\n/g, ' ') })); }@@ -312,8 +325,8 @@ this._segments(); this._tick(); } async _captions() {- const u = this.getAttribute('captions'); this.cues = [];- if (u) { const txt = await this._fetchText(u); if (txt) this.cues = parseCues(txt); }+ const u = this._opt('captions'); this.cues = [];+ if (u) { const txt = /-->/.test(u) ? u : await this._fetchText(u); if (txt) this.cues = parseCues(txt); } this.$('[data-k=cc]').hidden = !this.cues.length; this.capOn = this.cues.length > 0 && this.capOn; this._caps(); this._sync();