window.App = window.App || {}; App.ui = App.ui || {}; (function() { const state = App.state; App.ui.applyTheme = function() { 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; }; // Toast helper for playback + network errors. App.ui.showError = function(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 (state.errorToastTimer) { clearTimeout(state.errorToastTimer); } state.errorToastTimer = setTimeout(() => { toast.classList.remove('show'); }, 4000); }; App.ui.showInfo = function(video) { const modal = document.getElementById('info-modal'); if (!modal) return; const title = document.getElementById('info-title'); const list = document.getElementById('info-list'); const empty = document.getElementById('info-empty'); const data = video && video.meta ? video.meta : video; const titleText = data && data.title ? data.title : 'Video Info'; if (title) title.textContent = titleText; if (list) { list.innerHTML = ""; } let hasRows = false; if (data && typeof data === 'object') { Object.entries(data).forEach(([key, value]) => { if (!list) return; const row = document.createElement('div'); row.className = 'info-row'; const label = document.createElement('span'); label.className = 'info-label'; label.textContent = key; let valueNode; if (value && typeof value === 'object') { valueNode = document.createElement('pre'); valueNode.className = 'info-json'; valueNode.textContent = JSON.stringify(value, null, 2); } else { valueNode = document.createElement('span'); valueNode.className = 'info-value'; valueNode.textContent = value === undefined || value === null || value === '' ? '—' : String(value); } row.appendChild(label); row.appendChild(valueNode); list.appendChild(row); hasRows = true; }); } if (empty) { empty.style.display = hasRows ? 'none' : 'block'; } modal.classList.add('open'); modal.setAttribute('aria-hidden', 'false'); }; App.ui.closeInfo = function() { const modal = document.getElementById('info-modal'); if (!modal) return; modal.classList.remove('open'); modal.setAttribute('aria-hidden', 'true'); }; // Drawer controls shared by the inline HTML handlers. App.ui.closeDrawers = function() { 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'); }; App.ui.toggleDrawer = function(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 { App.ui.closeDrawers(); } }; // Settings + menu rendering. App.ui.renderMenu = function() { const session = App.storage.getSession(); const serverEntries = App.storage.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 = App.storage.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 ? App.session.hydrateOptions(nextChannel, savedOptions) : App.session.buildDefaultOptions(nextChannel)) : {} }; App.storage.setSession(nextSession); App.session.savePreference(nextSession); App.ui.renderMenu(); App.videos.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 = App.storage.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 ? App.session.hydrateOptions(nextChannel, savedOptions) : App.session.buildDefaultOptions(nextChannel)) : {} }; App.storage.setSession(nextSession); App.session.savePreference(nextSession); App.ui.renderMenu(); App.videos.resetAndReload(); }; App.ui.renderFilters(filtersContainer, session); const themeSelect = document.getElementById('theme-select'); if (themeSelect) { themeSelect.onchange = () => { const nextTheme = themeSelect.value === 'light' ? 'light' : 'dark'; localStorage.setItem('theme', nextTheme); App.ui.applyTheme(); }; } if (favoritesToggle) { favoritesToggle.checked = App.favorites.isVisible(); favoritesToggle.onchange = () => { App.favorites.setVisible(favoritesToggle.checked); App.favorites.renderBar(); }; } 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 = App.storage.getConfig(); config.servers = (config.servers || []).filter((serverObj) => { const key = Object.keys(serverObj)[0]; return key !== entry.url; }); App.storage.setConfig(config); const prefs = App.storage.getPreferences(); if (prefs[entry.url]) { delete prefs[entry.url]; App.storage.setPreferences(prefs); } const remaining = App.storage.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 ? App.session.hydrateOptions(nextChannel, savedOptions) : App.session.buildDefaultOptions(nextChannel)) : {} }; App.storage.setSession(nextSession); App.session.savePreference(nextSession); } await App.storage.initializeServerStatus(); App.videos.resetAndReload(); App.ui.renderMenu(); }; 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 = App.storage.getConfig(); const exists = (config.servers || []).some((serverObj) => Object.keys(serverObj)[0] === normalized); if (!exists) { config.servers = config.servers || []; config.servers.push({ [normalized]: {} }); App.storage.setConfig(config); sourceInput.value = ''; await App.storage.initializeServerStatus(); const session = App.storage.getSession(); if (!session || session.server !== normalized) { const entries = App.storage.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 ? App.session.buildDefaultOptions(nextChannel) : {} }; App.storage.setSession(nextSession); App.session.savePreference(nextSession); } App.ui.renderMenu(); App.videos.resetAndReload(); } }; } if (reloadChannelBtn) { reloadChannelBtn.onclick = () => { App.videos.resetAndReload(); }; } }; App.ui.renderFilters = function(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 = App.storage.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; App.storage.setSession(nextSession); App.session.savePreference(nextSession); App.videos.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 = App.storage.getSession(); if (!nextSession || !nextSession.channel) return; const selected = []; if (!allChecked) { options.forEach((opt) => selected.push(opt)); } nextSession.options[optionGroup.id] = selected; App.storage.setSession(nextSession); App.session.savePreference(nextSession); App.videos.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 = App.storage.getSession(); if (!nextSession || !nextSession.channel) return; const selected = options.find((item) => item.id === select.value); if (selected) { nextSession.options[optionGroup.id] = selected; } App.storage.setSession(nextSession); App.session.savePreference(nextSession); App.videos.resetAndReload(); }; wrapper.appendChild(labelRow); wrapper.appendChild(select); container.appendChild(wrapper); }); }; // Expose inline handlers + keyboard shortcuts. App.ui.bindGlobalHandlers = function() { window.toggleDrawer = App.ui.toggleDrawer; window.closeDrawers = App.ui.closeDrawers; window.closePlayer = App.player.close; window.handleSearch = App.videos.handleSearch; document.addEventListener('keydown', (event) => { if (event.key === 'Escape') { App.ui.closeDrawers(); App.ui.closeInfo(); App.videos.closeAllMenus(); } }); document.addEventListener('click', () => { App.videos.closeAllMenus(); }); const infoModal = document.getElementById('info-modal'); if (infoModal) { infoModal.addEventListener('click', (event) => { if (event.target === infoModal) { App.ui.closeInfo(); } }); } const infoClose = document.getElementById('info-close'); if (infoClose) { infoClose.addEventListener('click', () => { App.ui.closeInfo(); }); } }; })();