// 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;
// 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');
}
// 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 : [];
items.forEach(v => {
if (renderedVideoIds.has(v.id)) return;
const card = document.createElement('div');
card.className = 'video-card';
const durationText = formatDuration(v.duration);
card.innerHTML = `
${durationText}
` : ''} `; 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 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; } // 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); } 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(); } 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(); 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'); 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 (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();