feat: EPG source switcher in topbar + Personal EPG modal
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user