diff --git a/frontend/app.js b/frontend/app.js
deleted file mode 100644
index 39084f9..0000000
--- a/frontend/app.js
+++ /dev/null
@@ -1,1123 +0,0 @@
-// 1. Global State and Constants (Declare these first!)
-let currentPage = 1;
-const perPage = 12;
-const renderedVideoIds = new Set();
-let hasNextPage = true;
-let isLoading = false;
-let hlsPlayer = null;
-let currentLoadController = null;
-let errorToastTimer = null;
-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) => {
- if (entries[0].isIntersecting) loadVideos();
-}, {
- threshold: 1.0
-});
-
-// 3. Logic Functions
-async function InitializeLocalStorage() {
- if (!localStorage.getItem('config')) {
- localStorage.setItem('config', JSON.stringify({
- servers: [
- { "https://getfigleaf.com": {} },
- { "https://hottubapp.io": {} },
- { "https://hottub.spacemoehre.de": {} }
- ]
- }));
- }
- 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();
-}
-
-async function InitializeServerStatus() {
- const config = JSON.parse(localStorage.getItem('config'));
- if (!config || !config.servers) return;
-
- const statusPromises = config.servers.map(async (serverObj) => {
- const server = Object.keys(serverObj)[0];
- try {
- const response = await fetch(`/api/status`, {
- method: "POST",
- body: JSON.stringify({
- server: server
- }),
- headers: {
- "Content-Type": "application/json"
- },
- });
- const status = await response.json();
- serverObj[server] = status;
- } catch (err) {
- serverObj[server] = {
- online: false,
- channels: []
- };
- }
- });
-
- await Promise.all(statusPromises);
- localStorage.setItem('config', JSON.stringify(config));
-
- const existingSession = getSession();
- const serverKeys = config.servers.map((serverObj) => Object.keys(serverObj)[0]);
- const selectedServerKey = existingSession && serverKeys.includes(existingSession.server)
- ? existingSession.server
- : serverKeys[0];
- const serverData = config.servers.find((serverObj) => Object.keys(serverObj)[0] === selectedServerKey)[selectedServerKey];
-
- if (serverData.channels && serverData.channels.length > 0) {
- const prefs = getPreferences();
- const serverPrefs = prefs[selectedServerKey] || {};
- const preferredChannelId = serverPrefs.channelId;
- const channel = serverData.channels.find((ch) => ch.id === preferredChannelId) || serverData.channels[0];
- const savedOptions = serverPrefs.optionsByChannel ? serverPrefs.optionsByChannel[channel.id] : null;
- const options = savedOptions ? hydrateOptions(channel, savedOptions) : buildDefaultOptions(channel);
-
- const sessionData = {
- server: selectedServerKey,
- channel: channel,
- options: options,
- };
-
- setSession(sessionData);
- savePreference(sessionData);
- }
-}
-
-async function loadVideos() {
- const session = JSON.parse(localStorage.getItem('session'));
- if (!session) return;
- if (isLoading || !hasNextPage) return;
-
- const searchInput = document.getElementById('search-input');
- const query = searchInput ? searchInput.value : "";
-
- // Build the request body
- let body = {
- channel: session.channel.id,
- query: query || "",
- page: currentPage,
- perPage: perPage,
- server: session.server
- };
-
- // Correct way to loop through the options object
- Object.entries(session.options).forEach(([key, value]) => {
- if (Array.isArray(value)) {
- body[key] = value.map((entry) => entry.id).join(", ");
- } else if (value && value.id) {
- body[key] = value.id;
- }
- });
-
- try {
- isLoading = true;
- updateLoadMoreState();
- currentLoadController = new AbortController();
- const response = await fetch('/api/videos', {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json'
- },
- body: JSON.stringify(body),
- signal: currentLoadController.signal
- });
- const videos = await response.json();
- renderVideos(videos);
- hasNextPage = videos && videos.pageInfo ? videos.pageInfo.hasNextPage !== false : true;
- currentPage++;
- ensureViewportFilled();
- } catch (err) {
- if (err.name !== 'AbortError') {
- console.error("Failed to load videos:", err);
- }
- } finally {
- isLoading = false;
- currentLoadController = null;
- updateLoadMoreState();
- }
-}
-
-function renderVideos(videos) {
- const grid = document.getElementById('video-grid');
- 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.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);
- });
-
- ensureViewportFilled();
-}
-
-function formatDuration(seconds) {
- if (!seconds || seconds <= 0) return '';
- const totalSeconds = Math.floor(seconds);
- const hours = Math.floor(totalSeconds / 3600);
- const minutes = Math.floor((totalSeconds % 3600) / 60);
- if (hours > 0) {
- return `${hours}h ${String(minutes).padStart(2, '0')}m`;
- }
- return `${minutes}m`;
-}
-
-function isMobilePlayback() {
- if (navigator.userAgentData && typeof navigator.userAgentData.mobile === 'boolean') {
- return navigator.userAgentData.mobile;
- }
- const ua = navigator.userAgent || '';
- if (/iPhone|iPad|iPod|Android/i.test(ua)) return true;
- return window.matchMedia('(pointer: coarse)').matches && window.matchMedia('(max-width: 900px)').matches;
-}
-
-function isTvPlayback() {
- const ua = navigator.userAgent || '';
- return /SMART-TV|SmartTV|Smart TV|Internet\.TV|HbbTV|NetCast|Web0S|webOS|Tizen|AppleTV|Apple TV|GoogleTV|Android TV|AFTB|AFTS|AFTM|AFTT|AFTQ|AFTK|AFTN|AFTMM|AFTKR|Roku|DTV|BRAVIA|VIZIO|SHIELD|PhilipsTV|Hisense|VIDAA|TOSHIBA/i.test(ua);
-}
-
-function getMobileVideoHost() {
- let host = document.getElementById('mobile-video-host');
- if (!host) {
- host = document.createElement('div');
- host.id = 'mobile-video-host';
- document.body.appendChild(host);
- }
- 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.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
- // localStorage.clear();
-
- await InitializeLocalStorage();
- applyTheme();
- renderMenu();
- renderFavoritesBar();
-
- const sentinel = document.getElementById('sentinel');
- if (sentinel) {
- observer.observe(sentinel);
- }
-
- const loadMoreBtn = document.getElementById('load-more-btn');
- if (loadMoreBtn) {
- loadMoreBtn.onclick = () => {
- loadVideos();
- };
- }
-
- const errorToastClose = document.getElementById('error-toast-close');
- if (errorToastClose) {
- errorToastClose.onclick = () => {
- const toast = document.getElementById('error-toast');
- if (toast) toast.classList.remove('show');
- };
- }
-
- window.addEventListener('resize', () => {
- ensureViewportFilled();
- });
-
- await loadVideos();
- syncFavoriteButtons();
-}
-
-function applyTheme() {
- const theme = localStorage.getItem('theme') || 'dark';
- document.body.classList.toggle('theme-light', theme === 'light');
- const select = document.getElementById('theme-select');
- if (select) select.value = theme;
-}
-
-function showError(message) {
- const toast = document.getElementById('error-toast');
- const text = document.getElementById('error-toast-text');
- if (!toast || !text) return;
- text.textContent = message;
- toast.classList.add('show');
- if (errorToastTimer) {
- clearTimeout(errorToastTimer);
- }
- errorToastTimer = setTimeout(() => {
- toast.classList.remove('show');
- }, 4000);
-}
-
-async function openPlayer(url) {
- const modal = document.getElementById('video-modal');
- const video = document.getElementById('player');
- const useMobileFullscreen = isMobilePlayback() || isTvPlayback();
- let playbackStarted = false;
-
- if (!playerHome) {
- playerHome = video.parentElement;
- }
-
- // 1. Define isHls (the missing piece!)
- let refererParam = '';
- try {
- const origin = new URL(url).origin;
- refererParam = `&referer=${encodeURIComponent(origin + '/')}`;
- } catch (err) {
- refererParam = '';
- }
- const streamUrl = `/api/stream?url=${encodeURIComponent(url)}${refererParam}`;
- let isHls = /\.m3u8($|\?)/i.test(url);
-
- // 2. Cleanup existing player instance to prevent aborted bindings
- if (hlsPlayer) {
- hlsPlayer.stopLoad();
- hlsPlayer.detachMedia();
- hlsPlayer.destroy();
- hlsPlayer = null;
- }
-
- // 3. Reset the video element
- video.pause();
- video.removeAttribute('src');
- video.load();
-
- if (!isHls) {
- try {
- const headResp = await fetch(streamUrl, { method: 'HEAD' });
- const contentType = headResp.headers.get('Content-Type') || '';
- if (contentType.includes('application/vnd.apple.mpegurl')) {
- isHls = true;
- }
- } catch (err) {
- console.warn('Failed to detect stream type', err);
- }
- }
-
- if (useMobileFullscreen) {
- const host = getMobileVideoHost();
- if (video.parentElement !== host) {
- host.appendChild(video);
- }
- playerMode = 'mobile';
- video.removeAttribute('playsinline');
- video.removeAttribute('webkit-playsinline');
- video.playsInline = false;
- } else {
- if (playerHome && video.parentElement !== playerHome) {
- playerHome.appendChild(video);
- }
- playerMode = 'modal';
- video.setAttribute('playsinline', '');
- video.setAttribute('webkit-playsinline', '');
- video.playsInline = true;
- }
-
- const requestFullscreen = () => {
- if (playerMode !== 'mobile') return;
- if (typeof video.webkitEnterFullscreen === 'function') {
- try {
- video.webkitEnterFullscreen();
- } catch (err) {
- // Ignore if fullscreen is not allowed.
- }
- return;
- }
- if (video.requestFullscreen) {
- video.requestFullscreen().catch(() => {});
- }
- };
-
- const startPlayback = () => {
- if (playbackStarted) return;
- playbackStarted = true;
- const playPromise = video.play();
- if (playPromise && typeof playPromise.catch === 'function') {
- playPromise.catch(() => {});
- }
- if (playerMode === 'mobile') {
- if (video.readyState >= 1) {
- requestFullscreen();
- } else {
- video.addEventListener('loadedmetadata', requestFullscreen, { once: true });
- }
- }
- };
-
- if (isHls) {
- if (window.Hls && window.Hls.isSupported()) {
- hlsPlayer = new window.Hls();
- hlsPlayer.loadSource(streamUrl);
- hlsPlayer.attachMedia(video);
- hlsPlayer.on(window.Hls.Events.MANIFEST_PARSED, function() {
- startPlayback();
- });
- startPlayback();
- hlsPlayer.on(window.Hls.Events.ERROR, function(event, data) {
- if (data && data.fatal) {
- showError('Unable to play this stream.');
- closePlayer();
- }
- });
- } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
- video.src = streamUrl;
- startPlayback();
- } else {
- console.error("HLS not supported in this browser.");
- showError('HLS is not supported in this browser.');
- return;
- }
- } else {
- video.src = streamUrl;
- startPlayback();
- }
-
- video.onerror = () => {
- showError('Video failed to load.');
- closePlayer();
- };
-
- if (playerMode === 'modal') {
- modal.style.display = 'flex';
- document.body.style.overflow = 'hidden';
- } else {
- modal.style.display = 'none';
- document.body.style.overflow = 'auto';
- if (!onFullscreenChange) {
- onFullscreenChange = () => {
- if (playerMode === 'mobile' && !document.fullscreenElement) {
- closePlayer();
- }
- };
- }
- document.addEventListener('fullscreenchange', onFullscreenChange);
- if (!onWebkitEndFullscreen) {
- onWebkitEndFullscreen = () => {
- if (playerMode === 'mobile') {
- closePlayer();
- }
- };
- }
- video.addEventListener('webkitendfullscreen', onWebkitEndFullscreen);
- }
-}
-
-function closePlayer() {
- const modal = document.getElementById('video-modal');
- const video = document.getElementById('player');
- if (hlsPlayer) {
- hlsPlayer.destroy();
- hlsPlayer = null;
- }
- if (document.fullscreenElement && document.exitFullscreen) {
- document.exitFullscreen().catch(() => {});
- }
- if (onFullscreenChange) {
- document.removeEventListener('fullscreenchange', onFullscreenChange);
- }
- if (onWebkitEndFullscreen) {
- video.removeEventListener('webkitendfullscreen', onWebkitEndFullscreen);
- }
- video.onerror = null;
- video.pause();
- video.src = '';
- modal.style.display = 'none';
- document.body.style.overflow = 'auto';
- if (playerHome && video.parentElement !== playerHome) {
- playerHome.appendChild(video);
- }
- playerMode = 'modal';
-}
-
-function handleSearch(value) {
- currentPage = 1;
- hasNextPage = true;
- renderedVideoIds.clear();
- const grid = document.getElementById('video-grid');
- if (grid) grid.innerHTML = "";
- updateLoadMoreState();
- loadVideos();
-}
-
-function getConfig() {
- return JSON.parse(localStorage.getItem('config')) || {
- servers: []
- };
-}
-
-function getSession() {
- return JSON.parse(localStorage.getItem('session')) || null;
-}
-
-function setSession(nextSession) {
- localStorage.setItem('session', JSON.stringify(nextSession));
-}
-
-function getPreferences() {
- return JSON.parse(localStorage.getItem('preferences')) || {};
-}
-
-function setPreferences(nextPreferences) {
- localStorage.setItem('preferences', JSON.stringify(nextPreferences));
-}
-
-function getServerEntries() {
- const config = getConfig();
- if (!config.servers || !Array.isArray(config.servers)) return [];
- return config.servers.map((serverObj) => {
- const server = Object.keys(serverObj)[0];
- return {
- url: server,
- data: serverObj[server] || null
- };
- });
-}
-
-function setConfig(nextConfig) {
- localStorage.setItem('config', JSON.stringify(nextConfig));
-}
-
-async function refreshServerStatus() {
- await InitializeServerStatus();
- renderMenu();
-}
-
-function serializeOptions(options) {
- const serialized = {};
- Object.entries(options || {}).forEach(([key, value]) => {
- if (Array.isArray(value)) {
- serialized[key] = value.map((entry) => entry.id);
- } else if (value && value.id) {
- serialized[key] = value.id;
- }
- });
- return serialized;
-}
-
-function hydrateOptions(channel, savedOptions) {
- const hydrated = {};
- if (!channel || !Array.isArray(channel.options)) return hydrated;
- const saved = savedOptions || {};
- channel.options.forEach((optionGroup) => {
- const allOptions = optionGroup.options || [];
- const savedValue = saved[optionGroup.id];
- if (optionGroup.multiSelect) {
- const selectedIds = Array.isArray(savedValue) ? savedValue : [];
- const selected = allOptions.filter((opt) => selectedIds.includes(opt.id));
- hydrated[optionGroup.id] = selected.length > 0 ? selected : allOptions.slice(0, 1);
- } else {
- const selected = allOptions.find((opt) => opt.id === savedValue) || allOptions[0];
- if (selected) hydrated[optionGroup.id] = selected;
- }
- });
- return hydrated;
-}
-
-function savePreference(session) {
- if (!session || !session.server || !session.channel) return;
- const prefs = getPreferences();
- const serverPrefs = prefs[session.server] || {};
- serverPrefs.channelId = session.channel.id;
- serverPrefs.optionsByChannel = serverPrefs.optionsByChannel || {};
- serverPrefs.optionsByChannel[session.channel.id] = serializeOptions(session.options);
- prefs[session.server] = serverPrefs;
- setPreferences(prefs);
-}
-
-function buildDefaultOptions(channel) {
- const selected = {};
- if (!channel || !Array.isArray(channel.options)) return selected;
- channel.options.forEach((optionGroup) => {
- if (!optionGroup.options || optionGroup.options.length === 0) return;
- if (optionGroup.multiSelect) {
- selected[optionGroup.id] = [optionGroup.options[0]];
- } else {
- selected[optionGroup.id] = optionGroup.options[0];
- }
- });
- return selected;
-}
-
-function resetAndReload() {
- if (currentLoadController) {
- currentLoadController.abort();
- currentLoadController = null;
- isLoading = false;
- }
- currentPage = 1;
- hasNextPage = true;
- renderedVideoIds.clear();
- const grid = document.getElementById('video-grid');
- if (grid) grid.innerHTML = "";
- updateLoadMoreState();
- loadVideos();
-}
-
-function ensureViewportFilled() {
- if (!hasNextPage || isLoading) return;
- const grid = document.getElementById('video-grid');
- if (!grid) return;
- const docHeight = document.documentElement.scrollHeight;
- if (docHeight <= window.innerHeight + 120) {
- window.setTimeout(() => loadVideos(), 0);
- }
-}
-
-function updateLoadMoreState() {
- const loadMoreBtn = document.getElementById('load-more-btn');
- if (!loadMoreBtn) return;
- loadMoreBtn.disabled = isLoading || !hasNextPage;
- loadMoreBtn.style.display = hasNextPage ? 'flex' : 'none';
-}
-
-function renderMenu() {
- const session = getSession();
- const serverEntries = getServerEntries();
- const sourceSelect = document.getElementById('source-select');
- const channelSelect = document.getElementById('channel-select');
- const filtersContainer = document.getElementById('filters-container');
- const sourcesList = document.getElementById('sources-list');
- 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;
-
- sourceSelect.innerHTML = "";
- serverEntries.forEach((entry) => {
- const option = document.createElement('option');
- option.value = entry.url;
- option.textContent = entry.url;
- sourceSelect.appendChild(option);
- });
-
- if (session && session.server) {
- sourceSelect.value = session.server;
- }
-
- sourceSelect.onchange = () => {
- const selectedServerUrl = sourceSelect.value;
- const selectedServer = serverEntries.find((entry) => entry.url === selectedServerUrl);
- const channels = selectedServer && selectedServer.data && selectedServer.data.channels ?
- selectedServer.data.channels :
- [];
- const prefs = getPreferences();
- const serverPrefs = prefs[selectedServerUrl] || {};
- const preferredChannel = channels.find((channel) => channel.id === serverPrefs.channelId) || null;
- const nextChannel = preferredChannel || (channels.length > 0 ? channels[0] : null);
- const savedOptions = nextChannel && serverPrefs.optionsByChannel ?
- serverPrefs.optionsByChannel[nextChannel.id] :
- null;
- const nextSession = {
- server: selectedServerUrl,
- channel: nextChannel,
- options: nextChannel ? (savedOptions ? hydrateOptions(nextChannel, savedOptions) : buildDefaultOptions(nextChannel)) : {}
- };
- setSession(nextSession);
- savePreference(nextSession);
- renderMenu();
- resetAndReload();
- };
-
- const activeServer = serverEntries.find((entry) => entry.url === (session && session.server));
- const availableChannels = activeServer && activeServer.data && activeServer.data.channels ?
- [...activeServer.data.channels] :
- [];
- availableChannels.sort((a, b) => {
- const nameA = (a.name || a.id || '').toLowerCase();
- const nameB = (b.name || b.id || '').toLowerCase();
- return nameA.localeCompare(nameB);
- });
-
- channelSelect.innerHTML = "";
- availableChannels.forEach((channel) => {
- const option = document.createElement('option');
- option.value = channel.id;
- option.textContent = channel.name || channel.id;
- channelSelect.appendChild(option);
- });
-
- if (session && session.channel) {
- channelSelect.value = session.channel.id;
- }
-
- channelSelect.onchange = () => {
- const selectedId = channelSelect.value;
- const nextChannel = availableChannels.find((channel) => channel.id === selectedId) || null;
- const prefs = getPreferences();
- const serverPrefs = prefs[session.server] || {};
- const savedOptions = nextChannel && serverPrefs.optionsByChannel ?
- serverPrefs.optionsByChannel[nextChannel.id] :
- null;
- const nextSession = {
- server: session.server,
- channel: nextChannel,
- options: nextChannel ? (savedOptions ? hydrateOptions(nextChannel, savedOptions) : buildDefaultOptions(nextChannel)) : {}
- };
- setSession(nextSession);
- savePreference(nextSession);
- renderMenu();
- resetAndReload();
- };
-
- renderFilters(filtersContainer, session);
-
- const themeSelect = document.getElementById('theme-select');
- if (themeSelect) {
- themeSelect.onchange = () => {
- const nextTheme = themeSelect.value === 'light' ? 'light' : 'dark';
- localStorage.setItem('theme', nextTheme);
- applyTheme();
- };
- }
-
- if (favoritesToggle) {
- favoritesToggle.checked = isFavoritesVisible();
- favoritesToggle.onchange = () => {
- setFavoritesVisible(favoritesToggle.checked);
- renderFavoritesBar();
- };
- }
-
- if (sourcesList) {
- sourcesList.innerHTML = "";
- serverEntries.forEach((entry) => {
- const row = document.createElement('div');
- row.className = 'source-item';
-
- const text = document.createElement('span');
- text.textContent = entry.url;
-
- const removeBtn = document.createElement('button');
- removeBtn.type = 'button';
- removeBtn.textContent = 'Remove';
- removeBtn.onclick = async () => {
- const config = getConfig();
- config.servers = (config.servers || []).filter((serverObj) => {
- const key = Object.keys(serverObj)[0];
- return key !== entry.url;
- });
- setConfig(config);
- const prefs = getPreferences();
- if (prefs[entry.url]) {
- delete prefs[entry.url];
- setPreferences(prefs);
- }
-
- const remaining = getServerEntries();
- if (remaining.length === 0) {
- localStorage.removeItem('session');
- } else {
- const nextServerUrl = remaining[0].url;
- const nextServer = remaining[0];
- const serverPrefs = prefs[nextServerUrl] || {};
- const channels = nextServer.data && nextServer.data.channels ? nextServer.data.channels : [];
- const nextChannel = channels.find((channel) => channel.id === serverPrefs.channelId) || channels[0] || null;
- const savedOptions = nextChannel && serverPrefs.optionsByChannel ? serverPrefs.optionsByChannel[nextChannel.id] : null;
- const nextSession = {
- server: nextServerUrl,
- channel: nextChannel,
- options: nextChannel ? (savedOptions ? hydrateOptions(nextChannel, savedOptions) : buildDefaultOptions(nextChannel)) : {}
- };
- setSession(nextSession);
- savePreference(nextSession);
- }
-
- await refreshServerStatus();
- resetAndReload();
- };
-
- row.appendChild(text);
- row.appendChild(removeBtn);
- sourcesList.appendChild(row);
- });
- }
-
- if (addSourceBtn && sourceInput) {
- addSourceBtn.onclick = async () => {
- const raw = sourceInput.value.trim();
- if (!raw) return;
- const normalized = raw.endsWith('/') ? raw.slice(0, -1) : raw;
-
- const config = getConfig();
- const exists = (config.servers || []).some((serverObj) => Object.keys(serverObj)[0] === normalized);
- if (!exists) {
- config.servers = config.servers || [];
- config.servers.push({
- [normalized]: {}
- });
- setConfig(config);
- sourceInput.value = '';
- await refreshServerStatus();
-
- const session = getSession();
- if (!session || session.server !== normalized) {
- const entries = getServerEntries();
- const addedEntry = entries.find((entry) => entry.url === normalized);
- const nextChannel = addedEntry && addedEntry.data && addedEntry.data.channels ?
- addedEntry.data.channels[0] :
- null;
- const nextSession = {
- server: normalized,
- channel: nextChannel,
- options: nextChannel ? buildDefaultOptions(nextChannel) : {}
- };
- setSession(nextSession);
- savePreference(nextSession);
- }
- renderMenu();
- resetAndReload();
- }
- };
- }
-
- if (reloadChannelBtn) {
- reloadChannelBtn.onclick = () => {
- resetAndReload();
- };
- }
-}
-
-function renderFilters(container, session) {
- container.innerHTML = "";
-
- if (!session || !session.channel || !Array.isArray(session.channel.options)) {
- const empty = document.createElement('div');
- empty.className = 'filters-empty';
- empty.textContent = 'No filters available for this channel.';
- container.appendChild(empty);
- return;
- }
-
- session.channel.options.forEach((optionGroup) => {
- const wrapper = document.createElement('div');
- wrapper.className = 'setting-item';
-
- const labelRow = document.createElement('div');
- labelRow.className = 'setting-label-row';
-
- const label = document.createElement('label');
- label.textContent = optionGroup.title || optionGroup.id;
- labelRow.appendChild(label);
- const options = optionGroup.options || [];
- const currentSelection = session.options ? session.options[optionGroup.id] : null;
-
- if (optionGroup.multiSelect) {
- const actionBtn = document.createElement('button');
- actionBtn.type = 'button';
- actionBtn.className = 'btn-link';
-
- const list = document.createElement('div');
- list.className = 'multi-select';
-
- const selectedIds = new Set(
- Array.isArray(currentSelection)
- ? currentSelection.map((item) => item.id)
- : []
- );
-
- const updateActionLabel = () => {
- const allChecked = options.length > 0 &&
- Array.from(list.querySelectorAll('input[type="checkbox"]'))
- .every((cb) => cb.checked);
- actionBtn.textContent = allChecked ? 'Deselect all' : 'Select all';
- actionBtn.disabled = options.length === 0;
- };
-
- options.forEach((opt) => {
- const item = document.createElement('label');
- item.className = 'multi-select-item';
-
- const checkbox = document.createElement('input');
- checkbox.type = 'checkbox';
- checkbox.value = opt.id;
- checkbox.checked = selectedIds.has(opt.id);
-
- const text = document.createElement('span');
- text.textContent = opt.title || opt.id;
-
- checkbox.onchange = () => {
- const nextSession = getSession();
- if (!nextSession || !nextSession.channel) return;
- const selected = [];
- list.querySelectorAll('input[type="checkbox"]').forEach((cb) => {
- if (cb.checked) {
- const found = options.find((item) => item.id === cb.value);
- if (found) selected.push(found);
- }
- });
- nextSession.options[optionGroup.id] = selected;
- setSession(nextSession);
- savePreference(nextSession);
- resetAndReload();
- updateActionLabel();
- };
-
- item.appendChild(checkbox);
- item.appendChild(text);
- list.appendChild(item);
- });
-
- updateActionLabel();
-
- actionBtn.onclick = () => {
- const checkboxes = Array.from(list.querySelectorAll('input[type="checkbox"]'));
- const allChecked = checkboxes.length > 0 && checkboxes.every((cb) => cb.checked);
- checkboxes.forEach((cb) => {
- cb.checked = !allChecked;
- });
-
- const nextSession = getSession();
- if (!nextSession || !nextSession.channel) return;
- const selected = [];
- if (!allChecked) {
- options.forEach((opt) => selected.push(opt));
- }
- nextSession.options[optionGroup.id] = selected;
- setSession(nextSession);
- savePreference(nextSession);
- resetAndReload();
- updateActionLabel();
- };
-
- labelRow.appendChild(actionBtn);
- wrapper.appendChild(labelRow);
- wrapper.appendChild(list);
- container.appendChild(wrapper);
- return;
- }
-
- const select = document.createElement('select');
- options.forEach((opt) => {
- const option = document.createElement('option');
- option.value = opt.id;
- option.textContent = opt.title || opt.id;
- select.appendChild(option);
- });
-
- if (currentSelection && currentSelection.id) {
- select.value = currentSelection.id;
- }
-
- select.onchange = () => {
- const nextSession = getSession();
- if (!nextSession || !nextSession.channel) return;
- const selected = options.find((item) => item.id === select.value);
- if (selected) {
- nextSession.options[optionGroup.id] = selected;
- }
-
- setSession(nextSession);
- savePreference(nextSession);
- resetAndReload();
- };
-
- wrapper.appendChild(labelRow);
- wrapper.appendChild(select);
- container.appendChild(wrapper);
- });
-}
-
-function closeDrawers() {
- const menuDrawer = document.getElementById('drawer-menu');
- const settingsDrawer = document.getElementById('drawer-settings');
- const overlay = document.getElementById('overlay');
- const menuBtn = document.querySelector('.menu-toggle');
- const settingsBtn = document.querySelector('.settings-toggle');
-
- if (menuDrawer) menuDrawer.classList.remove('open');
- if (settingsDrawer) settingsDrawer.classList.remove('open');
- if (overlay) overlay.classList.remove('open');
- if (menuBtn) menuBtn.classList.remove('active');
- if (settingsBtn) settingsBtn.classList.remove('active');
- document.body.classList.remove('drawer-open');
-}
-
-function toggleDrawer(type) {
- const menuDrawer = document.getElementById('drawer-menu');
- const settingsDrawer = document.getElementById('drawer-settings');
- const overlay = document.getElementById('overlay');
- const menuBtn = document.querySelector('.menu-toggle');
- const settingsBtn = document.querySelector('.settings-toggle');
-
- const isMenu = type === 'menu';
- const targetDrawer = isMenu ? menuDrawer : settingsDrawer;
- const otherDrawer = isMenu ? settingsDrawer : menuDrawer;
- const targetBtn = isMenu ? menuBtn : settingsBtn;
- const otherBtn = isMenu ? settingsBtn : menuBtn;
-
- if (!targetDrawer || !overlay) return;
-
- const willOpen = !targetDrawer.classList.contains('open');
-
- if (otherDrawer) otherDrawer.classList.remove('open');
- if (otherBtn) otherBtn.classList.remove('active');
-
- if (willOpen) {
- targetDrawer.classList.add('open');
- if (targetBtn) targetBtn.classList.add('active');
- overlay.classList.add('open');
- document.body.classList.add('drawer-open');
- } else {
- closeDrawers();
- }
-}
-
-document.addEventListener('keydown', (event) => {
- if (event.key === 'Escape') closeDrawers();
-});
-
-initApp();
diff --git a/frontend/style.css b/frontend/css/style.css
similarity index 100%
rename from frontend/style.css
rename to frontend/css/style.css
diff --git a/frontend/index.html b/frontend/index.html
index 927ef78..5e617f7 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -8,7 +8,7 @@
-
+
@@ -118,7 +118,13 @@
-
+
+
+
+
+
+
+