// ── SEARCH ──────────────────────────────────────────────────────────────────── let searchActive = false; let searchQuery = ''; let searchTimer = null; function toggleSearch() { const bar = document.getElementById('searchBar'); const inp = document.getElementById('searchInput'); searchActive = !searchActive; bar.classList.toggle('visible', searchActive); if (searchActive) { inp.focus(); } else { clearSearch(); } } function clearSearch() { searchQuery = ''; document.getElementById('searchInput').value = ''; applySearch(''); } function onSearchInput(val) { clearTimeout(searchTimer); searchTimer = setTimeout(() => { searchQuery = val.trim().toLowerCase(); applySearch(searchQuery); }, 350); } function applySearch(q) { if (!q) { // Restore the full channel list when the query is cleared. renderAll(); return; } const today = new Date(); today.setHours(0,0,0,0); const tomorrow = new Date(today.getTime() + 24*3600000); // Keep channels whose name matches or that have a matching program today. const filtered = channels.filter(ch => { // Match against the channel name. if (ch.name.toLowerCase().includes(q)) return true; // Match against today's programs (title + description). return (programs[ch.id]||[]).some(p => { if (p.stop < today || p.start > tomorrow) return false; if (p.title.toLowerCase().includes(q)) return true; if (p.desc && p.desc.toLowerCase().includes(q)) return true; if ((p.categories || []).some(cat => cat.toLowerCase().includes(q))) return true; return false; }); }); renderFiltered(filtered, q); } function renderFiltered(filteredChannels, q) { if (isMobile()) { renderMobileFiltered(filteredChannels, q); return; } // ── GRID ── const list = document.getElementById('channelsList'); const inner = document.getElementById('programsInner'); const ROW_H = 80; list.innerHTML = ''; inner.innerHTML = ''; inner.style.width = TOTAL_WIDTH_PX + 'px'; inner.style.height = (filteredChannels.length * ROW_H) + 'px'; // Recreate the now-line after clearing the container. const nowLine = document.createElement('div'); nowLine.className = 'now-line'; nowLine.id = 'nowLine'; nowLine.style.height = (filteredChannels.length * ROW_H) + 'px'; inner.appendChild(nowLine); const now = new Date(); filteredChannels.forEach((ch, i) => { // Channel column. 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.addEventListener('click', () => openPip(ch.name, getNowPlaying(ch.id))); list.appendChild(cell); // Programs row. 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'; // Highlight programs that match the current search query. const matchesProg = p.title.toLowerCase().includes(q) || (p.desc && p.desc.toLowerCase().includes(q)) || (p.categories || []).some(cat => cat.toLowerCase().includes(q)); if (matchesProg) block.classList.add('search-match'); 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); if (p.start <= now && p.stop > now) { block.addEventListener('click', () => openPip(ch.name, p.title)); } row.appendChild(block); }); inner.appendChild(row); }); positionNowLine(); } function renderMobileFiltered(filteredChannels, q) { const container = document.getElementById('mobileView'); container.innerHTML = ''; const now = new Date(); const winStart = new Date(now.getTime() - 30*60000); const winEnd = new Date(now.getTime() + 4*3600000); filteredChannels.forEach(ch => { const progs = (programs[ch.id]||[]).filter(p => p.stop > winStart && p.start < winEnd); if (!progs.length) return; const section = document.createElement('div'); section.className = 'mobile-channel'; const header = document.createElement('div'); header.className = 'mobile-channel-header'; if (ch.icon) { const img = document.createElement('img'); img.className = 'channel-logo'; img.src = ch.icon; img.onerror = () => img.replaceWith(makePlaceholder(ch.name)); header.appendChild(img); } else { header.appendChild(makePlaceholder(ch.name)); } const name = document.createElement('div'); name.className = 'channel-name'; name.textContent = ch.name; header.appendChild(name); section.appendChild(header); const list = document.createElement('div'); list.className = 'mobile-programs'; progs.forEach(p => { const isLive = p.start <= now && p.stop > now; const isPast = p.stop < now; const dur = Math.round((p.stop - p.start) / 60000); const matchesProg = p.title.toLowerCase().includes(q) || (p.desc && p.desc.toLowerCase().includes(q)) || (p.categories || []).some(cat => cat.toLowerCase().includes(q)); const item = document.createElement('div'); item.className = 'mobile-program' + (isLive ? ' is-live' : '') + (isPast ? ' is-past' : '') + (matchesProg ? ' search-match' : ''); if (isLive) { const bar = document.createElement('div'); bar.className = 'mobile-program-progress'; bar.style.width = (((now-p.start)/(p.stop-p.start))*100).toFixed(1) + '%'; item.appendChild(bar); } const timeCol = document.createElement('div'); timeCol.className = 'mobile-program-time'; timeCol.innerHTML = `${esc(fmtTime(p.start))}${esc(fmtTime(p.stop))}`; item.appendChild(timeCol); const info = document.createElement('div'); info.className = 'mobile-program-info'; const ep = fmtEpisode(p.season, p.episode); info.innerHTML = `