144 lines
5.8 KiB
JavaScript
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'));
|
|
}
|