diff --git a/.gitignore b/.gitignore index dfd98bf..9d4ca52 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ */__pycache__ -*/__pycache__/* \ No newline at end of file +*/__pycache__/* +.tmp diff --git a/frontend/app.js b/frontend/app.js index 1fe7485..931931e 100644 --- a/frontend/app.js +++ b/frontend/app.js @@ -2,6 +2,7 @@ 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) => { @@ -13,6 +14,9 @@ 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(); } @@ -49,7 +53,11 @@ async function InitializeServerStatus() { if (channel.options) { channel.options.forEach(element => { // Ensure the options structure matches your API expectations - options[element.id] = element.options[0]; + if (element.multiSelect) { + options[element.id] = element.options.length > 0 ? [element.options[0]] : []; + } else { + options[element.id] = element.options[0]; + } }); } @@ -70,7 +78,7 @@ async function loadVideos() { // Build the request body let body = { channel: session.channel.id, - query: "", + query: currentQuery, page: currentPage, perPage: perPage, server: session.server @@ -78,7 +86,11 @@ async function loadVideos() { // Correct way to loop through the options object Object.entries(session.options).forEach(([key, value]) => { - body[key] = value.id; + if (Array.isArray(value)) { + body[key] = value.map((entry) => entry.id); + } else if (value && value.id) { + body[key] = value.id; + } }); try { @@ -122,6 +134,8 @@ async function initApp() { // localStorage.clear(); await InitializeLocalStorage(); + applyTheme(); + renderMenu(); const sentinel = document.getElementById('sentinel'); if (sentinel) { @@ -131,6 +145,13 @@ async function initApp() { 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'); @@ -148,4 +169,250 @@ function closePlayer() { document.body.style.overflow = 'auto'; } -initApp(); \ No newline at end of file +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(); diff --git a/frontend/index.html b/frontend/index.html index d159e7c..ed40e49 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -29,11 +29,24 @@