PageSourceSearch

https://www.vizionplus.tv/wp-content/themes/vizionplus/assets/js/modules/on-air.js?v=1789942277

js vizionplus.tv collected 2026-10-02 06:31:33 UTC 9,467 bytes, 190 lines download raw bytes

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.