// Normalize percent-encoded characters in the URL bar that are safe to show decoded. // Runs immediately so the address bar is clean before any other JS runs. (function () { try { const p = window.location.pathname; // Decode colon and space; leave %2F (/), %3F (?), %23 (#), %26 (&) encoded. const clean = p.replace(/%3A/gi, ':').replace(/%20/gi, ' '); if (clean !== p) { history.replaceState(null, '', clean + window.location.search + window.location.hash); } } catch (_) {} })(); window.requestAnimFrame = (function () { return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || function (callback) { window.setTimeout(callback, 1000 / 60); }; })(); window.cancelAnimFrame = (function () { return window.cancelAnimationFrame || window.webkitCancelAnimationFrame || window.mozCancelAnimationFrame || function (id) { window.clearTimeout(id); }; })(); (() => { var i18n = window.f0ckI18n || {}; window.escHTML = (str) => { if (!str) return ''; const div = document.createElement('div'); div.textContent = str; return div.innerHTML; }; window.getCurrentItemId = () => { const path = window.location.pathname; // Explicitly ignore admin/mod/settings paths to avoid false positives from user IDs, etc. if (path.includes('/admin/') || path.includes('/mod/') || path.includes('/settings') || path.includes('/user/')) return null; const match = path.match(/\/(\d+)\/?$/); return match ? match[1] : null; }; // OS and Browser detection for CSS targeting const ua = navigator.userAgent; const htmlEl = document.documentElement; if (ua.includes('Linux')) htmlEl.classList.add('is-linux'); if (ua.includes('Windows')) htmlEl.classList.add('is-windows'); if (ua.includes('Firefox')) htmlEl.classList.add('is-firefox'); if (ua.includes('Chrome')) htmlEl.classList.add('is-chrome'); if (ua.includes('Safari') && !ua.includes('Chrome')) htmlEl.classList.add('is-safari'); if (localStorage.getItem('blurNsfw') === 'true') htmlEl.classList.add('blur-nsfw-active'); if (localStorage.getItem('blurNsfl') === 'true') htmlEl.classList.add('blur-nsfl-active'); if (localStorage.getItem('blurSfw') === 'true') htmlEl.classList.add('blur-sfw-active'); if (localStorage.getItem('blurUntagged') === 'true') htmlEl.classList.add('blur-untagged-active'); if (localStorage.getItem('blurDetail') !== 'false') htmlEl.classList.add('blur-detail-active'); window.updateVisitIndicators = () => { try { // View indicators and counters have been permanently removed as requested. // This function is now a no-op to prevent injection into items. } catch (e) { console.error('Visit tracking error:', e); } }; window.trackVisit = (id) => { try { const visits = JSON.parse(localStorage.getItem('visited_videos') || '{}'); visits[id] = (visits[id] || 0) + 1; localStorage.setItem('visited_videos', JSON.stringify(visits)); // Delay update slightly to ensure DOM is ready? No, update immediately is fine. updateVisitIndicators(); } catch(e) { console.error('Visit tracking error:', e); } }; window.applyThumbCacheBust = (bgUrlStr) => { if (!bgUrlStr) return bgUrlStr; try { const bustedStr = localStorage.getItem('bustedThumbs'); if (!bustedStr) return bgUrlStr; const busted = JSON.parse(bustedStr); const match = bgUrlStr.match(/\/t\/(\d+)(?:_blur)?\.webp/); if (match) { const id = match[1]; if (busted[id]) { const url = new URL(bgUrlStr, window.location.origin); url.searchParams.set('t', busted[id]); return url.pathname + url.search; } } } catch(e) {} return bgUrlStr; }; /** * Forcefully refreshes all thumbnail occurrences for a specific item in the DOM. * Handles grid items (data-bg), images (src), and the background canvas. */ window.refreshItemThumbnails = (itemId, timestamp = Date.now()) => { if (!itemId) return; const idStr = String(itemId); // Update localStorage so future navigations use the new timestamp try { const bustedStr = localStorage.getItem('bustedThumbs'); const busted = bustedStr ? JSON.parse(bustedStr) : {}; busted[idStr] = timestamp; const keys = Object.keys(busted); if (keys.length > 50) delete busted[keys[0]]; localStorage.setItem('bustedThumbs', JSON.stringify(busted)); } catch(e) {} // Clear grid cache to force fresh render on next navigation if (typeof gridCacheMap !== 'undefined') gridCacheMap.clear(); // Update elements with data-bg (grid items). // We look for any data-bg or inline style containing the thumbnail path for this ID. document.querySelectorAll(`[data-bg*="/t/${idStr}.webp"], [data-bg*="/t/${idStr}_blur.webp"], [style*="/t/${idStr}.webp"], [style*="/t/${idStr}_blur.webp"]`).forEach(el => { // If it has data-bg, update it (this handles lazy-thumb logic) if (el.dataset.bg) { el.dataset.bg = window.applyThumbCacheBust(el.dataset.bg); } // If it's already showing the background, update the style directly if (el.style.backgroundImage || el.getAttribute('style')?.includes('background-image')) { const currentStyle = el.getAttribute('style') || ''; // Match url(...) contents const newStyle = currentStyle.replace(/url\(['"]?([^'"]+)['"]?\)/g, (match, p1) => { if (p1.includes(`/t/${idStr}.webp`) || p1.includes(`/t/${idStr}_blur.webp`)) { return `url('${window.applyThumbCacheBust(p1)}')`; } return match; }); el.setAttribute('style', newStyle); } }); // Update actual img tags document.querySelectorAll(`img[src*="/t/${idStr}.webp"], img[src*="/t/${idStr}_blur.webp"]`).forEach(el => { try { const url = new URL(el.src, window.location.origin); url.searchParams.set('t', timestamp); el.src = url.pathname + url.search; } catch(e) {} }); // Refresh background canvas if it matches the current item const currentId = window.getCurrentItemId(); if (currentId === idStr && window.initBackground) { window.initBackground(); } }; let lazyObserver; window.initLazyLoading = () => { if (!('IntersectionObserver' in window)) { document.querySelectorAll('.lazy-thumb').forEach(thumb => { let bg = thumb.dataset.bg; if (bg) { const mode = thumb.getAttribute('data-mode'); const blurNsfw = localStorage.getItem('blurNsfw') === 'true'; const blurNsfl = localStorage.getItem('blurNsfl') === 'true'; const blurSfw = localStorage.getItem('blurSfw') === 'true'; const blurUntagged = localStorage.getItem('blurUntagged') === 'true'; let shouldBlurThis = false; if (mode === 'nsfw') shouldBlurThis = blurNsfw; else if (mode === 'nsfl') shouldBlurThis = blurNsfl; else if (mode === 'sfw') shouldBlurThis = blurSfw; else if (mode === 'null' || !mode) shouldBlurThis = blurUntagged; if (shouldBlurThis && !thumb.classList.contains('revealed')) { bg = bg.replace('.webp', '_blur.webp'); } const finalBg = window.applyThumbCacheBust(bg); thumb.style.backgroundImage = `url('${finalBg}')`; thumb.classList.remove('lazy-thumb'); } }); return; } if (!lazyObserver) { lazyObserver = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const thumb = entry.target; let bg = thumb.dataset.bg; if (bg && !thumb.classList.contains('loaded')) { const mode = thumb.getAttribute('data-mode'); const blurNsfw = localStorage.getItem('blurNsfw') === 'true'; const blurNsfl = localStorage.getItem('blurNsfl') === 'true'; const blurSfw = localStorage.getItem('blurSfw') === 'true'; const blurUntagged = localStorage.getItem('blurUntagged') === 'true'; let shouldBlurThis = false; if (mode === 'nsfw') shouldBlurThis = blurNsfw; else if (mode === 'nsfl') shouldBlurThis = blurNsfl; else if (mode === 'sfw') shouldBlurThis = blurSfw; else if (mode === 'null' || !mode) shouldBlurThis = blurUntagged; if (shouldBlurThis && !thumb.classList.contains('revealed')) { bg = bg.replace('.webp', '_blur.webp'); } bg = window.applyThumbCacheBust(bg); const img = new Image(); img.onload = () => { thumb.style.backgroundImage = `url('${bg}')`; thumb.classList.add('loaded'); thumb.classList.remove('lazy-thumb'); }; img.onerror = () => { const retries = parseInt(thumb.dataset.retries || '0'); if (retries < 3) { thumb.dataset.retries = retries + 1; setTimeout(() => { img.src = bg + '?r=' + Date.now(); }, 1000); } else { // All retries exhausted — show fallback for audio items const mime = thumb.dataset.mime || ''; if (mime.startsWith('audio/')) { thumb.style.backgroundImage = `url('/s/img/audio.webp')`; thumb.classList.add('thumb-fallback'); } thumb.classList.remove('lazy-thumb'); } }; img.src = bg; } lazyObserver.unobserve(thumb); } }); }, { rootMargin: '300px 0px', threshold: 0.01 }); } // Nudge lazy loading on tab switch to prevent stuck skeletons in inactive tabs if (!window._lazyVisibilityBound) { window._lazyVisibilityBound = true; document.addEventListener('visibilitychange', () => { if (!document.hidden && typeof window.initLazyLoading === 'function') { // Clear observation state for pending items to force re-observation document.querySelectorAll('.lazy-thumb:not(.loaded)').forEach(t => { delete t.dataset.lazyObserved; }); window.initLazyLoading(); } }); } document.querySelectorAll('.lazy-thumb').forEach(thumb => { if (!thumb.dataset.lazyObserved) { thumb.dataset.lazyObserved = 'true'; lazyObserver.observe(thumb); } }); }; window.showMediaOverlay = (show = true) => { const overlay = document.querySelector('.v0ck_overlay'); if (overlay) overlay.classList[show ? 'remove' : 'add']('v0ck_hidden'); }; window.flashMessage = (text, duration = 2000, type = 'info') => { // Ensure the stacking container exists let container = document.getElementById('f0ck-flash-container'); if (!container) { container = document.createElement('div'); container.id = 'f0ck-flash-container'; container.style.cssText = 'position:fixed;bottom:20px;left:20px;z-index:9999;display:flex;flex-direction:column-reverse;gap:8px;pointer-events:none;'; document.body.appendChild(container); } const flash = document.createElement('div'); flash.textContent = text; let baseStyle = 'color:#fff;padding:10px 18px;border-radius:6px;font-size:13px;opacity:0;transition:opacity 0.3s,transform 0.3s;box-shadow:0 4px 12px rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.1);transform:translateY(6px);'; if (type === 'error') { baseStyle += 'background:rgba(200,30,30,0.95);'; } else if (type === 'success') { baseStyle += 'background:rgba(30,130,60,0.95);'; } else { baseStyle += 'background:rgba(30,30,30,0.95);'; } flash.style.cssText = baseStyle; container.appendChild(flash); requestAnimationFrame(() => { flash.style.opacity = '1'; flash.style.transform = 'translateY(0)'; }); setTimeout(() => { flash.style.opacity = '0'; flash.style.transform = 'translateY(6px)'; setTimeout(() => flash.remove(), 300); }, duration); }; let video; let isNavigating = false; const main = document.getElementById('main'); let posts = document.querySelector('.posts'); const navbar = document.querySelector("nav.navbar"); const gridCacheMap = new Map(); // Cache for detached grid nodes (URL -> {node, scroll}) window.activeMode = 0; // Default let audioCtx = null; let visualizerRafId = null; let audioSource = null; const updateMimeLabel = () => { let mimeStr = null; const cookieMime = document.cookie.split('; ').find(row => row.startsWith('mime=')); if (cookieMime) { mimeStr = cookieMime.split('=')[1]; } const selected = mimeStr ? mimeStr.split(',').filter(m => ['video', 'audio', 'image', 'flash'].includes(m)) : []; document.querySelectorAll('.nav-mime-btn').forEach(btn => { let label = 'ALL'; if (selected.length > 0) { label = selected.map(s => s.charAt(0).toUpperCase()).sort().join(','); } btn.innerHTML = `${label} ▾`; }); document.querySelectorAll('.nav-mime-menu').forEach(menu => { menu.querySelectorAll('input[type="checkbox"]').forEach(cb => { cb.checked = selected.includes(cb.value); }); }); if (window.updateFilterBadge) window.updateFilterBadge(); }; window.updateMimeLabel = updateMimeLabel; const updateFilterBadge = () => { const badge = document.getElementById('nav-filter-badge'); if (!badge) return; let activeMode = 0; if (window.activeMode !== undefined) { activeMode = window.activeMode; } else { const cookieMode = document.cookie.split('; ').find(row => row.startsWith('mode=')); if (cookieMode) { activeMode = +cookieMode.split('=')[1]; } else if (window.f0ckSession && window.f0ckSession.mode !== undefined) { activeMode = window.f0ckSession.mode; } } // Check multi-rating cookie const ratingsRaw = document.cookie.split('; ').find(row => row.startsWith('ratings=')); const activeRatings = window.getRatingsCookie ? window.getRatingsCookie() : (() => { if (!ratingsRaw) return []; const val = ratingsRaw.split('=').slice(1).join('='); const decoded = decodeURIComponent(val); const parts = decoded.includes('|') ? decoded.split('|') : decoded.split(','); return parts.filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)); })(); let hasMimeFilter = false; let mimeStr = ''; const cookieMime = document.cookie.split('; ').find(row => row.startsWith('mime=')); if (cookieMime) { mimeStr = cookieMime.split('=')[1] || ''; } const selectedMimes = mimeStr ? mimeStr.split(',').filter(m => ['video', 'audio', 'image', 'flash'].includes(m)) : []; if (selectedMimes.length > 0) { hasMimeFilter = true; } let badgeText = ''; let badgeClass = 'filter-badge'; if (activeRatings.length > 0) { // If every available rating is selected, treat as ALL const nsflEnabled = !!(window.f0ckSession?.enable_nsfl ?? true); // default true if unknown const allRatings = nsflEnabled ? ['sfw', 'nsfw', 'nsfl', 'untagged'] : ['sfw', 'nsfw', 'untagged']; const isAll = allRatings.every(r => activeRatings.includes(r)); if (isAll) { badgeText = 'ALL'; badgeClass += ' filter-badge-all'; } else if (activeRatings.length === 1) { // Single rating — keep existing single-color badge style const single = activeRatings[0]; const abbr = { sfw: 'SFW', nsfw: 'NSFW', nsfl: 'NSFL', untagged: 'UNT' }; badgeText = abbr[single] || single.toUpperCase(); if (single === 'nsfw') badgeClass += ' filter-badge-nsfw'; else if (single === 'nsfl') badgeClass += ' filter-badge-nsfl'; else if (single === 'sfw') badgeClass += ' filter-badge-sfw'; else badgeClass += ' filter-badge-unt'; } else { // Multi-rating: render individual colored letter chips, neutral container const letterMap = { sfw: 'S', nsfw: 'N', nsfl: 'N', untagged: 'U' }; const colorMap = { sfw: 'filter-letter-sfw', nsfw: 'filter-letter-nsfw', nsfl: 'filter-letter-nsfl', untagged: 'filter-letter-unt' }; badgeText = activeRatings.map(r => `${letterMap[r] || r[0].toUpperCase()}` ).join(''); badgeClass += ' filter-badge-multi'; } } else { switch (activeMode) { case 0: badgeText = 'SFW'; badgeClass += ' filter-badge-sfw'; break; case 1: badgeText = 'NSFW'; badgeClass += ' filter-badge-nsfw'; break; case 4: badgeText = 'NSFL'; badgeClass += ' filter-badge-nsfl'; break; case 2: badgeText = 'UNT'; badgeClass += ' filter-badge-unt'; break; case 3: badgeText = 'ALL'; badgeClass += ' filter-badge-all'; break; default: badgeText = 'SFW'; badgeClass += ' filter-badge-sfw'; } } const isRandom = document.cookie.includes('random_mode=1'); let zomgHtml = ''; if (isRandom) { zomgHtml = ' Z'; } badge.className = badgeClass; // Always wrap rating text in a row container so mime icons can sit on the next row cleanly badge.innerHTML = `${badgeText}${zomgHtml}`; if (hasMimeFilter) { const iconsContainer = document.createElement('span'); iconsContainer.className = 'filter-mime-icons'; selectedMimes.forEach(mime => { const icon = document.createElement('i'); let iconClass = 'mime-icon '; if (mime === 'audio') iconClass += 'fa-solid fa-music mime-icon-audio'; else if (mime === 'image') iconClass += 'fa-solid fa-image mime-icon-image'; else if (mime === 'video') iconClass += 'fa-solid fa-film mime-icon-video'; else if (mime === 'flash') iconClass += 'fa-solid fa-bolt mime-icon-flash'; icon.className = iconClass; icon.title = mime.charAt(0).toUpperCase() + mime.slice(1); iconsContainer.appendChild(icon); }); badge.appendChild(iconsContainer); } badge.style.display = 'inline-flex'; }; window.updateFilterBadge = updateFilterBadge; document.addEventListener('f0ck:modeChanged', () => { updateFilterBadge(); }); window.randomizeLogo = () => { const logoArr = window.f0ckBrandImages; if (!logoArr || !logoArr.length) return; const img = document.getElementById('navbar-logo'); if (!img) return; // Avoid picking the same image if there's more than one let randomImg; do { randomImg = logoArr[Math.floor(Math.random() * logoArr.length)]; } while (logoArr.length > 1 && randomImg === img.getAttribute('src')); img.src = randomImg; }; // Initialize active mode: prefer mode cookie, then session, then legacy mode-btn const _modeCookieRaw = document.cookie.split('; ').find(r => r.startsWith('mode=')); if (_modeCookieRaw) { window.activeMode = +_modeCookieRaw.split('=')[1]; } else if (window.f0ckSession && window.f0ckSession.mode !== undefined) { window.activeMode = +window.f0ckSession.mode; } else { // Legacy fallback: read from old .mode-btn.active if present const activeModeBtn = document.querySelector('.mode-btn.active'); if (activeModeBtn && activeModeBtn.href) { const modeMatch = activeModeBtn.href.match(/\/mode\/(\d)/); if (modeMatch) window.activeMode = +modeMatch[1]; } } // ---- Multi-select Rating Toggles ---- // Reads/writes a `ratings` cookie (e.g. "sfw|untagged") and syncs with server via /mode/3 (ALL). const getRatingsCookie = () => { const raw = document.cookie.split('; ').find(r => r.startsWith('ratings=')); if (!raw) return []; const val = raw.split('=').slice(1).join('='); // handle any = in value // Support both | separator (new) and , (legacy/encoded) const decoded = decodeURIComponent(val); const parts = decoded.includes('|') ? decoded.split('|') : decoded.split(','); return parts.filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)); }; window.getRatingsCookie = getRatingsCookie; const setRatingsCookie = (ratings) => { const val = ratings.join('|'); // Use | separator — no encoding needed, no ambiguity document.cookie = `ratings=${val}; Path=/; Max-Age=31536000; SameSite=Lax`; }; const clearRatingsCookie = () => { document.cookie = 'ratings=; Path=/; Max-Age=0'; }; const syncRatingButtonUI = () => { let activeRatings = getRatingsCookie(); if (activeRatings.length === 0) { if (window.activeMode === 0) activeRatings = ['sfw']; else if (window.activeMode === 1) activeRatings = ['nsfw']; else if (window.activeMode === 4) activeRatings = ['nsfl']; else if (window.activeMode === 2) activeRatings = ['untagged']; } const selector = document.getElementById('rating-selector'); if (!selector) return; selector.querySelectorAll('.rating-toggle-btn').forEach(btn => { const r = btn.dataset.rating; if (!r) return; // ALL button handled separately btn.classList.toggle('active', activeRatings.includes(r)); }); // ALL button: active when ratings cookie is empty/absent (server mode is the authority) const allBtn = document.getElementById('rating-btn-all'); if (allBtn) { allBtn.classList.toggle('active', (getRatingsCookie().length === 0 && window.activeMode === 3) || activeRatings.length === 0); } }; // Wire up rating toggle buttons document.addEventListener('click', (e) => { const btn = e.target.closest('.rating-toggle-btn'); if (!btn) return; e.preventDefault(); e.stopPropagation(); const isAllBtn = btn.classList.contains('rating-toggle-all'); const fromFilterModal = !!btn.closest('#excluded-tags-overlay'); if (isAllBtn) { // ALL: clear ratings cookie, set mode=3 on server // Must set activeMode BEFORE syncRatingButtonUI so the ALL button // active-state check (activeRatings.length === 0 && window.activeMode === 3) passes. clearRatingsCookie(); window.activeMode = 3; document.cookie = `mode=3; Path=/; Max-Age=31536000`; syncRatingButtonUI(); if (fromFilterModal) window._keepFilterModal = true; document.dispatchEvent(new CustomEvent('f0ck:modeChanged', { detail: { mode: 3 } })); fetch('/mode/3', { headers: { 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'include' }) .then(r => r.json()) .then(data => { if (data.success) { window.flashMessage('ALL MODE ACTIVATED'); gridCacheMap.clear(); const isGridView = document.querySelector('.posts, .tags-grid'); let reloadPromise = null; if (isGridView) { const currentUrl = new URL(window.location.href); currentUrl.searchParams.delete('mode'); reloadPromise = loadPageAjax(currentUrl.toString(), true, { skipCache: true }); } if (fromFilterModal) Promise.resolve(reloadPromise).finally(() => { window._keepFilterModal = false; }); } }) .catch(() => { if (fromFilterModal) window._keepFilterModal = false; }); return; } const rating = btn.dataset.rating; if (!rating) return; // Toggle rating in cookie const activeRatings = getRatingsCookie(); const idx = activeRatings.indexOf(rating); if (idx === -1) { activeRatings.push(rating); } else { activeRatings.splice(idx, 1); } if (activeRatings.length === 0) { // Nothing selected: treat as ALL (no filter = show everything) clearRatingsCookie(); window.activeMode = 3; document.cookie = `mode=3; Path=/; Max-Age=31536000`; } else { setRatingsCookie(activeRatings); // Use mode=3 (ALL) on server when multi-select; single-select maps to native mode const singleModeMap = { sfw: 0, nsfw: 1, nsfl: 4, untagged: 2 }; const serverMode = activeRatings.length === 1 ? (singleModeMap[activeRatings[0]] ?? 3) : 3; window.activeMode = serverMode; document.cookie = `mode=${serverMode}; Path=/; Max-Age=31536000`; } syncRatingButtonUI(); document.dispatchEvent(new CustomEvent('f0ck:modeChanged', { detail: { mode: window.activeMode } })); if (fromFilterModal) window._keepFilterModal = true; // Sync server mode and reload content fetch(`/mode/${window.activeMode}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'include' }) .then(r => r.json()) .then(data => { if (data.success) { const label = activeRatings.length > 0 ? activeRatings.map(r => r.toUpperCase()).join('+') + ' ACTIVE' : 'ALL MODE ACTIVATED'; window.flashMessage(label); gridCacheMap.clear(); const isGridView = document.querySelector('.posts, .tags-grid'); const isItemView = document.getElementById('prev') || document.getElementById('next'); let reloadPromise = null; if (isGridView) { const currentUrl = new URL(window.location.href); currentUrl.searchParams.delete('mode'); reloadPromise = loadPageAjax(currentUrl.toString(), true, { skipCache: true }); } else if (isItemView) { updateNavForMode(window.activeMode); } if (fromFilterModal) Promise.resolve(reloadPromise).finally(() => { window._keepFilterModal = false; }); } else { if (fromFilterModal) window._keepFilterModal = false; } }) .catch(() => { if (fromFilterModal) window._keepFilterModal = false; }); }); // Initialize rating toggle UI on page load window.syncRatingButtonUI = syncRatingButtonUI; // Migrate old URL-encoded ratings cookie to new pipe-separated format (function migrateRatingsCookie() { const raw = document.cookie.split('; ').find(r => r.startsWith('ratings=')); if (!raw) return; const val = raw.split('=').slice(1).join('='); if (val.includes('%')) { // Cookie is URL-encoded — rewrite it with new format const decoded = decodeURIComponent(val); const cleaned = decoded.replace(/,/g, '|'); document.cookie = `ratings=${cleaned}; Path=/; Max-Age=31536000; SameSite=Lax`; } })(); syncRatingButtonUI(); // Cleanup strict param from URL bar on initial load if present (legacy or external link) if (window.location.search.includes('strict=1')) { const cleanUrl = window.location.pathname + window.location.search.replace(/[?&]strict=1/, '').replace(/[?&]$/, '') + window.location.hash; history.replaceState({}, '', cleanUrl); } // User & Visitor dropdown toggle const userToggle = document.getElementById('nav-user-toggle'); const userMenu = document.getElementById('nav-user-menu'); const visitorToggle = document.getElementById('nav-visitor-toggle'); const visitorMenu = document.getElementById('nav-visitor-menu'); const hallsToggle = document.getElementById('nav-halls-toggle'); const hallsMenu = document.getElementById('nav-halls-menu'); const vHallsToggle = document.getElementById('nav-visitor-halls-toggle'); const vHallsMenu = document.getElementById('nav-visitor-halls-menu'); if (userToggle && userMenu) { userToggle.addEventListener('click', (e) => { e.stopPropagation(); const opening = !userMenu.classList.contains('show'); userMenu.classList.toggle('show'); userToggle.classList.toggle('is-active', opening); }); userMenu.querySelectorAll('a').forEach(link => { link.addEventListener('click', () => { userMenu.classList.remove('show'); userMenu.classList.remove('show-mobile'); userToggle.classList.remove('is-active'); }); }); } if (visitorToggle && visitorMenu) { visitorToggle.addEventListener('click', (e) => { e.stopPropagation(); visitorMenu.classList.toggle('show'); }); visitorMenu.querySelectorAll('a').forEach(link => { link.addEventListener('click', () => { visitorMenu.classList.remove('show'); visitorMenu.classList.remove('show-mobile'); }); }); } const setupHallsToggle = (toggle, menu) => { if (toggle && menu) { toggle.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); menu.classList.toggle('show'); }); menu.querySelectorAll('a').forEach(link => { link.addEventListener('click', () => menu.classList.remove('show')); }); } }; setupHallsToggle(hallsToggle, hallsMenu); setupHallsToggle(vHallsToggle, vHallsMenu); document.addEventListener('click', (e) => { if (userMenu && !userMenu.contains(e.target) && userToggle && !userToggle.contains(e.target)) { userMenu.classList.remove('show'); userToggle.classList.remove('is-active'); } if (visitorMenu && !visitorMenu.contains(e.target) && visitorToggle && !visitorToggle.contains(e.target)) { visitorMenu.classList.remove('show'); } if (hallsMenu && !hallsMenu.contains(e.target) && hallsToggle && !hallsToggle.contains(e.target)) { hallsMenu.classList.remove('show'); } if (vHallsMenu && !vHallsMenu.contains(e.target) && vHallsToggle && !vHallsToggle.contains(e.target)) { vHallsMenu.classList.remove('show'); } }); // Randomize logo on click document.addEventListener('click', (e) => { if (e.target.closest('.navbar-brand') || e.target.id === 'navbar-logo') { window.randomizeLogo(); } }); // Modal Logic (Login, Forgot, Reset, Register) const loginBtn = document.getElementById('nav-login-btn'); const loginModal = document.getElementById('login-modal'); const loginClose = document.getElementById('login-modal-close'); const registerBtn = document.getElementById('nav-register-btn'); const registerModal = document.getElementById('register-modal'); const registerClose = document.getElementById('register-modal-close'); const switchModalView = (view) => { if (!loginModal) return; const views = ['login', 'forgot', 'reset']; views.forEach(v => { const el = document.getElementById(`modal-${v}-view`); if (el) el.style.display = (v === view) ? 'block' : 'none'; }); }; const openModal = (modal, view = 'login') => { if (!modal) return; if (modal === loginModal) switchModalView(view); modal.style.display = 'flex'; document.body.classList.add('modal-open'); if (visitorMenu) visitorMenu.classList.remove('show'); }; const closeModal = (modal) => { if (modal) { modal.style.display = 'none'; document.body.classList.remove('modal-open'); } }; /** * Surgical cleanup of scroll-lock state and modal visibility. * Used during AJAX navigation to ensure the UI remains interactive. */ window.resetGlobalScrollState = () => { document.body.classList.remove('modal-open'); document.documentElement.classList.remove('modal-open'); document.body.style.overflow = ''; document.body.style.height = ''; document.documentElement.style.overflow = ''; document.documentElement.style.height = ''; const pw = document.querySelector('.pagewrapper'); if (pw) { pw.style.overflow = ''; pw.style.height = ''; } }; window.hideAllModals = () => { const modalIds = [ 'login-modal', 'register-modal', 'forgot-modal', 'reset-modal', 'report-modal', 'halls-modal', 'metadata-modal', 'warning-modal', 'shortcuts-modal', 'upload-drag-modal', 'excluded-tags-overlay', 'content-warning-modal', 'gchat-img-modal', 'image-modal', 'info-modal' ]; modalIds.forEach(id => { // Don't close the filter modal during a background mime-filter reload if (id === 'excluded-tags-overlay' && window._keepFilterModal) return; const el = document.getElementById(id); if (el) { el.classList.remove('show', 'visible'); // If the modal uses CSS classes for visibility, we must clear the inline display // to allow those classes to work later. For others, we force display: none. if (['upload-drag-modal', 'image-modal', 'gchat-img-modal', 'excluded-tags-overlay'].includes(id)) { el.style.display = ''; } else { el.style.display = 'none'; } } }); // Also handle class-based modals if any document.querySelectorAll('.modal-overlay, .modal-backdrop').forEach(el => { el.classList.remove('show', 'visible'); // Do NOT set display: none here as it might override CSS-based visibility // for modals that use the classes we just removed. }); }; if (loginModal) { if (loginBtn) { loginBtn.addEventListener('click', (e) => { e.preventDefault(); openModal(loginModal, 'login'); }); } if (loginClose) loginClose.addEventListener('click', () => closeModal(loginModal)); loginModal.addEventListener('click', (e) => { if (e.target === loginModal) closeModal(loginModal); }); // Forgot Password link const modalForgotBtn = document.getElementById('modal-forgot-btn'); if (modalForgotBtn) { modalForgotBtn.addEventListener('click', (e) => { e.preventDefault(); switchModalView('forgot'); }); } const forgotToLogin = document.getElementById('forgot-to-login'); if (forgotToLogin) { forgotToLogin.addEventListener('click', (e) => { e.preventDefault(); switchModalView('login'); }); } // Check for reset token or login flag in URL const urlParams = new URLSearchParams(window.location.search); const resetToken = urlParams.get('token'); if (resetToken) { const tokenInput = document.getElementById('reset-token'); if (tokenInput) { tokenInput.value = resetToken; openModal(loginModal, 'reset'); // Clean URL const cleanUrl = window.location.pathname + window.location.search.replace(/[?&]token=[^&]+/, '').replace(/[?&]$/, '') + window.location.hash; window.history.replaceState({}, '', cleanUrl); } } else if (urlParams.get('login') === '1') { openModal(loginModal, 'login'); // Clean URL const cleanUrl = window.location.pathname + window.location.search.replace(/[?&]login=1/, '').replace(/[?&]$/, '') + window.location.hash; window.history.replaceState({}, '', cleanUrl); } else if (urlParams.get('already_logged_in') === '1') { // Clean URL first, then show flash (deferred so window.showFlash is defined) const cleanUrl = window.location.pathname + window.location.search.replace(/[?&]already_logged_in=1/, '').replace(/[?&]$/, '') + window.location.hash; window.history.replaceState({}, '', cleanUrl); setTimeout(() => { window.showFlash(i18n.already_logged_in || 'Already logged in lol', 'error'); }, 0); } const loginForm = loginModal.querySelector('.login-form'); if (loginForm && loginForm.id !== 'forgot-password-form' && loginForm.id !== 'reset-password-form') { loginForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(loginForm); const params = new URLSearchParams(formData); if (!formData.get('password')) { let errDiv = loginForm.querySelector('.flash-error'); if (!errDiv) { errDiv = document.createElement('div'); errDiv.className = 'flash-error'; loginForm.insertBefore(errDiv, loginForm.firstChild); } errDiv.textContent = 'Invalid username or password.'; return; } try { const res = await fetch('/login', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, body: params }); if (res.redirected) { window.location.href = res.url; return; } const json = await res.json(); if (json && json.success === false) { let errDiv = loginForm.querySelector('.flash-error'); if (!errDiv) { errDiv = document.createElement('div'); errDiv.className = 'flash-error'; loginForm.insertBefore(errDiv, loginForm.firstChild); } errDiv.textContent = json.msg; } } catch (err) { console.error('Login error:', err); } }); } // Forgot Password Submit const forgotForm = document.getElementById('forgot-password-form'); if (forgotForm) { forgotForm.addEventListener('submit', async (e) => { e.preventDefault(); const email = document.getElementById('forgot-email').value; const status = document.getElementById('forgot-status'); const btn = forgotForm.querySelector('button'); btn.disabled = true; btn.textContent = i18n.sending || 'Sending...'; status.textContent = ''; status.className = ''; try { const res = await fetch('/forgot-password', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' }, body: new URLSearchParams({ email }) }); const data = await res.json(); if (data.success) { status.textContent = data.msg || 'Success! Check your email.'; status.className = 'flash-success'; forgotForm.reset(); } else { status.textContent = data.msg || 'Error sending link.'; status.className = 'flash-error'; } } catch (err) { status.textContent = 'Network error.'; status.className = 'flash-error'; } finally { btn.disabled = false; btn.textContent = 'Send Reset Link'; } }); } // Reset Password Submit const resetForm = document.getElementById('reset-password-form'); if (resetForm) { const resetToLogin = document.getElementById('reset-to-login'); resetForm.addEventListener('submit', async (e) => { e.preventDefault(); const token = document.getElementById('reset-token').value; const password = document.getElementById('reset-password').value; const password_confirm = document.getElementById('reset-password-confirm').value; const status = document.getElementById('reset-status'); const btn = resetForm.querySelector('button'); if (password !== password_confirm) { status.className = 'flash-error'; return; } if (password.length < 20) { status.textContent = 'Password is too short (minimum 20 characters).'; status.className = 'flash-error'; return; } btn.disabled = true; btn.textContent = i18n.updating || 'Updating...'; status.textContent = ''; status.className = ''; try { const res = await fetch('/reset-password', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Accept': 'application/json' }, body: new URLSearchParams({ token, password, password_confirm }) }); const data = await res.json(); if (data.success) { status.textContent = data.msg || 'Password updated successfully!'; status.className = 'flash-success'; resetForm.reset(); btn.style.display = 'none'; if (resetToLogin) resetToLogin.style.display = 'inline-block'; } else { status.textContent = data.msg || 'Error resetting password.'; status.className = 'flash-error'; } } catch (err) { status.textContent = 'Network error.'; status.className = 'flash-error'; } finally { btn.disabled = false; btn.textContent = 'Update Password'; } }); if (resetToLogin) { resetToLogin.addEventListener('click', (e) => { e.preventDefault(); switchModalView('login'); resetToLogin.style.display = 'none'; resetForm.querySelector('button').style.display = 'inline-block'; }); } } } if (registerBtn && registerModal) { registerBtn.addEventListener('click', (e) => { e.preventDefault(); openModal(registerModal); }); if (registerClose) registerClose.addEventListener('click', () => closeModal(registerModal)); registerModal.addEventListener('click', (e) => { if (e.target === registerModal) closeModal(registerModal); }); // Register Form AJAX const registerForm = document.getElementById('modal-register-form'); if (registerForm) { registerForm.addEventListener('submit', async (e) => { e.preventDefault(); const formData = new FormData(registerForm); const params = new URLSearchParams(formData); const status = document.getElementById('register-status'); const btn = registerForm.querySelector('button'); const password = formData.get('password'); const password_confirm = formData.get('password_confirm'); if (password && password.length < 20) { if (status) { status.textContent = 'Password is too short (minimum 20 characters).'; status.className = 'flash-error'; } return; } if (password !== password_confirm) { if (status) { status.textContent = 'Passwords do not match.'; status.className = 'flash-error'; } return; } btn.disabled = true; btn.textContent = i18n.registering || 'Registering...'; if (status) { status.textContent = ''; status.className = ''; } try { const res = await fetch('/register', { method: 'POST', headers: { 'X-Requested-With': 'XMLHttpRequest', 'Accept': 'application/json', 'Content-Type': 'application/x-www-form-urlencoded' }, body: params }); const json = await res.json(); if (json.success) { if (status) { status.textContent = json.msg || 'Registration successful! You can now login.'; status.className = 'flash-success'; } registerForm.reset(); // Optional: switch to login view after a delay setTimeout(() => { const loginToRegister = document.getElementById('login-to-register'); if (loginToRegister) { // If we are in register modal, we might want to close it and open login? // But registration modal is separate in HTML. closeModal(registerModal); openModal(loginModal, 'login'); } }, 3000); } else { if (status) { status.textContent = json.msg || 'Registration failed.'; status.className = 'flash-error'; } } } catch (err) { console.error('Registration error:', err); if (status) { status.textContent = 'Network error.'; status.className = 'flash-error'; } } finally { btn.disabled = false; btn.textContent = 'Create Account'; } }); } // Switch to register from login // Switch to register from login const loginToRegister = document.getElementById('login-to-register'); if (loginToRegister) { loginToRegister.addEventListener('click', (e) => { e.preventDefault(); closeModal(loginModal); openModal(registerModal); }); } // Switch to login from register const registerToLogin = document.getElementById('register-to-login'); if (registerToLogin) { registerToLogin.addEventListener('click', (e) => { e.preventDefault(); closeModal(registerModal); openModal(loginModal, 'login'); }); } } // Shortcuts Modal Logic const shortcutsModal = document.getElementById('shortcuts-modal'); const shortcutsClose = document.getElementById('shortcuts-modal-close'); if (shortcutsModal) { if (shortcutsClose) { shortcutsClose.addEventListener('click', () => closeModal(shortcutsModal)); } shortcutsModal.addEventListener('click', (e) => { if (e.target === shortcutsModal) closeModal(shortcutsModal); }); // Delegate help button click (since it's in a partial) document.addEventListener('click', (e) => { if (e.target.id === 'help-button') { openModal(shortcutsModal); } }); } // Handle ESC key to close any modal document.addEventListener('keydown', (e) => { if (e.key === 'Escape') { closeModal(loginModal); closeModal(registerModal); closeModal(shortcutsModal); const infoModal = document.getElementById('info-modal'); if (infoModal) closeModal(infoModal); } }); var background = (window.f0ckSession && window.f0ckSession.show_background !== undefined) ? window.f0ckSession.show_background : (localStorage.getItem('background') !== 'false'); window.toggleBackground = async () => { background = !background; localStorage.setItem('background', background ? 'true' : 'false'); window.initBackground(); // Update videoplayer toggle buttons if they exist document.querySelectorAll("#togglebg").forEach(el => { el.classList.toggle('active', background); }); // Update session preference and persist if logged in if (window.f0ckSession) { window.f0ckSession.show_background = background; try { await fetch('/api/v2/settings/background', { method: 'PUT', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': window.f0ckSession.csrf_token }, body: JSON.stringify({ show_background: background }) }); } catch (err) { console.error('Failed to sync background preference:', err); } } }; // Initialize autoplay preference if (localStorage.getItem('autoplay') == undefined) { localStorage.setItem('autoplay', 'false'); } var autoplay = localStorage.getItem('autoplay') === 'true'; window.toggleAutoplay = () => { autoplay = !autoplay; localStorage.setItem('autoplay', autoplay.toString()); // Update videoplayer toggle buttons if they exist document.querySelectorAll("#toggleautoplay").forEach(el => { el.classList.toggle('active', autoplay); }); }; let bgRafId = null; let lastBgElem = null; // Apply initial visual state var initialCanvas = document.getElementById('bg'); if (initialCanvas) { // No background on SWF pages if (document.getElementById('ruffle-container')) { initialCanvas.classList.add('fader-out'); initialCanvas.classList.remove('fader-in'); } else if (background) { initialCanvas.classList.add('fader-in'); initialCanvas.classList.remove('fader-out'); } else { initialCanvas.classList.add('fader-out'); initialCanvas.classList.remove('fader-in'); } } const setupMedia = () => { const elem = document.querySelector("#my-video") || document.querySelector("audio#my-video"); if (elem) { video = new v0ck(elem); } else { video = null; } }; // Initial Load document.addEventListener('DOMContentLoaded', () => { setupMedia(); }); // Export init function for dynamic calls window.initBackground = () => { // Media selection priority let elem = document.querySelector("#my-video"); if (!elem) { const rp = document.querySelector('ruffle-player'); if (rp) { elem = rp.shadowRoot ? rp.shadowRoot.querySelector('canvas') : null; if (!elem) { // If we have a player but no canvas yet, it's likely still initializing. // Re-init background in a moment. setTimeout(window.initBackground, 200); return; } } } if (elem && elem.tagName === 'AUDIO') { elem = document.querySelector("#f0ck-audio-cover") || elem; } if (!elem || (elem.tagName === 'AUDIO')) { elem = document.querySelector("#f0ck-image") || elem; } const canvas = document.getElementById('bg'); if (elem) { if (canvas) { // Restore visual state on re-init if (background) { canvas._bgFadingOut = false; // For images: defer fader-in until drawOnce draws the thumbnail. // For video/audio: fader-in immediately. if (elem.tagName !== 'IMG') { canvas.classList.add('fader-in'); canvas.classList.remove('fader-out', 'fast-fade'); } } else { // Don't clear the canvas here — let the existing content fade out. canvas._bgFadingOut = true; canvas.classList.add('fader-out'); canvas.classList.remove('fader-in', 'fast-fade'); const stopOnFadeEnd = (ev) => { if (ev.propertyName === 'opacity') { canvas._bgFadingOut = false; canvas.removeEventListener('transitionend', stopOnFadeEnd); } }; canvas.addEventListener('transitionend', stopOnFadeEnd); return; // nothing more to do — let CSS do the fade } // Only reset canvas dimensions when turning ON (avoids clearing pixels mid-fade-out). const context = canvas.getContext('2d'); const cw = canvas.width = canvas.clientWidth | 0; const ch = canvas.height = canvas.clientHeight | 0; const drawOnce = () => { if (!background || !context) return; // Always use the thumbnail first for instant backdrop — thumbnails are tiny, // often browser-cached from grid view, and give us a frame-0 equivalent for GIFs too. // Extract item ID from URL for thumbnail path. const itemId = window.getCurrentItemId(); const showCanvas = () => { canvas.classList.remove('fader-out', 'fast-fade'); canvas.classList.add('fader-in'); }; const isDrawable = elem && elem.tagName === 'IMG'; if (itemId) { // Step 1: draw thumbnail immediately for instant background const thumb = new Image(); thumb.onload = () => { try { context.drawImage(thumb, 0, 0, cw, ch); } catch (e) {} showCanvas(); // Step 2: upgrade with full image when it's ready (skip for AUDIO elements) if (isDrawable) { if (elem.complete) { try { context.drawImage(elem, 0, 0, cw, ch); } catch (e) {} } else { elem.onload = () => { try { context.drawImage(elem, 0, 0, cw, ch); } catch (e) {} }; } } }; thumb.onerror = () => { // Thumbnail failed — fall back to waiting for the main image (skip for AUDIO) if (isDrawable) { if (elem.complete) { try { context.drawImage(elem, 0, 0, cw, ch); } catch (e) {} showCanvas(); } else { elem.onload = () => { try { context.drawImage(elem, 0, 0, cw, ch); } catch (e) {} showCanvas(); }; } } // For audio-only items with no thumbnail, canvas stays blank (nothing to draw) }; let newSrc = `/t/${itemId}.webp`; if (window.applyThumbCacheBust) newSrc = window.applyThumbCacheBust(newSrc); thumb.src = newSrc; } else if (isDrawable) { // No item ID — fall back to waiting for the main image if (elem.complete) { try { context.drawImage(elem, 0, 0, cw, ch); } catch (e) {} showCanvas(); } else { elem.onload = () => { try { context.drawImage(elem, 0, 0, cw, ch); } catch (e) {} showCanvas(); }; } } }; const animationLoop = () => { if (!elem || elem.tagName === 'AUDIO' || elem.paused || elem.ended || (!background && !canvas._bgFadingOut)) { bgRafId = null; return; } try { context.drawImage(elem, 0, 0, cw, ch); } catch (e) { bgRafId = null; return; } bgRafId = window.requestAnimFrame(animationLoop); }; // Singleton: Ensure only one listener and one loop per element if (lastBgElem !== elem) { if (bgRafId) window.cancelAnimFrame(bgRafId); lastBgElem = elem; if (elem.tagName === 'VIDEO') { elem.addEventListener('play', () => { if (bgRafId) window.cancelAnimFrame(bgRafId); if (background) animationLoop(); }); } else if (elem.tagName === 'CANVAS') { // Ruffle canvas: start loop immediately if (bgRafId) window.cancelAnimFrame(bgRafId); if (background) animationLoop(); } } if (elem.tagName === 'VIDEO') { if (!elem.paused && background) { if (bgRafId) window.cancelAnimFrame(bgRafId); animationLoop(); } } else if (elem.tagName === 'CANVAS') { if (background) { if (bgRafId) window.cancelAnimFrame(bgRafId); animationLoop(); } } else if (elem.tagName === 'IMG' || elem.tagName === 'AUDIO') { // IMG: draw from thumbnail. AUDIO: draw thumbnail from URL (no drawable elem, just background). drawOnce(); } } } else if (canvas) { // No drawable element (e.g. YouTube iframe) — still handle canvas fade toggle if (background) { canvas._bgFadingOut = false; // Draw the item thumbnail if we have an item ID in the URL const itemId = window.getCurrentItemId(); if (itemId) { const context = canvas.getContext('2d'); const cw = canvas.width = canvas.clientWidth | 0; const ch = canvas.height = canvas.clientHeight | 0; const thumb = new Image(); thumb.onload = () => { try { context.drawImage(thumb, 0, 0, cw, ch); } catch (e) {} canvas.classList.remove('fader-out', 'fast-fade'); canvas.classList.add('fader-in'); }; thumb.src = `/t/${itemId}.webp`; } else { canvas.classList.remove('fader-out', 'fast-fade'); canvas.classList.add('fader-in'); } } else { canvas._bgFadingOut = true; canvas.classList.add('fader-out'); canvas.classList.remove('fader-in', 'fast-fade'); const stopOnFadeEnd = (ev) => { if (ev.propertyName === 'opacity') { canvas._bgFadingOut = false; canvas.removeEventListener('transitionend', stopOnFadeEnd); } }; canvas.addEventListener('transitionend', stopOnFadeEnd); } } }; window.initVisualizer = () => { const audioElement = document.querySelector("audio"); if (audioElement) { // Cleanup existing visualizer if (visualizerRafId) window.cancelAnimFrame(visualizerRafId); const existingCanvas = document.querySelector(".v0ck > canvas.audio-visualizer"); if (existingCanvas) existingCanvas.remove(); const canvas = document.createElement("canvas"); canvas.className = "audio-visualizer"; const ctx = canvas.getContext("2d"); canvas.width = 1920; canvas.height = 1080; setTimeout(() => { const v0ckContainer = document.querySelector(".v0ck"); if (v0ckContainer) v0ckContainer.insertAdjacentElement("afterbegin", canvas); }, 400); if (!audioCtx) { audioCtx = new (window.AudioContext || window.webkitAudioContext)(); } const analyser = audioCtx.createAnalyser(); analyser.fftSize = 2048; try { const source = audioCtx.createMediaElementSource(audioElement); source.connect(analyser); source.connect(audioCtx.destination); } catch (e) { console.warn("Visualizer Source creation failed (already connected?):", e); } let data = new Uint8Array(analyser.frequencyBinCount); const draw = (data) => { data = [...data]; ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = getComputedStyle(document.body).getPropertyValue("--accent") || "#9f0"; data.forEach((value, i) => { const percent = value / 256; const height = (canvas.height * percent / 2) - 40; const offset = canvas.height - height - 1; const barWidth = canvas.width / analyser.frequencyBinCount; ctx.fillRect(i * barWidth, offset, barWidth, height); }); }; const loopingFunction = () => { visualizerRafId = requestAnimationFrame(loopingFunction); analyser.getByteFrequencyData(data); draw(data); }; visualizerRafId = requestAnimationFrame(loopingFunction); audioElement.onplay = () => { if (audioCtx.state === 'suspended') { audioCtx.resume(); } }; } }; // Content Warning Logic const cwModal = document.getElementById('content-warning-modal'); if (cwModal) { if (!localStorage.getItem('content_warning_accepted')) { cwModal.style.display = 'flex'; document.body.classList.add('modal-open'); } const acceptBtn = document.getElementById('cw-accept'); const declineBtn = document.getElementById('cw-decline'); if (acceptBtn) { acceptBtn.addEventListener('click', () => { localStorage.setItem('content_warning_accepted', 'true'); cwModal.style.display = 'none'; document.body.classList.remove('modal-open'); }); } if (declineBtn) { declineBtn.addEventListener('click', () => { window.location.href = 'https://duckduckgo.com'; }); } } // Initial call window.initBackground(); window.initVisualizer(); // Ruffle / SWF support — only register when the site has SWF enabled. // The static ruffle.js