diff --git a/frontend/app.js b/frontend/app.js index 3614532..39084f9 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -11,6 +11,8 @@ let playerMode = 'modal'; let playerHome = null; let onFullscreenChange = null; let onWebkitEndFullscreen = null; +const FAVORITES_KEY = 'favorites'; +const FAVORITES_VISIBILITY_KEY = 'favoritesVisible'; // 2. Observer Definition (Must be defined before initApp uses it) const observer = new IntersectionObserver((entries) => { @@ -33,6 +35,12 @@ async function InitializeLocalStorage() { if (!localStorage.getItem('theme')) { localStorage.setItem('theme', 'dark'); } + if (!localStorage.getItem(FAVORITES_KEY)) { + localStorage.setItem(FAVORITES_KEY, JSON.stringify([])); + } + if (!localStorage.getItem(FAVORITES_VISIBILITY_KEY)) { + localStorage.setItem(FAVORITES_VISIBILITY_KEY, 'true'); + } // We always run this to make sure session is fresh await InitializeServerStatus(); } @@ -151,18 +159,29 @@ function renderVideos(videos) { if (!grid) return; const items = videos && Array.isArray(videos.items) ? videos.items : []; + const favoritesSet = getFavoritesSet(); items.forEach(v => { if (renderedVideoIds.has(v.id)) return; const card = document.createElement('div'); card.className = 'video-card'; const durationText = formatDuration(v.duration); + const favoriteKey = getFavoriteKey(v); card.innerHTML = ` + ${v.title}

${v.title}

${v.channel}

${durationText ? `

${durationText}

` : ''} `; + const favoriteBtn = card.querySelector('.favorite-btn'); + if (favoriteBtn && favoriteKey) { + setFavoriteButtonState(favoriteBtn, favoritesSet.has(favoriteKey)); + favoriteBtn.onclick = (event) => { + event.stopPropagation(); + toggleFavorite(v); + }; + } card.onclick = () => openPlayer(v.url); grid.appendChild(card); renderedVideoIds.add(v.id); @@ -206,6 +225,122 @@ function getMobileVideoHost() { return host; } +function getFavorites() { + try { + const raw = localStorage.getItem(FAVORITES_KEY); + const parsed = raw ? JSON.parse(raw) : []; + return Array.isArray(parsed) ? parsed : []; + } catch (err) { + return []; + } +} + +function setFavorites(items) { + localStorage.setItem(FAVORITES_KEY, JSON.stringify(items)); +} + +function getFavoriteKey(video) { + if (!video) return null; + return video.key || video.id || video.url || null; +} + +function normalizeFavorite(video) { + const key = getFavoriteKey(video); + if (!key) return null; + return { + key, + id: video.id || null, + url: video.url || '', + title: video.title || '', + thumb: video.thumb || '', + channel: video.channel || '', + duration: video.duration || 0 + }; +} + +function getFavoritesSet() { + return new Set(getFavorites().map((item) => item.key)); +} + +function setFavoriteButtonState(button, isFavorite) { + button.classList.toggle('is-favorite', isFavorite); + button.textContent = isFavorite ? '♥' : '♡'; + button.setAttribute('aria-pressed', isFavorite ? 'true' : 'false'); + button.setAttribute('aria-label', isFavorite ? 'Remove from favorites' : 'Add to favorites'); +} + +function syncFavoriteButtons() { + const favoritesSet = getFavoritesSet(); + document.querySelectorAll('.favorite-btn[data-fav-key]').forEach((button) => { + const key = button.dataset.favKey; + if (!key) return; + setFavoriteButtonState(button, favoritesSet.has(key)); + }); +} + +function toggleFavorite(video) { + const key = getFavoriteKey(video); + if (!key) return; + const favorites = getFavorites(); + const existingIndex = favorites.findIndex((item) => item.key === key); + if (existingIndex >= 0) { + favorites.splice(existingIndex, 1); + } else { + const entry = normalizeFavorite(video); + if (entry) favorites.unshift(entry); + } + setFavorites(favorites); + renderFavoritesBar(); + syncFavoriteButtons(); +} + +function isFavoritesVisible() { + return localStorage.getItem(FAVORITES_VISIBILITY_KEY) !== 'false'; +} + +function setFavoritesVisible(isVisible) { + localStorage.setItem(FAVORITES_VISIBILITY_KEY, isVisible ? 'true' : 'false'); +} + +function renderFavoritesBar() { + const bar = document.getElementById('favorites-bar'); + const list = document.getElementById('favorites-list'); + const empty = document.getElementById('favorites-empty'); + if (!bar || !list) return; + + const favorites = getFavorites(); + const visible = isFavoritesVisible(); + bar.style.display = visible ? 'block' : 'none'; + + list.innerHTML = ""; + favorites.forEach((item) => { + const card = document.createElement('div'); + card.className = 'favorite-card'; + card.dataset.favKey = item.key; + card.innerHTML = ` + + ${item.title} +
+

${item.title}

+

${item.channel}

+
+ `; + card.onclick = () => openPlayer(item.url); + const favoriteBtn = card.querySelector('.favorite-btn'); + if (favoriteBtn) { + favoriteBtn.onclick = (event) => { + event.stopPropagation(); + toggleFavorite(item); + }; + } + list.appendChild(card); + }); + + if (empty) { + empty.style.display = favorites.length > 0 ? 'none' : 'block'; + } +} + // 4. Initialization (Run this last) async function initApp() { // Clear old data if you want a fresh start every refresh @@ -214,6 +349,7 @@ async function initApp() { await InitializeLocalStorage(); applyTheme(); renderMenu(); + renderFavoritesBar(); const sentinel = document.getElementById('sentinel'); if (sentinel) { @@ -240,6 +376,7 @@ async function initApp() { }); await loadVideos(); + syncFavoriteButtons(); } function applyTheme() { @@ -595,6 +732,7 @@ function renderMenu() { const addSourceBtn = document.getElementById('add-source-btn'); const sourceInput = document.getElementById('source-input'); const reloadChannelBtn = document.getElementById('reload-channel-btn'); + const favoritesToggle = document.getElementById('favorites-toggle'); if (!sourceSelect || !channelSelect || !filtersContainer) return; @@ -686,6 +824,14 @@ function renderMenu() { }; } + if (favoritesToggle) { + favoritesToggle.checked = isFavoritesVisible(); + favoritesToggle.onchange = () => { + setFavoritesVisible(favoritesToggle.checked); + renderFavoritesBar(); + }; + } + if (sourcesList) { sourcesList.innerHTML = ""; serverEntries.forEach((entry) => { diff --git a/frontend/index.html b/frontend/index.html index 0ff17cf..927ef78 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -29,6 +29,14 @@ +
+
+

Favorites

+
+
+
No favorites yet. Tap the heart on a video to save it here.
+
+