4 Commits
Author SHA1 Message Date
Johnnybegood90 e924c945d6 Feat: Favorite channels in Localstorage. 2026-03-13 20:47:47 +01:00
Johnnybegood90 7b42539bc8 Feat: Fav channel in localstorage 2026-03-13 20:46:01 +01:00
Johnnybegood90 d7617bfeaa Update screenshots. 2026-03-12 15:57:24 +01:00
Johnnybegood90 23e0f29990 Update screenshots. 2026-03-12 15:57:00 +01:00
9 changed files with 213 additions and 76 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 642 KiB

After

Width:  |  Height:  |  Size: 1.0 MiB

+28 -5
View File
@@ -1,6 +1,27 @@
{ {
"days": ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"], "days": [
"months": ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"], "Sun",
"Mon",
"Tue",
"Wed",
"Thu",
"Fri",
"Sat"
],
"months": [
"Jan",
"Feb",
"Mar",
"Apr",
"May",
"Jun",
"Jul",
"Aug",
"Sep",
"Oct",
"Nov",
"Dec"
],
"now": "Now", "now": "Now",
"channels": "Channels", "channels": "Channels",
"live": "LIVE", "live": "LIVE",
@@ -11,7 +32,7 @@
"search_placeholder": "Search channel or program...", "search_placeholder": "Search channel or program...",
"copy_epg": "EPG", "copy_epg": "EPG",
"copy_m3u": "M3U", "copy_m3u": "M3U",
"copied": "\u2713 copied", "copied": "✓ copied",
"program_info": "Program info", "program_info": "Program info",
"watch_now": "Watch now", "watch_now": "Watch now",
"imdb_search": "Search on IMDb", "imdb_search": "Search on IMDb",
@@ -25,6 +46,8 @@
"cancel": "Cancel", "cancel": "Cancel",
"apply": "Apply", "apply": "Apply",
"setup_already_done": "Configuration already done. Use your admin key to access setup again.", "setup_already_done": "Configuration already done. Use your admin key to access setup again.",
"shift_back": "\u25c4 1h", "shift_back": " 1h",
"shift_fwd": "1h \u25ba" "shift_fwd": "1h ►",
"fav_add": "Add to favorites",
"fav_remove": "Remove from favorites"
} }
+30 -7
View File
@@ -1,17 +1,38 @@
{ {
"days": ["Dom", "Lun", "Mar", "Mi\u00e9", "Jue", "Vie", "S\u00e1b"], "days": [
"months": ["Ene", "Feb", "Mar", "Abr", "May", "Jun", "Jul", "Ago", "Sep", "Oct", "Nov", "Dic"], "Dom",
"Lun",
"Mar",
"Mié",
"Jue",
"Vie",
"Sáb"
],
"months": [
"Ene",
"Feb",
"Mar",
"Abr",
"May",
"Jun",
"Jul",
"Ago",
"Sep",
"Oct",
"Nov",
"Dic"
],
"now": "Ahora", "now": "Ahora",
"channels": "Canales", "channels": "Canales",
"live": "LIVE", "live": "LIVE",
"loading": "Cargando gu\u00eda...", "loading": "Cargando guía...",
"epg_error": "No se puede cargar el EPG", "epg_error": "No se puede cargar el EPG",
"epg_error_sub": "Comprueba la URL de tu fuente XMLTV.", "epg_error_sub": "Comprueba la URL de tu fuente XMLTV.",
"load": "Cargar", "load": "Cargar",
"search_placeholder": "Buscar canal o programa...", "search_placeholder": "Buscar canal o programa...",
"copy_epg": "EPG", "copy_epg": "EPG",
"copy_m3u": "M3U", "copy_m3u": "M3U",
"copied": "\u2713 copiado", "copied": "✓ copiado",
"program_info": "Info del programa", "program_info": "Info del programa",
"watch_now": "Ver ahora", "watch_now": "Ver ahora",
"imdb_search": "Buscar en IMDb", "imdb_search": "Buscar en IMDb",
@@ -24,7 +45,9 @@
"personal_epg_optional": "opcional", "personal_epg_optional": "opcional",
"cancel": "Cancelar", "cancel": "Cancelar",
"apply": "Aplicar", "apply": "Aplicar",
"setup_already_done": "Configuraci\u00f3n ya realizada. Usa tu clave de administrador para volver al setup.", "setup_already_done": "Configuración ya realizada. Usa tu clave de administrador para volver al setup.",
"shift_back": "\u25c4 1h", "shift_back": " 1h",
"shift_fwd": "1h \u25ba" "shift_fwd": "1h ►",
"fav_add": "Agregar a favoritos",
"fav_remove": "Quitar de favoritos"
} }
+32 -9
View File
@@ -1,17 +1,38 @@
{ {
"days": ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"], "days": [
"months": ["Jan", "F\u00e9v", "Mar", "Avr", "Mai", "Jun", "Jul", "Ao\u00fb", "Sep", "Oct", "Nov", "D\u00e9c"], "Dim",
"Lun",
"Mar",
"Mer",
"Jeu",
"Ven",
"Sam"
],
"months": [
"Jan",
"Fév",
"Mar",
"Avr",
"Mai",
"Jun",
"Jul",
"Aoû",
"Sep",
"Oct",
"Nov",
"Déc"
],
"now": "Maintenant", "now": "Maintenant",
"channels": "Cha\u00eenes", "channels": "Chaînes",
"live": "LIVE", "live": "LIVE",
"loading": "Chargement du guide...", "loading": "Chargement du guide...",
"epg_error": "Impossible de charger l'EPG", "epg_error": "Impossible de charger l'EPG",
"epg_error_sub": "V\u00e9rifiez l'URL de votre flux XMLTV.", "epg_error_sub": "Vérifiez l'URL de votre flux XMLTV.",
"load": "Charger", "load": "Charger",
"search_placeholder": "Rechercher une cha\u00eene ou un programme...", "search_placeholder": "Rechercher une chaîne ou un programme...",
"copy_epg": "EPG", "copy_epg": "EPG",
"copy_m3u": "M3U", "copy_m3u": "M3U",
"copied": "\u2713 copi\u00e9", "copied": "✓ copié",
"program_info": "Infos programme", "program_info": "Infos programme",
"watch_now": "Regarder", "watch_now": "Regarder",
"imdb_search": "Rechercher sur IMDb", "imdb_search": "Rechercher sur IMDb",
@@ -24,7 +45,9 @@
"personal_epg_optional": "optionnel", "personal_epg_optional": "optionnel",
"cancel": "Annuler", "cancel": "Annuler",
"apply": "Appliquer", "apply": "Appliquer",
"setup_already_done": "Configuration d\u00e9j\u00e0 effectu\u00e9e. Utilisez votre cl\u00e9 admin pour acc\u00e9der \u00e0 nouveau au setup.", "setup_already_done": "Configuration déjà effectuée. Utilisez votre clé admin pour accéder à nouveau au setup.",
"shift_back": "\u25c4 1h", "shift_back": " 1h",
"shift_fwd": "1h \u25ba" "shift_fwd": "1h ►",
"fav_add": "Ajouter aux favoris",
"fav_remove": "Retirer des favoris"
} }
+20 -7
View File
@@ -10,10 +10,7 @@ function makePlaceholder(name) {
return el; return el;
} }
function renderChannels() { function makeChannelCell(ch) {
const list = document.getElementById('channelsList');
list.innerHTML = '';
channels.forEach(ch => {
const cell = document.createElement('div'); const cell = document.createElement('div');
cell.className = 'channel-cell'; cell.className = 'channel-cell';
if (ch.icon) { if (ch.icon) {
@@ -25,9 +22,23 @@ function renderChannels() {
const name = document.createElement('div'); const name = document.createElement('div');
name.className = 'channel-name'; name.textContent = ch.name; name.title = ch.name; name.className = 'channel-name'; name.textContent = ch.name; name.title = ch.name;
cell.appendChild(name); cell.appendChild(name);
cell.appendChild(makeFavBtn(ch.id));
cell.addEventListener('click', () => openPip(ch.name, getNowPlaying(ch.id))); cell.addEventListener('click', () => openPip(ch.name, getNowPlaying(ch.id)));
list.appendChild(cell); return cell;
}); }
function renderChannels() {
const list = document.getElementById('channelsList');
list.innerHTML = '';
const { favs, rest } = sortedChannels(channels);
if (favs.length) {
favs.forEach(ch => list.appendChild(makeChannelCell(ch)));
const sep = document.createElement('div');
sep.className = 'fav-separator';
list.appendChild(sep);
}
rest.forEach(ch => list.appendChild(makeChannelCell(ch)));
} }
function renderRuler() { function renderRuler() {
@@ -70,7 +81,9 @@ function renderPrograms() {
const now = new Date(); const now = new Date();
channels.forEach((ch, i) => { const { favs: _f, rest: _r } = sortedChannels(channels);
const orderedChannels = [..._f, ..._r];
orderedChannels.forEach((ch, i) => {
const row = document.createElement('div'); const row = document.createElement('div');
row.className = 'program-row'; row.className = 'program-row';
row.style.cssText = `top:${i*ROW_H}px;position:absolute;left:0;right:0;`; row.style.cssText = `top:${i*ROW_H}px;position:absolute;left:0;right:0;`;
+35
View File
@@ -0,0 +1,35 @@
// ── FAVORITES ─────────────────────────────────────────────────────────────────
let favorites = new Set(JSON.parse(localStorage.getItem('gridtv-favorites') || '[]'));
function saveFavorites() {
localStorage.setItem('gridtv-favorites', JSON.stringify([...favorites]));
}
function toggleFavorite(chId, btn) {
if (favorites.has(chId)) {
favorites.delete(chId);
btn.classList.remove('is-fav');
btn.title = L.fav_add;
} else {
favorites.add(chId);
btn.classList.add('is-fav');
btn.title = L.fav_remove;
}
saveFavorites();
renderAll();
}
function makeFavBtn(chId) {
const btn = document.createElement('button');
btn.className = 'fav-btn' + (favorites.has(chId) ? ' is-fav' : '');
btn.title = favorites.has(chId) ? L.fav_remove : L.fav_add;
btn.textContent = '★';
btn.addEventListener('click', e => { e.stopPropagation(); toggleFavorite(chId, btn); });
return btn;
}
function sortedChannels(list) {
const favs = list.filter(ch => favorites.has(ch.id));
const rest = list.filter(ch => !favorites.has(ch.id));
return { favs, rest };
}
+3 -1
View File
@@ -6,7 +6,9 @@ function renderMobile() {
const winStart = new Date(now.getTime() - 30*60000); const winStart = new Date(now.getTime() - 30*60000);
const winEnd = new Date(now.getTime() + 4*3600000); const winEnd = new Date(now.getTime() + 4*3600000);
channels.forEach(ch => { const { favs: _f, rest: _r } = sortedChannels(channels);
const orderedChannels = [..._f, ..._r];
orderedChannels.forEach(ch => {
const progs = (programs[ch.id]||[]).filter(p => p.stop>winStart && p.start<winEnd); const progs = (programs[ch.id]||[]).filter(p => p.stop>winStart && p.start<winEnd);
if (!progs.length) return; if (!progs.length) return;
+1 -1
View File
@@ -3,7 +3,7 @@
<?php <?php
$js_dir = __DIR__ . '/../js/'; $js_dir = __DIR__ . '/../js/';
include $js_dir . 'config.js'; include $js_dir . 'config.js';
$js_modules = ['utils', 'epg', 'mobile', 'tooltip', 'sources', 'm3u', 'player', 'themes', 'search', 'program', 'live']; $js_modules = ['utils', 'favorites', 'epg', 'mobile', 'tooltip', 'sources', 'm3u', 'player', 'themes', 'search', 'program', 'live'];
foreach ($js_modules as $mod) { foreach ($js_modules as $mod) {
echo file_get_contents($js_dir . $mod . '.js'); echo file_get_contents($js_dir . $mod . '.js');
} }
+18
View File
@@ -851,6 +851,24 @@
background: var(--accent); opacity: .4; width: 2px; background: var(--accent); opacity: .4; width: 2px;
} }
/* ── FAVORITES ───────────────────────────────────────────────────────────── */
.fav-btn {
background: none; border: none; color: var(--text-muted);
cursor: pointer; font-size: 14px; line-height: 1;
margin-left: auto; padding: 2px 4px; flex-shrink: 0;
transition: color .15s, transform .15s;
opacity: 0;
}
.channel-cell:hover .fav-btn { opacity: 1; }
.fav-btn.is-fav { color: var(--accent); opacity: 1; }
.fav-btn:hover { transform: scale(1.2); }
.fav-separator {
height: 2px; background: var(--accent);
opacity: .3; margin: 0;
}
</style> </style>
<link id="themeStylesheet" rel="stylesheet" href="themes/default.css"> <link id="themeStylesheet" rel="stylesheet" href="themes/default.css">
</head> </head>