/** * Default blueprint factories for the vanilla player. * * Each function receives a BlueprintContext and returns an HTMLElement (or null). * Reactivity is wired via ctx.subscribe.on() — each fires immediately with the * current value, then on every change. */ import type { BlueprintContext, BlueprintMap } from "./Blueprint"; import { formatTime } from "../core/TimeFormat"; function el(tag: string, className?: string): HTMLElement { const e = document.createElement(tag); if (className) e.className = className; return e; } function btn(className: string, label: string, onClick: () => void): HTMLButtonElement { const b = document.createElement("button"); b.type = "button"; b.className = className; b.setAttribute("aria-label", label); b.title = label; b.addEventListener("click", onClick); return b; } // SVG icon helpers const ICONS = { play: ``, pause: ``, volumeUp: ``, volumeOff: ``, skipBack: ``, skipForward: ``, fullscreen: ``, fullscreenExit: ``, pip: ``, settings: ``, }; function icon(name: keyof typeof ICONS, size = 16): HTMLElement { const span = document.createElement("span"); span.className = "fw-bp-icon"; span.style.width = `${size}px`; span.style.height = `${size}px`; span.style.display = "inline-flex"; span.innerHTML = ICONS[name]; const svg = span.querySelector("svg"); if (svg) { svg.setAttribute("width", String(size)); svg.setAttribute("height", String(size)); } return span; } // ---- Blueprint factories ---- function container(ctx: BlueprintContext): HTMLElement { const root = el("div", "fw-player-surface fw-player-root fw-bp-container"); root.setAttribute("role", "region"); root.setAttribute("aria-label", ctx.translate("player", "Video player")); root.setAttribute("tabindex", "0"); root.style.position = "relative"; root.style.width = "100%"; root.style.height = "100%"; root.style.overflow = "hidden"; root.style.backgroundColor = "black"; return root; } function videocontainer(_ctx: BlueprintContext): HTMLElement { const wrap = el("div", "fw-bp-video-container"); wrap.style.position = "absolute"; wrap.style.inset = "0"; return wrap; } function controls(ctx: BlueprintContext): HTMLElement { const bar = el("div", "fw-bp-controls"); bar.style.position = "absolute"; bar.style.bottom = "0"; bar.style.left = "0"; bar.style.right = "0"; bar.style.zIndex = "10"; bar.style.transition = "opacity 0.2s"; bar.style.background = "linear-gradient(transparent, rgba(0,0,0,0.7))"; bar.style.padding = "8px 12px 6px"; ctx.subscribe.on("playing", () => { // Auto-hide controls after 3s of playback // (simplified — full implementation would use hover tracking) }); return bar; } function controlbar(_ctx: BlueprintContext): HTMLElement { const bar = el("div", "fw-bp-controlbar"); bar.style.display = "flex"; bar.style.alignItems = "center"; bar.style.gap = "6px"; return bar; } function play(ctx: BlueprintContext): HTMLElement { const b = btn("fw-btn-flush fw-bp-play", ctx.translate("play", "Play"), () => ctx.api.togglePlay() ); const iconPlay = icon("play"); const iconPause = icon("pause"); b.appendChild(iconPlay); b.appendChild(iconPause); iconPause.style.display = "none"; ctx.subscribe.on("playing", (val) => { const playing = val as boolean; iconPlay.style.display = playing ? "none" : ""; iconPause.style.display = playing ? "" : "none"; b.setAttribute( "aria-label", playing ? ctx.translate("pause", "Pause") : ctx.translate("play", "Play") ); b.title = b.getAttribute("aria-label") ?? ""; }); return b; } function seekBackward(ctx: BlueprintContext): HTMLElement { return btn("fw-btn-flush fw-bp-seek-back", ctx.translate("skipBack", "Skip back 10s"), () => ctx.api.skipBack(10000) ); } function seekForward(ctx: BlueprintContext): HTMLElement { return btn("fw-btn-flush fw-bp-seek-fwd", ctx.translate("skipForward", "Skip forward 10s"), () => ctx.api.skipForward(10000) ); } function live(ctx: BlueprintContext): HTMLElement { const badge = el("div", "fw-bp-live"); badge.style.display = "none"; const dot = el("span", "fw-bp-live-dot"); dot.style.width = "6px"; dot.style.height = "6px"; dot.style.borderRadius = "50%"; dot.style.backgroundColor = "#ef4444"; dot.style.marginRight = "4px"; dot.style.display = "inline-block"; const label = document.createElement("button"); label.type = "button"; label.className = "fw-btn-flush"; label.textContent = "LIVE"; label.style.fontSize = "0.625rem"; label.style.fontWeight = "700"; label.style.textTransform = "uppercase"; label.style.letterSpacing = "0.05em"; label.style.display = "inline-flex"; label.style.alignItems = "center"; label.style.cursor = "pointer"; label.addEventListener("click", () => ctx.api.jumpToLive()); label.prepend(dot); badge.appendChild(label); ctx.subscribe.on("playing", () => { badge.style.display = ctx.api.live ? "" : "none"; }); return badge; } function currentTimeBlueprint(ctx: BlueprintContext): HTMLElement { const span = el("span", "fw-bp-time"); span.style.fontSize = "0.75rem"; span.style.fontVariantNumeric = "tabular-nums"; span.style.whiteSpace = "nowrap"; span.textContent = "0:00"; ctx.subscribe.on("currentTime", (val) => { span.textContent = formatTime(val as number); }); return span; } function totalTime(ctx: BlueprintContext): HTMLElement { const span = el("span", "fw-bp-duration"); span.style.fontSize = "0.75rem"; span.style.fontVariantNumeric = "tabular-nums"; span.style.whiteSpace = "nowrap"; span.style.opacity = "0.7"; span.textContent = "0:00"; ctx.subscribe.on("duration", (val) => { const d = val as number; span.textContent = isNaN(d) || !isFinite(d) ? "" : formatTime(d); }); return span; } function speaker(ctx: BlueprintContext): HTMLElement { const b = btn("fw-btn-flush fw-bp-speaker", ctx.translate("mute", "Mute"), () => ctx.api.toggleMute() ); const iconOn = icon("volumeUp"); const iconOff = icon("volumeOff"); b.appendChild(iconOn); b.appendChild(iconOff); iconOff.style.display = "none"; ctx.subscribe.on("muted", (val) => { const muted = val as boolean; iconOn.style.display = muted ? "none" : ""; iconOff.style.display = muted ? "" : "none"; b.setAttribute( "aria-label", muted ? ctx.translate("unmute", "Unmute") : ctx.translate("mute", "Mute") ); b.title = b.getAttribute("aria-label") ?? ""; }); return b; } function volumeBlueprint(ctx: BlueprintContext): HTMLElement { const wrap = el("div", "fw-bp-volume"); wrap.style.display = "flex"; wrap.style.alignItems = "center"; wrap.style.width = "80px"; const slider = document.createElement("input"); slider.type = "range"; slider.min = "0"; slider.max = "1"; slider.step = "0.01"; slider.className = "fw-bp-volume-slider"; slider.style.width = "100%"; slider.style.cursor = "pointer"; slider.setAttribute("aria-label", ctx.translate("volume", "Volume")); slider.addEventListener("input", () => { ctx.api.volume = parseFloat(slider.value); }); ctx.subscribe.on("volume", (val) => { slider.value = String(val as number); }); wrap.appendChild(slider); return wrap; } function fullscreenBlueprint(ctx: BlueprintContext): HTMLElement { const b = btn("fw-btn-flush fw-bp-fullscreen", ctx.translate("fullscreen", "Fullscreen"), () => ctx.api.toggleFullscreen() ); const iconEnter = icon("fullscreen"); const iconExit = icon("fullscreenExit"); b.appendChild(iconEnter); b.appendChild(iconExit); iconExit.style.display = "none"; ctx.subscribe.on("fullscreen", (val) => { const fs = val as boolean; iconEnter.style.display = fs ? "none" : ""; iconExit.style.display = fs ? "" : "none"; b.setAttribute( "aria-label", fs ? ctx.translate("exitFullscreen", "Exit fullscreen") : ctx.translate("fullscreen", "Fullscreen") ); b.title = b.getAttribute("aria-label") ?? ""; }); return b; } function pipBlueprint(ctx: BlueprintContext): HTMLElement { const b = btn("fw-btn-flush fw-bp-pip", ctx.translate("pip", "Picture-in-Picture"), () => ctx.api.togglePiP() ); b.appendChild(icon("pip")); return b; } function settingsBlueprint(ctx: BlueprintContext): HTMLElement { const b = btn("fw-btn-flush fw-bp-settings", ctx.translate("settings", "Settings"), () => { ctx.log( "Settings clicked (no built-in menu in blueprint mode — use a skin or custom blueprint)" ); }); b.appendChild(icon("settings")); return b; } function progress(ctx: BlueprintContext): HTMLElement { const wrap = el("div", "fw-bp-progress"); wrap.style.position = "relative"; wrap.style.height = "4px"; wrap.style.backgroundColor = "rgba(255,255,255,0.2)"; wrap.style.borderRadius = "2px"; wrap.style.cursor = "pointer"; wrap.style.marginBottom = "6px"; const filled = el("div", "fw-bp-progress-filled"); filled.style.height = "100%"; filled.style.backgroundColor = "var(--fw-accent, #3b82f6)"; filled.style.borderRadius = "2px"; filled.style.width = "0%"; filled.style.transition = "width 0.1s linear"; wrap.appendChild(filled); ctx.subscribe.on("currentTime", () => { const t = ctx.api.currentTime; const d = ctx.api.duration; if (d && isFinite(d) && d > 0) { filled.style.width = `${Math.min(100, (t / d) * 100)}%`; } }); wrap.addEventListener("click", (e) => { const rect = wrap.getBoundingClientRect(); const pct = (e.clientX - rect.left) / rect.width; const d = ctx.api.duration; if (d && isFinite(d)) { ctx.api.seek(pct * d); } }); return wrap; } function loading(ctx: BlueprintContext): HTMLElement { const overlay = el("div", "fw-bp-loading"); overlay.style.position = "absolute"; overlay.style.inset = "0"; overlay.style.display = "none"; overlay.style.alignItems = "center"; overlay.style.justifyContent = "center"; overlay.style.backgroundColor = "rgba(0,0,0,0.4)"; overlay.style.zIndex = "20"; const spinner = el("div", "fw-bp-spinner"); spinner.style.width = "32px"; spinner.style.height = "32px"; spinner.style.border = "3px solid rgba(255,255,255,0.3)"; spinner.style.borderTopColor = "white"; spinner.style.borderRadius = "50%"; spinner.style.animation = "fw-spin 0.8s linear infinite"; overlay.appendChild(spinner); ctx.subscribe.on("buffering", (val) => { overlay.style.display = (val as boolean) ? "flex" : "none"; }); return overlay; } function errorBlueprint(ctx: BlueprintContext): HTMLElement { const overlay = el("div", "fw-bp-error"); overlay.style.position = "absolute"; overlay.style.inset = "0"; overlay.style.display = "none"; overlay.style.alignItems = "center"; overlay.style.justifyContent = "center"; overlay.style.backgroundColor = "rgba(0,0,0,0.7)"; overlay.style.zIndex = "25"; overlay.style.flexDirection = "column"; overlay.style.gap = "12px"; overlay.style.color = "white"; const msg = el("p", "fw-bp-error-msg"); msg.style.fontSize = "0.875rem"; overlay.appendChild(msg); const retryBtn = btn("fw-btn-flush fw-bp-error-retry", ctx.translate("retry", "Retry"), () => { ctx.api.clearError(); ctx.api.retry(); }); retryBtn.textContent = ctx.translate("retry", "Retry"); retryBtn.style.padding = "6px 16px"; retryBtn.style.borderRadius = "4px"; retryBtn.style.backgroundColor = "rgba(255,255,255,0.15)"; overlay.appendChild(retryBtn); ctx.subscribe.on("error", (val) => { const err = val as string | null; if (err) { msg.textContent = err; overlay.style.display = "flex"; } else { overlay.style.display = "none"; } }); return overlay; } function spacer(): HTMLElement { const s = el("div", "fw-bp-spacer"); s.style.flex = "1"; return s; } /** All default blueprints keyed by type name */ export const DEFAULT_BLUEPRINTS: BlueprintMap = { container, videocontainer, controls, controlbar, play, seekBackward, seekForward, live, currentTime: currentTimeBlueprint, totalTime, speaker, volume: volumeBlueprint, fullscreen: fullscreenBlueprint, pip: pipBlueprint, settings: settingsBlueprint, progress, loading, error: errorBlueprint, spacer, };