Files
GridTV/src/js/epg.js
T

144 lines
5.8 KiB
JavaScript

// ── GRID ──────────────────────────────────────────────────────────────────────
function renderGrid() {
renderChannels(); renderRuler(); renderPrograms(); positionNowLine();
}
function makePlaceholder(name) {
const el = document.createElement('div');
el.className = 'channel-logo-placeholder';
el.textContent = name.substring(0,2).toUpperCase();
return el;
}
function makeChannelCell(ch) {
const cell = document.createElement('div');
cell.className = 'channel-cell';
if (ch.icon) {
const img = document.createElement('img');
img.className = 'channel-logo'; img.src = ch.icon;
img.onerror = () => img.replaceWith(makePlaceholder(ch.name));
cell.appendChild(img);
} else { cell.appendChild(makePlaceholder(ch.name)); }
const name = document.createElement('div');
name.className = 'channel-name'; name.textContent = ch.name; name.title = ch.name;
cell.appendChild(name);
cell.appendChild(makeFavBtn(ch.id));
cell.addEventListener('click', () => openPip(ch.name, getNowPlaying(ch.id)));
return cell;
}
function renderChannels() {
const list = document.getElementById('channelsList');
list.innerHTML = '';
const { favs, rest } = sortedChannels(channels);
if (favs.length) {
favs.forEach(ch => list.appendChild(makeChannelCell(ch)));
const sep = document.createElement('div');
sep.className = 'fav-separator';
list.appendChild(sep);
}
rest.forEach(ch => list.appendChild(makeChannelCell(ch)));
}
function renderRuler() {
const ruler = document.getElementById('timeRulerInner');
ruler.innerHTML = ''; ruler.style.width = TOTAL_WIDTH_PX + 'px';
document.getElementById('timeRuler').style.width = TOTAL_WIDTH_PX + 'px';
let t = new Date(GRID_START); t.setSeconds(0,0);
const rem = t.getMinutes()%15; if (rem) t.setMinutes(t.getMinutes()+(15-rem));
while (t <= GRID_END) {
const x = msToX(t.getTime());
const isHour = t.getMinutes() === 0;
const isMidnight = isHour && t.getHours() === 0;
const tick = document.createElement('div'); tick.className='time-tick'; tick.style.left=x+'px';
const lbl = document.createElement('div');
lbl.className = 'time-tick-label' + (isHour ? ' hour' : '') + (isMidnight ? ' midnight' : '');
if (isMidnight) {
lbl.textContent = `${L.days[t.getDay()]} ${t.getDate()} ${L.months[t.getMonth()]}`;
} else {
lbl.textContent = fmtTime(t);
}
const line = document.createElement('div'); line.className='time-tick-line'+(isHour?' hour':'')+(isMidnight?' midnight':'');
tick.appendChild(lbl); tick.appendChild(line); ruler.appendChild(tick);
t = new Date(t.getTime()+15*60000);
}
}
function renderPrograms() {
const ROW_H = 80;
const inner = document.getElementById('programsInner');
const totalH = channels.length * ROW_H;
inner.innerHTML = '';
inner.style.width = TOTAL_WIDTH_PX+'px';
inner.style.height = totalH+'px';
// Recreate the now-line because it was removed by innerHTML = ''.
const nowLine = document.createElement('div');
nowLine.className = 'now-line'; nowLine.id = 'nowLine';
nowLine.style.height = totalH + 'px';
inner.appendChild(nowLine);
const now = new Date();
const { favs: _f, rest: _r } = sortedChannels(channels);
const orderedChannels = [..._f, ..._r];
orderedChannels.forEach((ch, i) => {
const row = document.createElement('div');
row.className = 'program-row';
row.style.cssText = `top:${i*ROW_H}px;position:absolute;left:0;right:0;`;
(programs[ch.id]||[]).filter(p => p.stop>GRID_START && p.start<GRID_END).forEach(p => {
const x = Math.max(0, msToX(p.start.getTime()));
const w = Math.min(TOTAL_WIDTH_PX, msToX(p.stop.getTime())) - x;
if (w < 2) return;
const block = document.createElement('div'); block.className='program-block';
if (p.stop < now) block.classList.add('is-past');
if (p.start <= now && p.stop > now) {
block.classList.add('is-live');
block.style.setProperty('--progress',(((now-p.start)/(p.stop-p.start))*100).toFixed(1)+'%');
}
block.style.left=x+'px'; block.style.width=w+'px';
if (w>40) { const t=document.createElement('div'); t.className='program-title'; t.textContent=p.title; block.appendChild(t); }
if (w>80) {
const ep = fmtEpisode(p.season, p.episode);
const t=document.createElement('div'); t.className='program-time';
t.textContent = (ep ? ep + ' ' : '') + `${fmtTime(p.start)}${fmtTime(p.stop)}`;
block.appendChild(t);
}
block.addEventListener('mouseenter', e=>showTooltip(e,p));
block.addEventListener('mousemove', e=>moveTooltip(e));
block.addEventListener('mouseleave', hideTooltip);
block.addEventListener('click', (e) => { hideTooltip(); openProgramModal(ch, p); });
row.appendChild(block);
});
inner.appendChild(row);
});
positionNowLine();
if (!scrollListenerAdded) {
scrollListenerAdded = true;
document.getElementById('timelineArea').addEventListener('scroll', function() {
document.getElementById('channelsList').scrollTop = this.scrollTop;
});
}
}
function positionNowLine() {
const x = msToX(Date.now());
const line = document.getElementById('nowLine');
if (line) line.style.left = x + 'px';
}
function centerNow() {
const area = document.getElementById('timelineArea');
area.scrollLeft = Math.max(0, msToX(Date.now()) - area.offsetWidth * 0.3);
document.querySelectorAll('.nav-btn').forEach((b,i) => b.classList.toggle('active', i===1));
}
function shiftView(mins) {
const area = document.getElementById('timelineArea');
area.scrollLeft = Math.max(0, area.scrollLeft + mins * PX_PER_MIN);
document.querySelectorAll('.nav-btn').forEach(b => b.classList.remove('active'));
}