From 7b42539bc833b3ad8a215496e40ca831b30d4504 Mon Sep 17 00:00:00 2001 From: Johnnybegood90 Date: Fri, 13 Mar 2026 20:46:01 +0100 Subject: [PATCH] Feat: Fav channel in localstorage --- locales/en.json | 59 +++++++++++++++++++++++++++------------- locales/es.json | 61 +++++++++++++++++++++++++++++------------- locales/fr.json | 65 ++++++++++++++++++++++++++++++--------------- src/js/epg.js | 45 ++++++++++++++++++++----------- src/js/favorites.js | 35 ++++++++++++++++++++++++ src/js/mobile.js | 4 ++- src/tpl/footer.php | 2 +- src/tpl/head.php | 18 +++++++++++++ 8 files changed, 213 insertions(+), 76 deletions(-) create mode 100644 src/js/favorites.js diff --git a/locales/en.json b/locales/en.json index 87b8bb7..328e822 100644 --- a/locales/en.json +++ b/locales/en.json @@ -1,30 +1,53 @@ { - "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", + "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", + "load": "Load", "search_placeholder": "Search channel or program...", - "copy_epg": "EPG", - "copy_m3u": "M3U", - "copied": "\u2713 copied", + "copy_epg": "EPG", + "copy_m3u": "M3U", + "copied": "✓ copied", "program_info": "Program info", - "watch_now": "Watch now", + "watch_now": "Watch now", "imdb_search": "Search on IMDb", - "close": "Close", + "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", + "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" -} + "shift_back": "◄ 1h", + "shift_fwd": "1h ►", + "fav_add": "Add to favorites", + "fav_remove": "Remove from favorites" +} \ No newline at end of file diff --git a/locales/es.json b/locales/es.json index 3b37ae6..5b71a3e 100644 --- a/locales/es.json +++ b/locales/es.json @@ -1,30 +1,53 @@ { - "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", + "days": [ + "Dom", + "Lun", + "Mar", + "Mié", + "Jue", + "Vie", + "Sáb" + ], + "months": [ + "Ene", + "Feb", + "Mar", + "Abr", + "May", + "Jun", + "Jul", + "Ago", + "Sep", + "Oct", + "Nov", + "Dic" + ], + "now": "Ahora", + "channels": "Canales", + "live": "LIVE", + "loading": "Cargando guía...", + "epg_error": "No se puede cargar el EPG", "epg_error_sub": "Comprueba la URL de tu fuente XMLTV.", - "load": "Cargar", + "load": "Cargar", "search_placeholder": "Buscar canal o programa...", - "copy_epg": "EPG", - "copy_m3u": "M3U", - "copied": "\u2713 copiado", + "copy_epg": "EPG", + "copy_m3u": "M3U", + "copied": "✓ copiado", "program_info": "Info del programa", - "watch_now": "Ver ahora", + "watch_now": "Ver ahora", "imdb_search": "Buscar en IMDb", - "close": "Cerrar", + "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" -} + "cancel": "Cancelar", + "apply": "Aplicar", + "setup_already_done": "Configuración ya realizada. Usa tu clave de administrador para volver al setup.", + "shift_back": "◄ 1h", + "shift_fwd": "1h ►", + "fav_add": "Agregar a favoritos", + "fav_remove": "Quitar de favoritos" +} \ No newline at end of file diff --git a/locales/fr.json b/locales/fr.json index 99d1735..2ba1338 100644 --- a/locales/fr.json +++ b/locales/fr.json @@ -1,30 +1,53 @@ { - "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", + "days": [ + "Dim", + "Lun", + "Mar", + "Mer", + "Jeu", + "Ven", + "Sam" + ], + "months": [ + "Jan", + "Fév", + "Mar", + "Avr", + "Mai", + "Jun", + "Jul", + "Aoû", + "Sep", + "Oct", + "Nov", + "Déc" + ], + "now": "Maintenant", + "channels": "Chaînes", + "live": "LIVE", + "loading": "Chargement du guide...", + "epg_error": "Impossible de charger l'EPG", + "epg_error_sub": "Vérifiez l'URL de votre flux XMLTV.", + "load": "Charger", + "search_placeholder": "Rechercher une chaîne ou un programme...", + "copy_epg": "EPG", + "copy_m3u": "M3U", + "copied": "✓ copié", "program_info": "Infos programme", - "watch_now": "Regarder", + "watch_now": "Regarder", "imdb_search": "Rechercher sur IMDb", - "close": "Fermer", + "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" -} + "cancel": "Annuler", + "apply": "Appliquer", + "setup_already_done": "Configuration déjà effectuée. Utilisez votre clé admin pour accéder à nouveau au setup.", + "shift_back": "◄ 1h", + "shift_fwd": "1h ►", + "fav_add": "Ajouter aux favoris", + "fav_remove": "Retirer des favoris" +} \ No newline at end of file diff --git a/src/js/epg.js b/src/js/epg.js index 444c0dd..3d30c5c 100644 --- a/src/js/epg.js +++ b/src/js/epg.js @@ -10,24 +10,35 @@ function makePlaceholder(name) { return el; } +function makeChannelCell(ch) { + const cell = document.createElement('div'); + cell.className = 'channel-cell'; + if (ch.icon) { + const img = document.createElement('img'); + img.className = 'channel-logo'; img.src = ch.icon; + img.onerror = () => img.replaceWith(makePlaceholder(ch.name)); + cell.appendChild(img); + } else { cell.appendChild(makePlaceholder(ch.name)); } + const name = document.createElement('div'); + name.className = 'channel-name'; name.textContent = ch.name; name.title = ch.name; + cell.appendChild(name); + cell.appendChild(makeFavBtn(ch.id)); + cell.addEventListener('click', () => openPip(ch.name, getNowPlaying(ch.id))); + return cell; +} + function renderChannels() { const list = document.getElementById('channelsList'); list.innerHTML = ''; - channels.forEach(ch => { - const cell = document.createElement('div'); - cell.className = 'channel-cell'; - if (ch.icon) { - const img = document.createElement('img'); - img.className = 'channel-logo'; img.src = ch.icon; - img.onerror = () => img.replaceWith(makePlaceholder(ch.name)); - cell.appendChild(img); - } else { cell.appendChild(makePlaceholder(ch.name)); } - const name = document.createElement('div'); - name.className = 'channel-name'; name.textContent = ch.name; name.title = ch.name; - cell.appendChild(name); - cell.addEventListener('click', () => openPip(ch.name, getNowPlaying(ch.id))); - list.appendChild(cell); - }); + 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() { @@ -70,7 +81,9 @@ function renderPrograms() { 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'); row.className = 'program-row'; row.style.cssText = `top:${i*ROW_H}px;position:absolute;left:0;right:0;`; diff --git a/src/js/favorites.js b/src/js/favorites.js new file mode 100644 index 0000000..6b31e40 --- /dev/null +++ b/src/js/favorites.js @@ -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 }; +} diff --git a/src/js/mobile.js b/src/js/mobile.js index febaf31..d26e1f6 100644 --- a/src/js/mobile.js +++ b/src/js/mobile.js @@ -6,7 +6,9 @@ function renderMobile() { const winStart = new Date(now.getTime() - 30*60000); 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