1/** 2 * Keeps everything "on air" fresh without a reload (one tick a minute, on the minute, 3 * LiteSpeed-cache safe): 4 * · progress bars + "started ⦠ago" 5 * · when the programme changes: title, time·genre line, progress attrs, the hero card's 6 * backdrop art (cross-fade; falls back to data.onairDefault when nothing is on air), 7 * the LIVE pill and the "Live tani / Në vijim" kick. 8 * · the home now/next strip (.nownext, home/live.php): its "until â¦" and the whole "Më pas" row 9 * (data-onair-next-*), plus the "starts in â¦" countdown while nothing is on. 10 * Every hook is optional â the card on the live page and the strip on the homepage each carry 11 * the subset they need, and a missing element is simply skipped. 12 */ 13import { data, toMin, nowMin, nowSec, todayDate, fmtDur, t } from './data.js'; 14 15/* The page was rendered with one day of guide in it. A tab left open overnight would otherwise 16 keep painting yesterday, so when the date rolls over we fetch the new day once and carry on. */ 17let days = (data.schedule || []).slice(); 18let pending = ''; 19let lastTry = 0; 20const today = () => days.find((d) => d.date === todayDate()) || null; 21 22const fetchToday = (repaint) => { 23 const date = todayDate(); 24 // The page ships a week of guide, so this only ever fires for a tab left open for days â 25 // and then at most once every five minutes, never as a poll. 26 if (today() || !data.restUrl) return; 27 if (pending === date && Date.now() - lastTry < 300000) return; 28 pending = date; 29 lastTry = Date.now(); 30 fetch(data.restUrl + 'epg?date=' + encodeURIComponent(date), { credentials: 'same-origin' }) 31 .then((r) => (r.ok ? r.json() : null)) 32 .then((json) => { 33 const day = json && (json.days || []).find((d) => d.date === date); 34 if (!day) return; 35 days = days.concat([{ date, items: day.slots || day.items || [] }]); 36 repaint(); 37 }) 38 .catch(() => {}); 39}; 40 41/* What to say under the bar. "Started 0 min ago" means nothing to someone who just tuned in, and 42 how long a film has been running stops being the useful number once it is nearly over. 43 vp_onair_elapsed_label() prints the same three states server-side. */ 44const elapsedLabel = (start, end, now) => { 45 const left = end - now; 46 if (left <= 0) return t('endingNow', 'ending now'); 47 if (now - start < 1) return t('justStarted', 'just started'); 48 if (left <= 15) return t('endsIn', 'ends in %s').replace('%s', fmtDur(left)); 49 return t('started', 'started %s ago').replace('%s', fmtDur(now - start)); 50}; 51 52/* The off-air mirror: how long until the next programme starts (vp_onair_starts_label()). The 53 translated strings ride on the card itself (data-i18n-*), so the module needs no new bridge keys. */ 54const startsLabel = (card, start, now) => { 55 let left = start - now; 56 if (left < 0) left += 1440; 57 const d = (card && card.dataset) || {}; 58 if (left < 1) return d.i18nStarting || t('startingNow', 'starting now'); 59 return (d.i18nStarts || t('startsIn', 'starts in %s')).replace('%s', fmtDur(left)); 60}; 61 62function currentItem() { 63 const d = today(); 64 if (!d) return { now: null, next: null }; 65 const n = nowMin(); 66 let now = null; let next = null; 67 for (const it of d.items) { 68 const s = toMin(it.start); let e = toMin(it.end); 69 if (e <= s) e = 24 * 60; 70 if (s <= n && n < e) now = it; 71 else if (s > n && !next) next = it; 72 } 73 return { now, next }; 74} 75 76export default function onAir() { 77 document.querySelectorAll('[data-progress]').forEach((el) => { const s = el.querySelector('span'); if (s) s.style.width = Math.min(100, +el.dataset.progress || 0) + '%'; }); 78 const bars = document.querySelectorAll('[data-onair-prog]'); 79 const card = document.querySelector('[data-onair]'); 80 let shown = null; // start of the programme currently painted 81 82 const tickBars = () => bars.forEach((bar) => { 83 const s = toMin(bar.dataset.start); 84 let e = toMin(bar.dataset.end) || s + 120; 85 let n = nowMin(); 86 if (e <= s) e += 1440; // the programme runs past midnight 87 if (n < s) n += 1440; // we are past midnight and it started yesterday 88 const span = bar.querySelector('span');
88 if (span) span.style.width = Math.max(0, Math.min(100, ((n - s) / (e - s)) * 100)) + '%'; 89 const el = bar.parentElement.querySelector('[data-onair-elapsed]'); if (el) el.textContent = elapsedLabel(s, e, n); 90 }); 91 92 /* cross-fade the hero backdrop to a new image (or the branded default) */ 93 const swapArt = (src) => { 94 const bg = card && card.querySelector('[data-onair-bg]'); 95 if (!bg) return; 96 const url = src || data.onairDefault || ''; 97 const old = bg.querySelector('img'); 98 if (!url || (old && old.src.endsWith(url))) return; 99 const img = document.createElement('img'); 100 img.alt = ''; 101 img.className = 'is-entering'; 102 img.src = url; 103 img.addEventListener('load', () => { 104 bg.appendChild(img); 105 requestAnimationFrame(() => img.classList.remove('is-entering')); 106 setTimeout(() => { if (old) old.remove(); }, 700); 107 }); 108 }; 109 110 /* Off air, the label under the title counts down to the next programme; it moves once a minute 111 like the elapsed label does while something is on. */ 112 const tickNext = () => { 113 if (!card) return; 114 const { next } = currentItem(); 115 if (!next) return; 116 const label = startsLabel(card, toMin(next.start), nowMin()); 117 /* the strip's "Më pas" row counts down in every state (CSS shows it only off air) */ 118 card.querySelectorAll('[data-onair-starts]').forEach((el) => { el.textContent = label; }); 119 if (!card.classList.contains('onair--off')) return; 120 const elapsed = card.querySelector('[data-onair-elapsed]'); 121 if (elapsed) elapsed.textContent = label; 122 }; 123 124 const repaintCard = () => { 125 if (!card) return; 126 const { now, next } = currentItem(); 127 const key = now ? now.start : (next ? 'next-' + next.start : 'off'); 128 if (key === shown) return; 129 shown = key; 130 const it = now || next; 131 document.querySelectorAll('[data-onair-title]').forEach((el) => { el.textContent = it ? it.title : ''; }); 132 /* Nothing on: the card is about what comes next â "Next · 07:00", no red, no pulse. */ 133 card.classList.toggle('onair--off', !now); 134 if (next) card.dataset.onairNext = next.start; else delete card.dataset.onairNext; 135 const meta = card.querySelector('.onair__meta'); 136 if (meta && it) meta.textContent = `${it.start} â ${it.end}${it.genre ? ' · ' + it.genre : ''}`; 137 const bar = card.querySelector('[data-onair-prog]'); 138 if (bar) { bar.hidden = !now; if (now) { bar.dataset.start = now.start; bar.dataset.end = now.end; } } 139 const elapsed = card.querySelector('[data-onair-elapsed]'); 140 if (elapsed) { elapsed.hidden = !it; if (!now && next) elapsed.textContent = startsLabel(card, toMin(next.start), nowMin()); } 141 const link = card.querySelector('[data-onair-link]'); 142 if (link && it) link.href = it.show_url || data.liveHref || link.href; 143 const kick = card.querySelector('.onair__kick'); 144 if (kick) kick.textContent = now ? (card.dataset.i18nNow || t('onAirNow', 'On air now')) : (next ? (card.dataset.i18nNext || t('nextKick', 'Next · %s')).replace('%s', next.start) : (card.dataset.i18nOff || t('offAir', 'Off air'))); 145 const pill = card.querySelector('.onair__head .pill--live'); 146 if (pill) pill.hidden = !now; 147 /* the strip: "until 20:10" beside what is on, and the "Më pas" row for what follows */ 148 const until = card.querySelector('[data-onair-until]'); 149 if (until) until.textContent = now ? t('until', 'until %s').replace('%s', now.end) : ''; 150 const nextRow = card.querySelector('[data-onair-next-row]'); 151 if (nextRow) nextRow.hidden = !next; 152 if (next) { 153 card.querySelectorAll('[data-onair-next-time]').forEach((el) => { el.textContent = next.start; }); 154 card.querySelectorAll('[data-onair-next-title]').forEach((el) => { el.textContent = next.title; }); 155 const nl = card.querySelector('[data-onair-next-link]'); 156 if (nl && nl.tagName === 'A' && next.show_url) nl.href = next.show_url; 157 } 158 document.querySelectorAll('[data-onair-next-teaser]').forEach((el) => { el.textContent = next ? `${next.start} ${next.title}` : ''; }); 159 swapArt(it && it.thumbnail); 160 }; 161 162 /* header now-playing chip (all pages) */ 163 const chip = document.querySelector('[data-onair-chip]'); 164 const repaintChip = () => { 165 if (!chip) return; 166 const { now } = currentItem(); 167 chip.hidden = !now; 168 if (now) { 169 chip.querySelector('[data-onair-chip-title]').textContent = now.title; 170 chip.querySelector('[data-onair-chip-until]').textContent = t('until', 'until %s').replace('%s', now.end); 171 } 172 }; 173 174 const tick = () => { fetchToday(tick); tickBars(); repaintCard(); tickNext(); repaintChip(); }; 175 176 /* On the minute, not every 30 s: the label changes exactly when the clock does. Timers in a 177 background tab are throttled to a crawl, so a tab brought back to the front repaints at once 178 instead of showing a stale "started 1 h 54 min ago" until the next tick lands. */ 179 const loop = () => setTimeout(() => { tick(); loop(); }, Math.max(2000, (60 - nowSec()) * 1000 + 200)); 180 const wake = () => { if (!document.hidden) tick(); }; 181 182 if (bars.length || card || chip) { 183 tick(); 184 loop(); 185 document.addEventListener('visibilitychange', wake); 186 window.addEventListener('pageshow', wake); 187 window.addEventListener('focus', wake); 188 window.addEventListener('online', wake); 189 } 190}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.