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');
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';
if (mode === 'nsfw' && blurNsfw && !thumb.classList.contains('revealed')) {
bg = bg.replace('.webp', '_blur.webp');
} else if (mode === 'nsfl' && blurNsfl && !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';
if (mode === 'nsfw' && blurNsfw && !thumb.classList.contains('revealed')) {
bg = bg.replace('.webp', '_blur.webp');
} else if (mode === 'nsfl' && blurNsfl && !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') => {
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';
}
const isRandom = document.cookie.includes('random_mode=1');
let zomgHtml = '';
if (isRandom) {
zomgHtml = ' Z';
}
badge.className = badgeClass;
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 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', '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);
}
};
// 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