From 8826926168f91fab454a8df7b4cff79c8ffb71c4 Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Thu, 12 Mar 2026 13:11:49 +0100 Subject: [PATCH 1/2] Feat: Language support via json files in the locales folder + Feat: Popup with title, channel, schedule, duration, season, episode, synopsis, Watch button (if M3U provided) and IMDb link + Fix: Date added to the timebar --- locales/en.json | 30 +++++++++++++++++++++++++++ locales/es.json | 30 +++++++++++++++++++++++++++ locales/fr.json | 30 +++++++++++++++++++++++++++ src/js/config.js | 1 + src/js/epg.js | 18 ++++++++++------ src/js/program.js | 45 ++++++++++++++++++++++++++++++++++++++++ src/js/utils.js | 10 ++++----- src/tpl/config.php | 51 ++++++++++++++++++++++++++++++++++++++++++++++ src/tpl/footer.php | 2 +- src/tpl/grid.php | 10 ++++----- src/tpl/head.php | 51 ++++++++++++++++++++++++++++++++++++++++++++++ src/tpl/modals.php | 28 +++++++++++++++++++------ src/tpl/topbar.php | 14 ++++++------- 13 files changed, 289 insertions(+), 31 deletions(-) create mode 100644 locales/en.json create mode 100644 locales/es.json create mode 100644 locales/fr.json create mode 100644 src/js/program.js create mode 100644 src/tpl/config.php diff --git a/locales/en.json b/locales/en.json new file mode 100644 index 0000000..87b8bb7 --- /dev/null +++ b/locales/en.json @@ -0,0 +1,30 @@ +{ + "days": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], + "months": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], + "now": "Now", + "channels": "Channels", + "live": "LIVE", + "loading": "Loading guide...", + "epg_error": "Unable to load EPG", + "epg_error_sub": "Check your XMLTV stream URL.", + "load": "Load", + "search_placeholder": "Search channel or program...", + "copy_epg": "EPG", + "copy_m3u": "M3U", + "copied": "\u2713 copied", + "program_info": "Program info", + "watch_now": "Watch now", + "imdb_search": "Search on IMDb", + "close": "Close", + "duration_min": "min", + "personal_epg_title": "Personal EPG", + "personal_epg_subtitle": "Use your own EPG/M3U source on this instance.", + "personal_epg_label": "EPG URL (XMLTV) *", + "personal_epg_m3u_label": "M3U URL", + "personal_epg_optional": "optional", + "cancel": "Cancel", + "apply": "Apply", + "setup_already_done": "Configuration already done. Use your admin key to access setup again.", + "shift_back": "\u25c4 1h", + "shift_fwd": "1h \u25ba" +} diff --git a/locales/es.json b/locales/es.json new file mode 100644 index 0000000..3b37ae6 --- /dev/null +++ b/locales/es.json @@ -0,0 +1,30 @@ +{ + "days": ["Dom", "Lun", "Mar", "Mi\u00e9", "Jue", "Vie", "S\u00e1b"], + "months": ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"], + "now": "Ahora", + "channels": "Canales", + "live": "LIVE", + "loading": "Cargando gu\u00eda...", + "epg_error": "No se puede cargar el EPG", + "epg_error_sub": "Comprueba la URL de tu fuente XMLTV.", + "load": "Cargar", + "search_placeholder": "Buscar canal o programa...", + "copy_epg": "EPG", + "copy_m3u": "M3U", + "copied": "\u2713 copiado", + "program_info": "Info del programa", + "watch_now": "Ver ahora", + "imdb_search": "Buscar en IMDb", + "close": "Cerrar", + "duration_min": "min", + "personal_epg_title": "EPG Personal", + "personal_epg_subtitle": "Usa tu propia fuente EPG/M3U en esta instancia.", + "personal_epg_label": "URL EPG (XMLTV) *", + "personal_epg_m3u_label": "URL M3U", + "personal_epg_optional": "opcional", + "cancel": "Cancelar", + "apply": "Aplicar", + "setup_already_done": "Configuraci\u00f3n ya realizada. Usa tu clave de administrador para volver al setup.", + "shift_back": "\u25c4 1h", + "shift_fwd": "1h \u25ba" +} diff --git a/locales/fr.json b/locales/fr.json new file mode 100644 index 0000000..99d1735 --- /dev/null +++ b/locales/fr.json @@ -0,0 +1,30 @@ +{ + "days": ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"], + "months": ["Jan", "F\u00e9v", "Mar", "Avr", "Mai", "Jun", "Jul", "Ao\u00fb", "Sep", "Oct", "Nov", "D\u00e9c"], + "now": "Maintenant", + "channels": "Cha\u00eenes", + "live": "LIVE", + "loading": "Chargement du guide...", + "epg_error": "Impossible de charger l'EPG", + "epg_error_sub": "V\u00e9rifiez l'URL de votre flux XMLTV.", + "load": "Charger", + "search_placeholder": "Rechercher une cha\u00eene ou un programme...", + "copy_epg": "EPG", + "copy_m3u": "M3U", + "copied": "\u2713 copi\u00e9", + "program_info": "Infos programme", + "watch_now": "Regarder", + "imdb_search": "Rechercher sur IMDb", + "close": "Fermer", + "duration_min": "min", + "personal_epg_title": "EPG Personnel", + "personal_epg_subtitle": "Utilisez votre propre source EPG/M3U sur cette instance.", + "personal_epg_label": "URL EPG (XMLTV) *", + "personal_epg_m3u_label": "URL M3U", + "personal_epg_optional": "optionnel", + "cancel": "Annuler", + "apply": "Appliquer", + "setup_already_done": "Configuration d\u00e9j\u00e0 effectu\u00e9e. Utilisez votre cl\u00e9 admin pour acc\u00e9der \u00e0 nouveau au setup.", + "shift_back": "\u25c4 1h", + "shift_fwd": "1h \u25ba" +} diff --git a/src/js/config.js b/src/js/config.js index 313521c..aac6ca6 100644 --- a/src/js/config.js +++ b/src/js/config.js @@ -1,4 +1,5 @@ const EPG_SOURCES = ; +const L = ; const ALLOW_PERSONAL_EPG = ; // Source active (index) diff --git a/src/js/epg.js b/src/js/epg.js index 333c858..444c0dd 100644 --- a/src/js/epg.js +++ b/src/js/epg.js @@ -37,10 +37,18 @@ function renderRuler() { 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 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':''); lbl.textContent=fmtTime(t); - const line = document.createElement('div'); line.className='time-tick-line'+(isHour?' hour':''); + 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); } @@ -88,9 +96,7 @@ function renderPrograms() { 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)); - } + block.addEventListener('click', (e) => { hideTooltip(); openProgramModal(ch, p); }); row.appendChild(block); }); inner.appendChild(row); diff --git a/src/js/program.js b/src/js/program.js new file mode 100644 index 0000000..ab714ef --- /dev/null +++ b/src/js/program.js @@ -0,0 +1,45 @@ +// ── PROGRAM MODAL ───────────────────────────────────────────────────────────── +function openProgramModal(ch, p) { + const now = new Date(); + const isLive = p.start <= now && p.stop > now; + const dur = Math.round((p.stop - p.start) / 60000); + const ep = fmtEpisode(p.season, p.episode); + + document.getElementById('pm-title').textContent = p.title; + document.getElementById('pm-channel').textContent = ch.name; + document.getElementById('pm-time').textContent = + `${fmtTime(p.start)} — ${fmtTime(p.stop)} · ${dur} ${L.duration_min}` + + (ep ? ` · ${ep}` : ''); + document.getElementById('pm-desc').textContent = p.desc || ''; + document.getElementById('pm-desc').style.display = p.desc ? 'block' : 'none'; + + // Bouton Watch now — seulement si live + const watchBtn = document.getElementById('pm-watch'); + if (isLive) { + watchBtn.style.display = 'inline-flex'; + watchBtn.onclick = () => { closeProgramModal(); openPip(ch.name, p.title); }; + } else { + watchBtn.style.display = 'none'; + } + + // Bouton IMDb + document.getElementById('pm-imdb').href = + `https://www.imdb.com/find/?q=${encodeURIComponent(p.title)}&s=tt`; + + document.getElementById('programModal').classList.add('visible'); +} + +function closeProgramModal() { + document.getElementById('programModal').classList.remove('visible'); +} + +// Fermer sur clic overlay +document.addEventListener('click', e => { + const modal = document.getElementById('programModal'); + if (e.target === modal) closeProgramModal(); +}); + +// Fermer sur Escape +document.addEventListener('keydown', e => { + if (e.key === 'Escape') closeProgramModal(); +}); diff --git a/src/js/utils.js b/src/js/utils.js index 754f1db..ffc6be2 100644 --- a/src/js/utils.js +++ b/src/js/utils.js @@ -12,12 +12,12 @@ function copyUrl(type) { navigator.clipboard.writeText(url).then(() => { const btns = document.querySelectorAll('.copy-btn'); btns.forEach(b => { if (b.textContent.toLowerCase() === type) { - b.textContent = '✓ copié'; + b.textContent = L.copied; b.classList.add('copied'); setTimeout(() => { b.textContent = type.toUpperCase(); b.classList.remove('copied'); }, 1800); }}); }).catch(() => { - prompt('Copiez cette URL :', url); + prompt(url, url); }); } function fmtTime(d) { return `${pad(d.getHours())}:${pad(d.getMinutes())}`; } @@ -36,10 +36,8 @@ function parseXMLTVDate(str) { function updateClock() { const now = new Date(); document.getElementById('clock').textContent = fmtTimeFull(now); - const days=['Dim','Lun','Mar','Mer','Jeu','Ven','Sam']; - const months=['Jan','Fév','Mar','Avr','Mai','Jun','Jul','Aoû','Sep','Oct','Nov','Déc']; document.getElementById('dateLabel').textContent = - `${days[now.getDay()]} ${now.getDate()} ${months[now.getMonth()]} ${now.getFullYear()}`; + `${L.days[now.getDay()]} ${now.getDate()} ${L.months[now.getMonth()]} ${now.getFullYear()}`; } setInterval(updateClock, 1000); updateClock(); @@ -62,7 +60,7 @@ async function loadEPG(url) { } catch(e) { document.getElementById('loading').style.display = 'none'; document.getElementById('errorMsg').style.display = 'flex'; - document.getElementById('errorDetail').textContent = `Erreur: ${e.message}`; + document.getElementById('errorDetail').textContent = `${L.epg_error_sub} (${e.message})`; document.getElementById('epgInput').value = url; } } diff --git a/src/tpl/config.php b/src/tpl/config.php new file mode 100644 index 0000000..393a8da --- /dev/null +++ b/src/tpl/config.php @@ -0,0 +1,51 @@ + 'Main', + 'epg_url' => $config['epg_url'], + 'm3u_url' => $config['m3u_url'] ?? '', + ]]; + $config['allow_personal_epg'] = false; + unset($config['epg_url'], $config['m3u_url']); + file_put_contents($config_path, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); +} +// ───────────────────────────────────────────────────────────────────────────── + +$group_name = htmlspecialchars($config['group_name'] ?? 'GridTV'); +$epg_sources = $config['epg_sources'] ?? []; +$allow_personal_epg = !empty($config['allow_personal_epg']); + +// Première source par défaut (pour compatibilité avec le reste du code) +$first_source = $epg_sources[0] ?? []; +$epg_url = htmlspecialchars($first_source['epg_url'] ?? ''); +$m3u_url = htmlspecialchars($first_source['m3u_url'] ?? ''); + +// ── Locale detection ───────────────────────────────────────────────────────── +$supported_locales = ['en', 'fr', 'es']; +$locale = 'en'; +$accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en'; +foreach (explode(',', $accept) as $lang) { + $code = strtolower(substr(trim($lang), 0, 2)); + if (in_array($code, $supported_locales)) { $locale = $code; break; } +} +$locale_path = __DIR__ . "/../locales/{$locale}.json"; +$L = json_decode(file_get_contents($locale_path), true); +// ───────────────────────────────────────────────────────────────────────────── +?> + + + + + +GridTV — <?= $group_name ?> diff --git a/src/tpl/footer.php b/src/tpl/footer.php index d6b49ca..4d73e37 100644 --- a/src/tpl/footer.php +++ b/src/tpl/footer.php @@ -3,7 +3,7 @@ -
Chargement du guide
+
-
⚠ Impossible de charger l'EPG
-
Vérifiez l'URL de votre flux XMLTV.
+
+
- +
@@ -32,7 +32,7 @@
-
Chaînes
+
diff --git a/src/tpl/head.php b/src/tpl/head.php index cb1823d..362ece7 100644 --- a/src/tpl/head.php +++ b/src/tpl/head.php @@ -800,6 +800,57 @@ border-left: 3px solid var(--accent); } + + /* ── PROGRAM MODAL ───────────────────────────────────────────────────────── */ + .program-card { + max-width: 520px; padding: 28px; position: relative; + } + .pm-close { + position: absolute; top: 14px; right: 14px; + background: none; border: none; color: var(--text-muted); + cursor: pointer; font-size: 18px; line-height: 1; + transition: color .2s; + } + .pm-close:hover { color: var(--accent); } + .pm-channel { + font-size: 11px; letter-spacing: .1em; text-transform: uppercase; + color: var(--accent); margin-bottom: 6px; + } + .pm-title { + font-size: 20px; font-weight: 700; color: var(--text); + margin-bottom: 8px; line-height: 1.2; + } + .pm-time { + font-size: 12px; color: var(--accent); margin-bottom: 14px; + } + .pm-desc { + font-size: 13px; color: var(--text-muted); line-height: 1.6; + margin-bottom: 20px; max-height: 140px; overflow-y: auto; + } + .pm-actions { + display: flex; gap: 10px; flex-wrap: wrap; + } + .pm-btn { + border-radius: 4px; cursor: pointer; font-family: inherit; + font-size: 13px; font-weight: 600; padding: 8px 16px; + text-decoration: none; transition: opacity .2s; + display: inline-flex; align-items: center; gap: 6px; + border: none; + } + .pm-btn:hover { opacity: .8; } + .pm-watch { background: var(--accent); color: var(--bg); } + .pm-imdb { background: #f5c518; color: #000; } + .pm-cancel { background: var(--bg); color: var(--text-muted); border: 1px solid var(--border); } + + /* Date midnight dans la timebar */ + .time-tick-label.midnight { + color: var(--accent); font-weight: 700; font-size: 11px; + white-space: nowrap; + } + .time-tick-line.midnight { + background: var(--accent); opacity: .4; width: 2px; + } + diff --git a/src/tpl/modals.php b/src/tpl/modals.php index 22d36b8..2e242c7 100644 --- a/src/tpl/modals.php +++ b/src/tpl/modals.php @@ -1,19 +1,35 @@ + + + diff --git a/src/tpl/topbar.php b/src/tpl/topbar.php index d3384a3..f17f7a4 100644 --- a/src/tpl/topbar.php +++ b/src/tpl/topbar.php @@ -3,14 +3,14 @@
00:00:00
- + - +
1 || $allow_personal_epg): ?> @@ -48,10 +48,10 @@ foreach ($theme_files as $file) { ?> -
LIVE
+
From de7ef1645b9a73ffc8caf1d10ead3f4d70faeb41 Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Thu, 12 Mar 2026 13:14:09 +0100 Subject: [PATCH 2/2] Bad config.php location. --- src/config.php | 14 ++++++++++++- src/tpl/config.php | 51 ---------------------------------------------- 2 files changed, 13 insertions(+), 52 deletions(-) delete mode 100644 src/tpl/config.php diff --git a/src/config.php b/src/config.php index da6c8d2..393a8da 100644 --- a/src/config.php +++ b/src/config.php @@ -30,9 +30,21 @@ $allow_personal_epg = !empty($config['allow_personal_epg']); $first_source = $epg_sources[0] ?? []; $epg_url = htmlspecialchars($first_source['epg_url'] ?? ''); $m3u_url = htmlspecialchars($first_source['m3u_url'] ?? ''); + +// ── Locale detection ───────────────────────────────────────────────────────── +$supported_locales = ['en', 'fr', 'es']; +$locale = 'en'; +$accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en'; +foreach (explode(',', $accept) as $lang) { + $code = strtolower(substr(trim($lang), 0, 2)); + if (in_array($code, $supported_locales)) { $locale = $code; break; } +} +$locale_path = __DIR__ . "/../locales/{$locale}.json"; +$L = json_decode(file_get_contents($locale_path), true); +// ───────────────────────────────────────────────────────────────────────────── ?> - + diff --git a/src/tpl/config.php b/src/tpl/config.php deleted file mode 100644 index 393a8da..0000000 --- a/src/tpl/config.php +++ /dev/null @@ -1,51 +0,0 @@ - 'Main', - 'epg_url' => $config['epg_url'], - 'm3u_url' => $config['m3u_url'] ?? '', - ]]; - $config['allow_personal_epg'] = false; - unset($config['epg_url'], $config['m3u_url']); - file_put_contents($config_path, json_encode($config, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES)); -} -// ───────────────────────────────────────────────────────────────────────────── - -$group_name = htmlspecialchars($config['group_name'] ?? 'GridTV'); -$epg_sources = $config['epg_sources'] ?? []; -$allow_personal_epg = !empty($config['allow_personal_epg']); - -// Première source par défaut (pour compatibilité avec le reste du code) -$first_source = $epg_sources[0] ?? []; -$epg_url = htmlspecialchars($first_source['epg_url'] ?? ''); -$m3u_url = htmlspecialchars($first_source['m3u_url'] ?? ''); - -// ── Locale detection ───────────────────────────────────────────────────────── -$supported_locales = ['en', 'fr', 'es']; -$locale = 'en'; -$accept = $_SERVER['HTTP_ACCEPT_LANGUAGE'] ?? 'en'; -foreach (explode(',', $accept) as $lang) { - $code = strtolower(substr(trim($lang), 0, 2)); - if (in_array($code, $supported_locales)) { $locale = $code; break; } -} -$locale_path = __DIR__ . "/../locales/{$locale}.json"; -$L = json_decode(file_get_contents($locale_path), true); -// ───────────────────────────────────────────────────────────────────────────── -?> - - - - - -GridTV — <?= $group_name ?>