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 = = json_encode(array_values($epg_sources)) ?>; +const L = = json_encode($L) ?>; const ALLOW_PERSONAL_EPG = = $allow_personal_epg ? 'true' : 'false' ?>; // 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); +// ───────────────────────────────────────────────────────────────────────────── +?> + + +
+ + +