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 = `
+
${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.channel}
+