From 100ad77058ba9d9bf068e75ca47759093fe09f9e Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Thu, 12 Mar 2026 12:15:51 +0100 Subject: [PATCH 1/2] feat: Search engine channel name + program name today --- src/js/search.js | 194 +++++++++++++++++++++++++++++++++++++++++++++ src/js/sources.js | 1 + src/tpl/footer.php | 2 +- src/tpl/head.php | 41 ++++++++++ src/tpl/topbar.php | 5 ++ 5 files changed, 242 insertions(+), 1 deletion(-) create mode 100644 src/js/search.js diff --git a/src/js/search.js b/src/js/search.js new file mode 100644 index 0000000..58543db --- /dev/null +++ b/src/js/search.js @@ -0,0 +1,194 @@ +// ── SEARCH ──────────────────────────────────────────────────────────────────── +let searchActive = false; +let searchQuery = ''; + +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) { + searchQuery = val.trim().toLowerCase(); + applySearch(searchQuery); +} + +function applySearch(q) { + if (!q) { + // Remettre toutes les chaines + renderAll(); + return; + } + + const today = new Date(); + today.setHours(0,0,0,0); + const tomorrow = new Date(today.getTime() + 24*3600000); + + // Filtrer les chaines : nom OU programme aujourd'hui correspond + const filtered = channels.filter(ch => { + // Match sur le nom de la chaine + if (ch.name.toLowerCase().includes(q)) return true; + // Match sur les programmes d'aujourd'hui (titre + desc) + 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; + 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'; + + // Remettre la now-line + 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) => { + // Colonne chaîne + 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); + + // Programmes + 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 si le programme matche la recherche + const matchesProg = p.title.toLowerCase().includes(q) || (p.desc && p.desc.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)); + 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 = `${fmtTime(p.start)}${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 = `
${p.title}
${ep ? ep + ' · ' : ''}${dur} min
`; + item.appendChild(info); + + if (isLive) { + const badge = document.createElement('div'); badge.className = 'mobile-live-badge'; badge.textContent = 'LIVE'; + item.appendChild(badge); + } + list.appendChild(item); + }); + section.appendChild(list); container.appendChild(section); + }); +} diff --git a/src/js/sources.js b/src/js/sources.js index 31c819f..6b9de34 100644 --- a/src/js/sources.js +++ b/src/js/sources.js @@ -14,6 +14,7 @@ function switchSource(value) { currentEpgUrl = src.epg_url; currentM3uUrl = src.m3u_url ?? ''; localStorage.setItem('gridtv-active-source', idx); + if (typeof clearSearch === "function") clearSearch(); loadEPG(currentEpgUrl); if (currentM3uUrl) loadM3U(currentM3uUrl); updateCopyButtons(); diff --git a/src/tpl/footer.php b/src/tpl/footer.php index dc7e6eb..d6b49ca 100644 --- a/src/tpl/footer.php +++ b/src/tpl/footer.php @@ -3,7 +3,7 @@ diff --git a/src/tpl/topbar.php b/src/tpl/topbar.php index 579f985..d3384a3 100644 --- a/src/tpl/topbar.php +++ b/src/tpl/topbar.php @@ -47,6 +47,11 @@ foreach ($theme_files as $file) { } ?> +
LIVE
+ From 4e1518f4844d63edc44840fc48a247fa80c136f2 Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Thu, 12 Mar 2026 12:19:45 +0100 Subject: [PATCH 2/2] prez: Adding roadmap into README.md --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index c47c40c..558fbaf 100644 --- a/README.md +++ b/README.md @@ -306,3 +306,25 @@ PRs are welcome! Got an idea, a fix, or a feature request — open an issue or s ## 📄 License GNU Affero General Public License v3.0 (AGPLv3) + +--- + +## 🗺️ Roadmap + +### ✅ Done +- Timeline grid with live "now" indicator +- Mobile responsive list view +- Built-in HLS PiP player +- HTTP→HTTPS proxy +- Theme system (4 built-in themes) +- Multi-EPG sources with topbar switcher +- Personal EPG (localStorage) +- Modular codebase (src/tpl + src/js) +- Docker support +- **Search** — filter grid by channel name or program title/synopsis + +### 🔜 Coming soon +- **Favorites** — pin channels to the top of the grid (localStorage) +- **Setup re-editable** — reopen setup.php with an admin key, no SSH required +- **Dark/Light auto mode** — follow system preference when using the default theme +- **Grid export** — export today's schedule as PDF or image