// 1. Global State and Constants (Declare these first!) let currentPage = 1; const perPage = 12; const renderedVideoIds = new Set(); let currentQuery = ""; // 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": {} }] })); } if (!localStorage.getItem('theme')) { localStorage.setItem('theme', 'dark'); } // 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 firstServerKey = Object.keys(config.servers[0])[0]; const serverData = config.servers[0][firstServerKey]; if (serverData.channels && serverData.channels.length > 0) { const channel = serverData.channels[0]; let options = {}; if (channel.options) { channel.options.forEach(element => { // Ensure the options structure matches your API expectations if (element.multiSelect) { options[element.id] = element.options.length > 0 ? [element.options[0]] : []; } else { options[element.id] = element.options[0]; } }); } const sessionData = { server: firstServerKey, channel: channel, options: options, }; localStorage.setItem('session', JSON.stringify(sessionData)); } } async function loadVideos() { const session = JSON.parse(localStorage.getItem('session')); if (!session) return; // Build the request body let body = { channel: session.channel.id, query: currentQuery, 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); } else if (value && value.id) { body[key] = value.id; } }); try { const response = await fetch('/api/videos', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) }); const videos = await response.json(); renderVideos(videos); currentPage++; } catch (err) { console.error("Failed to load videos:", err); } } function renderVideos(videos) { const grid = document.getElementById('video-grid'); if (!grid) return; videos.items.forEach(v => { if (renderedVideoIds.has(v.id)) return; const card = document.createElement('div'); card.className = 'video-card'; const durationText = v.duration === 0 ? '' : ` • ${v.duration}s`; card.innerHTML = ` ${v.title}

${v.title}

${v.channel}${durationText}

`; card.onclick = () => openPlayer(v.url); grid.appendChild(card); renderedVideoIds.add(v.id); }); } // 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(); const sentinel = document.getElementById('sentinel'); if (sentinel) { observer.observe(sentinel); } await loadVideos(); } 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 openPlayer(url) { const modal = document.getElementById('video-modal'); const video = document.getElementById('player'); video.src = `/api/stream?url=${encodeURIComponent(url)}`; modal.style.display = 'flex'; document.body.style.overflow = 'hidden'; } function closePlayer() { const modal = document.getElementById('video-modal'); const video = document.getElementById('player'); video.pause(); video.src = ''; modal.style.display = 'none'; document.body.style.overflow = 'auto'; } function handleSearch(value) { currentQuery = value || ""; currentPage = 1; renderedVideoIds.clear(); const grid = document.getElementById('video-grid'); if (grid) grid.innerHTML = ""; 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 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 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() { currentPage = 1; renderedVideoIds.clear(); const grid = document.getElementById('video-grid'); if (grid) grid.innerHTML = ""; loadVideos(); } 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'); 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 nextChannel = channels.length > 0 ? channels[0] : null; const nextSession = { server: selectedServerUrl, channel: nextChannel, options: nextChannel ? buildDefaultOptions(nextChannel) : {} }; setSession(nextSession); renderMenu(); resetAndReload(); }; const activeServer = serverEntries.find((entry) => entry.url === (session && session.server)); const availableChannels = activeServer && activeServer.data && activeServer.data.channels ? activeServer.data.channels : []; 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 nextSession = { server: session.server, channel: nextChannel, options: nextChannel ? buildDefaultOptions(nextChannel) : {} }; setSession(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(); }; } } 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 label = document.createElement('label'); label.textContent = optionGroup.title || optionGroup.id; const select = document.createElement('select'); select.multiple = Boolean(optionGroup.multiSelect); (optionGroup.options || []).forEach((opt) => { const option = document.createElement('option'); option.value = opt.id; option.textContent = opt.title || opt.id; select.appendChild(option); }); const currentSelection = session.options ? session.options[optionGroup.id] : null; if (Array.isArray(currentSelection)) { const ids = new Set(currentSelection.map((item) => item.id)); Array.from(select.options).forEach((opt) => { opt.selected = ids.has(opt.value); }); } else if (currentSelection && currentSelection.id) { select.value = currentSelection.id; } select.onchange = () => { const nextSession = getSession(); if (!nextSession || !nextSession.channel) return; const selectedOptions = optionGroup.options || []; if (optionGroup.multiSelect) { const selected = Array.from(select.selectedOptions).map((opt) => selectedOptions.find((item) => item.id === opt.value) ).filter(Boolean); nextSession.options[optionGroup.id] = selected; } else { const selected = selectedOptions.find((item) => item.id === select.value); if (selected) { nextSession.options[optionGroup.id] = selected; } } setSession(nextSession); resetAndReload(); }; wrapper.appendChild(label); 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();