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'); 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 => { if (thumb.dataset.bg) { const finalBg = window.applyThumbCacheBust(thumb.dataset.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')) { 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') => { const existing = document.getElementById('f0ck-flash'); if (existing) existing.remove(); const flash = document.createElement('div'); flash.id = 'f0ck-flash'; flash.textContent = text; let baseStyle = 'position:fixed;bottom:20px;left:20px;color:#fff;padding:10px 18px;border-radius:6px;font-size:13px;z-index:9999;opacity:0;transition:opacity 0.3s;pointer-events:none;box-shadow:0 4px 12px rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.1);'; if (type === 'error') { baseStyle += 'background:rgba(200,30,30,0.95);'; } else { baseStyle += 'background:rgba(30,30,30,0.95);'; } flash.style.cssText = baseStyle; document.body.appendChild(flash); requestAnimationFrame(() => { flash.style.opacity = '1'; }); setTimeout(() => { flash.style.opacity = '0'; 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; } } 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'; 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'; } badge.className = badgeClass; badge.innerHTML = badgeText; if (hasMimeFilter) { const dotsContainer = document.createElement('span'); dotsContainer.className = 'filter-mime-dots'; selectedMimes.forEach(mime => { const dot = document.createElement('span'); dot.className = `mime-dot mime-dot-${mime}`; dot.title = mime.charAt(0).toUpperCase() + mime.slice(1); dotsContainer.appendChild(dot); }); badge.appendChild(dotsContainer); } 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 from UI const activeModeBtn = document.querySelector('.mode-btn.active'); if (activeModeBtn) { const modeMatch = activeModeBtn.href.match(/\/mode\/(\d)/); if (modeMatch) window.activeMode = +modeMatch[1]; } // 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' ]; modalIds.forEach(id => { 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); } }); 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); } }; // 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