(function () { const root = document.getElementById("regia-app"); if (!root) return; const cfg = { token: root.dataset.token, board: root.dataset.board || "volley", statusUrl: root.dataset.statusUrl, scoreUrl: root.dataset.scoreUrl, pauseUrl: root.dataset.pauseUrl, resumeUrl: root.dataset.resumeUrl, stopUrl: root.dataset.stopUrl, cableUrl: root.dataset.cableUrl, hlsUrl: root.dataset.hlsUrl, homeName: root.dataset.homeName, awayName: root.dataset.awayName, shareUrl: root.dataset.shareUrl, shareTitle: root.dataset.shareTitle, streamClosed: root.dataset.streamClosed === "true" }; const els = { badge: document.getElementById("regia-badge"), setsLine: document.getElementById("sets-line"), scoreLine: document.getElementById("score-line"), partialsLine: document.getElementById("partials-line"), homePoints: document.getElementById("home-points"), toast: document.getElementById("regia-toast"), modal: document.getElementById("regia-modal"), modalTitle: document.getElementById("modal-title"), modalBody: document.getElementById("modal-body"), modalConfirm: document.getElementById("modal-confirm"), modalCancel: document.getElementById("modal-cancel"), preview: document.getElementById("regia-preview"), previewPlaceholder: document.getElementById("regia-preview-placeholder"), btnPause: document.getElementById("btn-pause") }; let pendingAction = null; let hls = null; let previewStarted = false; let wasPausedPreview = false; let streamPaused = root.dataset.initialPaused === "true"; function hlsUrlFresh() { const sep = cfg.hlsUrl.includes("?") ? "&" : "?"; return `${cfg.hlsUrl}${sep}_t=${Date.now()}`; } function hidePreviewPlaceholder() { if (els.previewPlaceholder) els.previewPlaceholder.hidden = true; } function showPreviewPlaceholder(text) { if (!els.previewPlaceholder) return; els.previewPlaceholder.textContent = text; els.previewPlaceholder.hidden = false; } function destroyPreview() { previewStarted = false; if (hls) { hls.destroy(); hls = null; } if (els.preview) { els.preview.removeAttribute("src"); els.preview.load(); } } function refreshPreviewForCover() { if (!els.preview || !cfg.hlsUrl || cfg.streamClosed) return; destroyPreview(); previewStarted = false; startPreview(); } function toast(msg) { els.toast.textContent = msg; els.toast.classList.add("visible"); setTimeout(() => els.toast.classList.remove("visible"), 2800); } function formatPartials(partials) { if (!partials || !partials.length) return ""; return partials .map((p) => `Set ${p.set || p["set"]} ${p.home || p["home"]}-${p.away || p["away"]}`) .join(" · "); } function formatClock(secs, countUp) { const total = parseInt(secs, 10) || 0; if (total <= 0 && !countUp) return "0:00"; const m = Math.floor(total / 60); const s = total % 60; return `${m}:${String(s).padStart(2, "0")}`; } function applyScorePayload(data) { const s = data.score; if (!s) return; const board = s.board_type || s.boardType || cfg.board; const homePts = s.home_points ?? s.homePoints ?? 0; const awayPts = s.away_points ?? s.awayPoints ?? 0; const homeSets = s.home_sets ?? s.homeSets; const awaySets = s.away_sets ?? s.awaySets; const currentSet = s.current_set ?? s.currentSet; const period = s.period; const clockSecs = s.clock_secs ?? s.clockSecs ?? s.data?.clock_secs; const awayEl = document.getElementById("away-points"); const centerHint = document.getElementById("center-hint"); const clockLine = document.getElementById("clock-line"); if (els.homePoints) els.homePoints.textContent = homePts; if (awayEl) awayEl.textContent = awayPts; if (board === "basket" || board === "timed") { if (els.setsLine) els.setsLine.textContent = period || "—"; if (els.scoreLine) els.scoreLine.textContent = `${cfg.homeName} ${homePts} - ${awayPts} ${cfg.awayName}`; if (centerHint) centerHint.textContent = formatClock(clockSecs); if (clockLine) clockLine.textContent = `Cronometro: ${formatClock(clockSecs)}`; } else if (board === "timer") { if (els.setsLine) els.setsLine.textContent = "Cronometro"; if (clockLine) clockLine.textContent = formatClock(clockSecs, true); } else if (board === "generic") { if (els.setsLine) els.setsLine.textContent = "Punteggio libero"; if (els.scoreLine) els.scoreLine.textContent = `${cfg.homeName} ${homePts} - ${awayPts} ${cfg.awayName}`; } else { if (els.setsLine) els.setsLine.textContent = `Set ${currentSet} · Set vinti ${homeSets}-${awaySets}`; if (els.scoreLine) els.scoreLine.textContent = `${cfg.homeName} ${homePts} - ${awayPts} ${cfg.awayName}`; const partialsText = formatPartials(s.set_partials || s.setPartials); if (els.partialsLine && partialsText) { els.partialsLine.textContent = `Parziali: ${partialsText}`; els.partialsLine.hidden = false; } } } function syncPauseButton(data) { if (!els.btnPause) return; const paused = !!(data.paused || data.status === "paused"); streamPaused = paused; els.btnPause.textContent = paused ? "Riprendi diretta" : "Metti in pausa"; } function setBadge(data) { syncPauseButton(data); if (data.stream_closed) { els.badge.textContent = "Terminata"; els.badge.className = "regia-badge regia-badge--ended"; return; } if (data.status === "paused" || data.paused) { els.badge.textContent = "In pausa"; els.badge.className = "regia-badge regia-badge--wait"; return; } if (data.on_air) { els.badge.textContent = "In onda"; els.badge.className = "regia-badge regia-badge--live"; } else { els.badge.textContent = "In attesa"; els.badge.className = "regia-badge regia-badge--wait"; } } async function postScore(action) { const res = await fetch(cfg.scoreUrl, { method: "PATCH", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify({ cmd: action }) }); if (!res.ok) throw new Error("Errore aggiornamento punteggio"); return res.json(); } async function handleAction(action) { const prevHome = els.homePoints?.textContent; const prevAway = document.getElementById("away-points")?.textContent; try { const data = await postScore(action); applyScorePayload(data); setBadge(data); if (cfg.board === "volley" || cfg.board === "racket") { if (data.set_won && action !== "close_set") { const winner = data.winner === "home" ? cfg.homeName : cfg.awayName; openModal("Set vinto", `${winner} ha vinto il set. Chiudere il set?`, () => postScore("close_set")); } } if (data.match_won) { const winner = data.winner === "home" ? cfg.homeName : cfg.awayName; openModal("Partita vinta", `${winner} ha vinto la partita. Chiudere la diretta?`, () => stopStream()); } } catch (e) { if (prevHome != null && els.homePoints) els.homePoints.textContent = prevHome; const awayEl = document.getElementById("away-points"); if (prevAway != null && awayEl) awayEl.textContent = prevAway; toast(e.message || "Errore"); } } function openModal(title, body, onConfirm) { els.modalTitle.textContent = title; els.modalBody.textContent = body; els.modal.classList.add("open"); pendingAction = onConfirm; } function closeModal() { els.modal.classList.remove("open"); pendingAction = null; } els.modalConfirm.addEventListener("click", async () => { const fn = pendingAction; closeModal(); if (fn) { try { const data = await fn(); if (data) { applyScorePayload(data); setBadge(data); } } catch (e) { toast(e.message || "Errore"); } } }); els.modalCancel.addEventListener("click", closeModal); root.querySelectorAll("[data-action]").forEach((btn) => { btn.addEventListener("click", () => { const action = btn.dataset.action; if (action === "close_set") { openModal("Chiudi set", "Confermi la chiusura del set corrente?", () => postScore("close_set")); } else { handleAction(action); } }); }); function syncPreviewFromStatus(data) { if (data.stream_closed) { cfg.streamClosed = true; stopPreview(); return; } const paused = data.paused || data.status === "paused"; if (data.on_air) { if (!previewStarted) { startPreview(); } else if (paused && !wasPausedPreview) { refreshPreviewForCover(); } else if (paused) { if (els.preview && els.preview.readyState >= 2) hidePreviewPlaceholder(); else els.preview?.play().catch(() => {}); } else if (els.preview && els.preview.readyState >= 2) { hidePreviewPlaceholder(); } } else { destroyPreview(); showPreviewPlaceholder("Anteprima in attesa del segnale…"); } wasPausedPreview = paused; } async function pollStatus() { try { const res = await fetch(cfg.statusUrl, { headers: { Accept: "application/json" } }); if (!res.ok) return; const data = await res.json(); applyScorePayload(data); setBadge(data); syncPreviewFromStatus(data); } catch (_) {} } function bindPreviewEvents() { if (!els.preview) return; ["playing", "loadeddata", "canplay"].forEach((ev) => { els.preview.addEventListener(ev, hidePreviewPlaceholder); }); } function startPreview() { if (cfg.streamClosed || !els.preview || !cfg.hlsUrl || previewStarted) return; previewStarted = true; const url = hlsUrlFresh(); if (window.Hls && Hls.isSupported()) { hls = new Hls({ lowLatencyMode: false, maxBufferLength: 30, maxMaxBufferLength: 60, liveSyncDurationCount: 5, maxLiveSyncPlaybackRate: 1.0 }); hls.loadSource(url); hls.attachMedia(els.preview); hls.on(Hls.Events.MANIFEST_PARSED, () => { els.preview.play().catch(() => {}); }); hls.on(Hls.Events.ERROR, (_, err) => { if (err.fatal) { destroyPreview(); setTimeout(() => { if (!cfg.streamClosed) startPreview(); }, 2500); } }); } else if (els.preview.canPlayType("application/vnd.apple.mpegurl")) { els.preview.src = url; els.preview.addEventListener( "loadedmetadata", () => els.preview.play().catch(() => {}), { once: true } ); } } function stopPreview() { destroyPreview(); if (els.preview) els.preview.style.display = "none"; showPreviewPlaceholder("Diretta terminata"); } async function togglePauseStream() { const url = streamPaused ? cfg.resumeUrl : cfg.pauseUrl; const res = await fetch(url, { method: "POST", headers: { Accept: "application/json" } }); if (!res.ok) { const body = await res.json().catch(() => ({})); throw new Error(body.error || (streamPaused ? "Errore ripresa diretta" : "Errore pausa diretta")); } const data = await res.json(); const wasPaused = streamPaused; applyScorePayload(data); setBadge(data); syncPreviewFromStatus(data); toast(wasPaused ? "Diretta ripresa" : "Diretta in pausa — copertina in onda"); } async function stopStream() { const ok = confirm("Chiudere definitivamente la diretta?"); if (!ok) return null; const res = await fetch(cfg.stopUrl, { method: "POST", headers: { Accept: "application/json" } }); if (!res.ok) throw new Error("Errore chiusura"); const data = await res.json(); cfg.streamClosed = true; stopPreview(); toast("Diretta chiusa"); return data; } els.btnPause?.addEventListener("click", () => togglePauseStream().catch((e) => toast(e.message))); document.getElementById("btn-stop")?.addEventListener("click", () => stopStream().catch((e) => toast(e.message))); async function shareLink() { const payload = { title: cfg.shareTitle, text: "Apri questo link per gestire il punteggio della diretta:", url: cfg.shareUrl }; if (navigator.share) { try { await navigator.share(payload); return; } catch (err) { if (err.name === "AbortError") return; } } await copyLink(); } async function copyLink() { try { await navigator.clipboard.writeText(cfg.shareUrl); toast("Link copiato"); } catch (_) { prompt("Copia il link:", cfg.shareUrl); } } document.getElementById("btn-share")?.addEventListener("click", () => shareLink()); document.getElementById("btn-copy")?.addEventListener("click", () => copyLink()); function bindPreviewStallRecovery() { if (!els.preview) return; let stallAt = 0; let lastProgressAt = Date.now(); let lastTime = 0; const markProgress = () => { const t = els.preview.currentTime; if (t > lastTime + 0.05) { lastTime = t; lastProgressAt = Date.now(); } }; els.preview.addEventListener("timeupdate", markProgress); els.preview.addEventListener("playing", markProgress); const nudge = (force) => { if (!previewStarted || cfg.streamClosed) return; const now = Date.now(); if (!force && now - lastProgressAt < 8000) { els.preview.play().catch(() => {}); return; } if (now - stallAt < 5000) return; stallAt = now; lastProgressAt = now; if (hls) { hls.startLoad(-1); els.preview.play().catch(() => {}); } }; setInterval(() => { if (previewStarted && !cfg.streamClosed && Date.now() - lastProgressAt > 12000) nudge(true); }, 4000); } bindPreviewEvents(); bindPreviewStallRecovery(); if (!cfg.streamClosed && cfg.hlsUrl) { pollStatus(); } else if (cfg.streamClosed) { stopPreview(); } setInterval(pollStatus, 4000); })();