// ── 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 { 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')); }