feat: EPG source switcher in topbar + Personal EPG modal

This commit is contained in:
Johnnybegood90
2026-03-11 19:38:50 +01:00
parent 34e0b408f8
commit edf76d58ce
+148 -1
View File
@@ -652,6 +652,67 @@ $m3u_url = htmlspecialchars($first_source['m3u_url'] ?? '');
#navBtns { display: none !important; } #navBtns { display: none !important; }
} }
/* ── MODAL PERSONAL EPG ─────────────────────────────────────────────────── */
.modal-overlay {
position: fixed; inset: 0; background: rgba(0,0,0,0.7);
z-index: 3000; display: none; align-items: center; justify-content: center;
}
.modal-overlay.visible { display: flex; }
.modal-card {
background: var(--surface); border: 1px solid var(--border-bright);
padding: 28px; width: 100%; max-width: 420px; margin: 16px;
}
.modal-title {
font-family: var(--font-mono); font-size: 13px; color: var(--accent);
letter-spacing: 0.1em; text-transform: uppercase; margin-bottom: 6px;
}
.modal-subtitle { font-size: 12px; color: var(--text-dim); margin-bottom: 20px; line-height: 1.5; }
.modal-field { margin-bottom: 14px; }
.modal-field label {
display: block; font-family: var(--font-mono); font-size: 10px;
letter-spacing: 0.1em; text-transform: uppercase; color: var(--text-dim); margin-bottom: 6px;
}
.modal-field input {
width: 100%; background: var(--surface2); border: 1px solid var(--border-bright);
color: var(--text-bright); font-family: var(--font-mono); font-size: 12px;
padding: 9px 10px; outline: none; transition: border-color 0.15s;
}
.modal-field input:focus { border-color: var(--accent); }
.modal-field input::placeholder { color: var(--text-dim); opacity: 0.4; }
.modal-actions { display: flex; gap: 10px; margin-top: 20px; justify-content: flex-end; }
.modal-btn-cancel {
background: none; border: 1px solid var(--border-bright); color: var(--text-dim);
font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em;
text-transform: uppercase; padding: 8px 16px; cursor: pointer; transition: border-color 0.15s;
}
.modal-btn-cancel:hover { border-color: var(--text-dim); color: var(--text); }
.modal-btn-apply {
background: var(--accent); border: none; color: #000;
font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em;
text-transform: uppercase; padding: 8px 20px; cursor: pointer; transition: opacity 0.15s;
}
.modal-btn-apply:hover { opacity: 0.85; }
/* ── SOURCE SELECTOR ─────────────────────────────────────────────────────── */
.source-select {
background: var(--surface2);
border: 1px solid var(--border-bright);
color: var(--text);
font-family: var(--font-mono);
font-size: 11px;
padding: 5px 8px;
cursor: pointer;
letter-spacing: 0.06em;
height: 28px;
outline: none;
transition: border-color 0.15s, color 0.15s;
max-width: 130px;
}
.source-select:hover { border-color: var(--accent2); color: var(--accent2); }
.source-select option { background: #111; color: #eee; font-family: sans-serif; }
/* ═══════════════════════════════════════════ /* ═══════════════════════════════════════════
PLAYER PiP PLAYER PiP
═══════════════════════════════════════════ */ ═══════════════════════════════════════════ */
@@ -772,6 +833,16 @@ $m3u_url = htmlspecialchars($first_source['m3u_url'] ?? '');
<button class="copy-btn" onclick="copyUrl('m3u')" title="Copier l'URL M3U">M3U</button> <button class="copy-btn" onclick="copyUrl('m3u')" title="Copier l'URL M3U">M3U</button>
<?php endif; ?> <?php endif; ?>
</div> </div>
<?php if (count($epg_sources) > 1 || $allow_personal_epg): ?>
<select class="source-select" id="sourceSelect" onchange="switchSource(this.value)" title="Source EPG">
<?php foreach ($epg_sources as $i => $src): ?>
<option value="<?= $i ?>"><?= htmlspecialchars($src['name']) ?></option>
<?php endforeach; ?>
<?php if ($allow_personal_epg): ?>
<option value="personal">✏ Personal EPG</option>
<?php endif; ?>
</select>
<?php endif; ?>
<select class="theme-select" id="themeSelect" onchange="setTheme(this.value)" title="Thème"> <select class="theme-select" id="themeSelect" onchange="setTheme(this.value)" title="Thème">
<?php <?php
$theme_dir = __DIR__ . '/themes'; $theme_dir = __DIR__ . '/themes';
@@ -859,7 +930,7 @@ let scrollListenerAdded = false; // sync scroll vertical chaînes
function pad(n) { return String(n).padStart(2,'0'); } function pad(n) { return String(n).padStart(2,'0'); }
function copyUrl(type) { function copyUrl(type) {
const url = type === 'epg' ? DEFAULT_EPG_URL : DEFAULT_M3U_URL; const url = type === 'epg' ? currentEpgUrl : currentM3uUrl;
if (!url) return; if (!url) return;
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
const btns = document.querySelectorAll('.copy-btn'); const btns = document.querySelectorAll('.copy-btn');
@@ -1192,6 +1263,61 @@ function hideTooltip() { document.getElementById('tooltip').classList.remove('vi
// ── SOURCE SWITCHER ───────────────────────────────────────────────────────────
let currentEpgUrl = EPG_SOURCES[0]?.epg_url ?? '';
let currentM3uUrl = EPG_SOURCES[0]?.m3u_url ?? '';
function switchSource(value) {
if (value === 'personal') {
openPersonalEpgModal();
return;
}
const idx = parseInt(value);
const src = EPG_SOURCES[idx];
if (!src) return;
activeSourceIndex = idx;
currentEpgUrl = src.epg_url;
currentM3uUrl = src.m3u_url ?? '';
loadEPG(currentEpgUrl);
if (currentM3uUrl) loadM3U(currentM3uUrl);
// Mettre à jour les boutons copy
updateCopyButtons();
}
function updateCopyButtons() {
// Rien à faire visuellement, copyUrl() lit currentEpgUrl/currentM3uUrl
}
function openPersonalEpgModal() {
const saved_epg = localStorage.getItem('gridtv-personal-epg') ?? '';
const saved_m3u = localStorage.getItem('gridtv-personal-m3u') ?? '';
const modal = document.getElementById('personalEpgModal');
document.getElementById('personalEpgInput').value = saved_epg;
document.getElementById('personalM3uInput').value = saved_m3u;
modal.classList.add('visible');
}
function closePersonalEpgModal() {
document.getElementById('personalEpgModal').classList.remove('visible');
// Si annulé, remettre le select sur la source précédente
const sel = document.getElementById('sourceSelect');
if (sel) sel.value = activeSourceIndex;
}
function applyPersonalEpg() {
const epg = document.getElementById('personalEpgInput').value.trim();
const m3u = document.getElementById('personalM3uInput').value.trim();
if (!epg) { alert('URL EPG obligatoire'); return; }
localStorage.setItem('gridtv-personal-epg', epg);
localStorage.setItem('gridtv-personal-m3u', m3u);
currentEpgUrl = epg;
currentM3uUrl = m3u;
closePersonalEpgModal();
loadEPG(epg);
if (m3u) loadM3U(m3u);
}
// ── M3U PARSER ──────────────────────────────────────────────────────────────── // ── M3U PARSER ────────────────────────────────────────────────────────────────
function slugify(str) { function slugify(str) {
return str.toLowerCase() return str.toLowerCase()
@@ -1336,5 +1462,26 @@ window.addEventListener('resize', () => {
window.addEventListener('load', () => { loadEPG(DEFAULT_EPG_URL); loadM3U(DEFAULT_M3U_URL); }); window.addEventListener('load', () => { loadEPG(DEFAULT_EPG_URL); loadM3U(DEFAULT_M3U_URL); });
</script> </script>
<!-- MODAL PERSONAL EPG -->
<div class="modal-overlay" id="personalEpgModal">
<div class="modal-card">
<div class="modal-title">✏ Personal EPG</div>
<div class="modal-subtitle">Utilisez votre propre source EPG/M3U sur cette instance.</div>
<div class="modal-field">
<label>URL EPG (XMLTV) *</label>
<input type="url" id="personalEpgInput" placeholder="http://mon-serveur/xmltv.xml">
</div>
<div class="modal-field">
<label>URL M3U <span style="opacity:.5;font-size:10px">(optionnel)</span></label>
<input type="url" id="personalM3uInput" placeholder="http://mon-serveur/channels.m3u">
</div>
<div class="modal-actions">
<button class="modal-btn-cancel" onclick="closePersonalEpgModal()">Annuler</button>
<button class="modal-btn-apply" onclick="applyPersonalEpg()">Appliquer</button>
</div>
</div>
</div>
</body> </body>
</html> </html>