// 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 (_) {}
})();
if (typeof window.f0ckDebug !== 'function') {
window.f0ckDebug = (...args) => {
if (window.f0ckSession?.development) console.log(...args);
};
}
window.getCsrfToken = () => {
return (window.f0ckSession && window.f0ckSession.csrf_token) ||
document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') ||
document.querySelector('input[name="csrf_token"]')?.value ||
'';
};
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 = () => {
// 1. If Onara modal is open or mounted, strictly search within #onara-item-mount
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
if (isOnaraOpen) {
const mount = document.getElementById('onara-item-mount');
if (mount) {
const idEl = mount.querySelector('.item-layout-container[data-item-id], a.id-link[data-item-id], #comments-container[data-item-id], .rating-tag[data-item-id], #tags[data-item-id]');
if (idEl?.dataset?.itemId) {
const parsed = parseInt(idEl.dataset.itemId, 10);
if (!isNaN(parsed) && parsed > 0) return String(parsed);
}
const idLink = mount.querySelector('a.id-link');
if (idLink && idLink.innerText) {
const parsed = parseInt(idLink.innerText.trim(), 10);
if (!isNaN(parsed) && parsed > 0) return String(parsed);
}
}
const activeThumb = document.querySelector('.posts > a.thumb.onara-active[data-item-id]');
if (activeThumb?.dataset?.itemId) {
const parsed = parseInt(activeThumb.dataset.itemId, 10);
if (!isNaN(parsed) && parsed > 0) return String(parsed);
}
}
// 2. If viewing an item in main content (regular full-page view or PJAX)
const itemContainer = document.querySelector('.item-layout-container, #main.item-view, #main > .container.item-view');
if (itemContainer) {
const idEl = itemContainer.querySelector('.item-layout-container[data-item-id], a.id-link[data-item-id], #comments-container[data-item-id], .rating-tag[data-item-id], #tags[data-item-id]');
if (idEl?.dataset?.itemId) {
const parsed = parseInt(idEl.dataset.itemId, 10);
if (!isNaN(parsed) && parsed > 0) return String(parsed);
}
if (itemContainer.dataset?.itemId) {
const parsed = parseInt(itemContainer.dataset.itemId, 10);
if (!isNaN(parsed) && parsed > 0) return String(parsed);
}
const idLink = itemContainer.querySelector('a.id-link');
if (idLink && idLink.innerText) {
const parsed = parseInt(idLink.innerText.trim(), 10);
if (!isNaN(parsed) && parsed > 0) return String(parsed);
}
}
// 3. Fallback to URL pathname if on an item page
const path = window.location.pathname;
if (path.includes('/admin/') || path.includes('/mod/') || path.includes('/settings') || path.includes('/user/')) return null;
const match = path.match(/\/(\d+)\/?$/);
if (match) return match[1];
// NOTE: NEVER use document.querySelector('[data-item-id]') globally here,
// as it erroneously matches feed/grid thumbnails!
return null;
};
// - disabled for clean guest mode
const f0ckGuestFavs = {
get: () => [],
has: () => false,
toggle: () => false,
clear: () => {
try {
localStorage.removeItem('f0ck_guest_favs');
localStorage.removeItem('guest_favs');
} catch (e) {}
},
count: () => 0
};
window.f0ckGuestFavs = f0ckGuestFavs;
const syncGuestFavoIcon = () => {};
window.syncGuestFavoIcon = syncGuestFavoIcon;
document.addEventListener('click', e => {
const target = e.target.nodeType === 3 ? e.target.parentElement : e.target;
const favoBtn = target.closest('#a_favo');
if (!favoBtn) return;
if (window.f0ckSession?.is_anon && window.f0ckSession?.anon_permissions && window.f0ckSession.anon_permissions.favorite === false) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (typeof window.flashMessage === 'function') {
window.flashMessage('Anonymous favoriting is disabled.', 3000, 'error');
}
return;
}
if (window.f0ckSession && window.f0ckSession.user) return;
e.preventDefault();
e.stopPropagation();
if (typeof window.flashMessage === 'function') {
window.flashMessage('Login to favorite posts', 3000, 'warning');
}
});
const checkGuestFavsImportBanner = () => {
if (!window.f0ckSession || !window.f0ckSession.user) return;
const isUserFavs = window.location.pathname.match(/\/user\/([^/]+)\/favs/);
if (!isUserFavs) return;
const currentUser = window.f0ckSession.user.toLowerCase();
if (decodeURIComponent(isUserFavs[1]).toLowerCase() !== currentUser) return;
const count = window.f0ckGuestFavs ? window.f0ckGuestFavs.count() : 0;
if (count <= 0) {
const existing = document.getElementById('guest-favs-import-banner');
if (existing) existing.remove();
return;
}
if (document.getElementById('guest-favs-import-banner')) return;
const postsContainer = document.querySelector('.posts');
if (!postsContainer || !postsContainer.parentElement) return;
const banner = document.createElement('div');
banner.id = 'guest-favs-import-banner';
banner.className = 'guest-favs-banner';
banner.style.cssText = 'background: rgba(255, 107, 157, 0.12); border: 1px solid rgba(255, 107, 157, 0.35); border-radius: 8px; padding: 12px 18px; margin: 15px auto; max-width: 900px; display: flex; align-items: center; justify-content: space-between; gap: 12px; font-size: 0.95em; color: var(--text-color, #fff);';
const textSpan = document.createElement('span');
const msg = (window.f0ckI18n && window.f0ckI18n.guest_favs_saved) || 'You have {count} guest favorites saved on this device.';
textSpan.innerHTML = ` ${msg.replace('{count}', `${count}`)}`;
const actionsDiv = document.createElement('div');
actionsDiv.style.cssText = 'display: flex; gap: 8px; align-items: center; flex-shrink: 0;';
const importBtn = document.createElement('button');
importBtn.id = 'btn-import-guest-favs';
importBtn.className = 'btn btn-sm';
importBtn.style.cssText = 'background: #ff6b9d; border: none; border-radius: 4px; padding: 6px 14px; color: white; cursor: pointer; font-weight: 600; font-size: 0.85em; transition: background 0.15s;';
importBtn.textContent = (window.f0ckI18n && window.f0ckI18n.sync_guest_favs) || 'Import to Account';
const dismissBtn = document.createElement('button');
dismissBtn.id = 'btn-dismiss-guest-favs';
dismissBtn.className = 'btn btn-sm';
dismissBtn.style.cssText = 'background: transparent; border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; padding: 6px 10px; color: #ccc; cursor: pointer; font-size: 0.85em;';
dismissBtn.textContent = (window.f0ckI18n && window.f0ckI18n.dismiss) || 'Dismiss';
importBtn.onclick = async () => {
importBtn.disabled = true;
importBtn.textContent = '...';
try {
const ids = window.f0ckGuestFavs.get();
const res = await fetch('/api/v2/favorites/import', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': window.f0ckSession?.csrf_token || ''
},
body: JSON.stringify({ ids })
}).then(r => r.json());
if (res.success) {
window.f0ckGuestFavs.clear();
banner.remove();
if (window.flashMessage) {
const succMsg = (window.f0ckI18n && window.f0ckI18n.guest_favs_imported) || 'Imported favorites to your account!';
window.flashMessage(succMsg);
}
if (typeof window.loadPageAjax === 'function') {
window.loadPageAjax(window.location.pathname, true, { bypassCache: true });
} else {
window.location.reload();
}
} else {
importBtn.disabled = false;
importBtn.textContent = (window.f0ckI18n && window.f0ckI18n.sync_guest_favs) || 'Import to Account';
}
} catch (err) {
importBtn.disabled = false;
importBtn.textContent = (window.f0ckI18n && window.f0ckI18n.sync_guest_favs) || 'Import to Account';
}
};
dismissBtn.onclick = () => {
window.f0ckGuestFavs.clear();
banner.remove();
};
actionsDiv.appendChild(importBtn);
actionsDiv.appendChild(dismissBtn);
banner.appendChild(textSpan);
banner.appendChild(actionsDiv);
postsContainer.parentElement.insertBefore(banner, postsContainer);
};
window.checkGuestFavsImportBanner = checkGuestFavsImportBanner;
//
// 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');
// DPR / zoom-level detection — works in Firefox (which ignores zoom in CSS dppx queries)
const _updateDprTier = () => {
const dpr = window.devicePixelRatio || 1;
// Tiers mirror the CSS resolution media-query breakpoints
let tier = 'dpr-1x'; // 1.0 – 1.04
if (dpr >= 1.55) tier = 'dpr-high'; // ≥ 155% zoom
else if (dpr >= 1.39) tier = 'dpr-150'; // ~140–154% zoom
else if (dpr >= 1.26) tier = 'dpr-130'; // ~126–138%
else if (dpr >= 1.20) tier = 'dpr-120'; // ~120–125%
else if (dpr >= 1.05) tier = 'dpr-110'; // ~105–115%
htmlEl.setAttribute('data-dpr', tier);
};
_updateDprTier();
// Re-evaluate when the user changes zoom (fires on every DPR change)
if (window.matchMedia) {
let _dprMql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
const _onDprChange = () => {
_updateDprTier();
// Re-bind to the NEW dpr value so we catch the next change too
_dprMql.removeEventListener('change', _onDprChange);
_dprMql = window.matchMedia(`(resolution: ${window.devicePixelRatio}dppx)`);
_dprMql.addEventListener('change', _onDprChange);
};
_dprMql.addEventListener('change', _onDprChange);
}
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');
if (localStorage.getItem('imageExpandOnClick') !== 'false') htmlEl.classList.add('image-expand-active');
if (localStorage.getItem('hideItemRatings') === 'true') htmlEl.classList.add('hide-item-ratings-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); }
};
const ensureLayoutClass = () => {
const isLegacy = window.f0ckSession ? !window.f0ckSession.use_new_layout : (window.f0ckDefaultLayout === 'legacy');
document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy');
if (isLegacy) {
document.body.classList.add('layout-legacy');
} else {
document.body.classList.add('layout-modern');
}
};
window.ensureLayoutClass = ensureLayoutClass;
ensureLayoutClass();
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(), hasCoverart = undefined) => {
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();
document.querySelectorAll(`[data-bg*="/t/${idStr}.webp"], [data-bg*="/t/${idStr}_blur.webp"]`).forEach(el => {
if (el.dataset.bg) {
el.dataset.bg = window.applyThumbCacheBust(el.dataset.bg);
}
// Update the --thumb-bg CSS custom property used by the ::after pseudo-element
const currentBg = el.style.getPropertyValue('--thumb-bg');
if (currentBg && (currentBg.includes(`/t/${idStr}.webp`) || currentBg.includes(`/t/${idStr}_blur.webp`))) {
const newUrl = currentBg.replace(/url\(['"](.*?)['"]\)/, (_, p1) => `url('${window.applyThumbCacheBust(p1)}')`);
el.style.setProperty('--thumb-bg', newUrl);
}
});
// 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) {}
});
const currentId = typeof window.getCurrentItemId === 'function' ? window.getCurrentItemId() : null;
// Update audio cover in player if viewing this audio item
const audioCover = document.getElementById('f0ck-audio-cover');
if (audioCover && currentId === idStr) {
if (hasCoverart === true || (hasCoverart === undefined && audioCover.src && audioCover.src.includes('/ca/'))) {
const coverUrl = `/ca/${idStr}.webp?t=${timestamp}`;
audioCover.src = coverUrl;
const parent = audioCover.parentElement;
if (parent) {
parent.style.background = `url('${coverUrl}') no-repeat center / contain black`;
}
const audioEl = document.querySelector('audio#my-video');
if (audioEl) {
audioEl.setAttribute('poster', coverUrl);
}
} else if (hasCoverart === false) {
const fallbackUrl = '/s/img/audio.webp';
audioCover.src = fallbackUrl;
const parent = audioCover.parentElement;
if (parent) {
parent.style.background = `url('${fallbackUrl}') no-repeat center / contain black`;
}
const audioEl = document.querySelector('audio#my-video');
if (audioEl) {
audioEl.setAttribute('poster', fallbackUrl);
}
}
}
// Refresh background canvas if it matches the current item
if (currentId === idStr && window.initBackground) {
window.initBackground();
}
};
window.loadedThumbs = window.loadedThumbs || new Set();
let lazyObserver;
window.initLazyLoading = () => {
const getFinalBg = (thumb) => {
let bg = thumb.dataset.bg;
if (!bg) return null;
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');
}
return window.applyThumbCacheBust(bg);
};
// Helper: apply thumb image via CSS custom property — picked up by the loaded CSS rule
const applyThumb = (thumb, finalBg) => {
thumb.style.setProperty('--thumb-bg', `url('${finalBg}')`);
thumb.classList.add('loaded');
thumb.classList.remove('lazy-thumb');
window.loadedThumbs.add(finalBg);
};
// Synchronously resolve any already-loaded thumbnails (cached from previous navigation)
document.querySelectorAll('.lazy-thumb').forEach(thumb => {
const finalBg = getFinalBg(thumb);
if (finalBg && window.loadedThumbs.has(finalBg)) {
applyThumb(thumb, finalBg);
}
});
if (!('IntersectionObserver' in window)) {
document.querySelectorAll('.lazy-thumb').forEach(thumb => {
const finalBg = getFinalBg(thumb);
if (finalBg) applyThumb(thumb, finalBg);
});
return;
}
// ── Synchronous first pass: load all thumbs currently visible in the viewport ──
// IntersectionObserver fires asynchronously — visible items would show skeleton
// for a brief frame. This eliminates that by loading them in the same JS tick.
//
// IMPORTANT: batch all getBoundingClientRect() reads before any writes.
// Chromium invalidates its layout cache on every DOM write, so interleaving
// reads (getBoundingClientRect) and writes (classList.add, style.setProperty)
// inside one loop causes one full layout recalculation per thumbnail — O(n) thrash.
// Separating into a read pass then a write pass means a single layout flush.
const vw = window.innerWidth;
const vh = window.innerHeight;
const unloadedThumbs = Array.from(document.querySelectorAll('.lazy-thumb'))
.filter(t => !t.classList.contains('loaded'));
// ── Read phase: one layout flush for all rects ──
const thumbRects = unloadedThumbs.map(t => t.getBoundingClientRect());
// ── Write phase: no layout reads ──
unloadedThumbs.forEach((thumb, i) => {
const r = thumbRects[i];
if (r.bottom > 0 && r.top < vh && r.right > 0 && r.left < vw) {
const finalBg = getFinalBg(thumb);
if (finalBg) {
const img = new Image();
img.src = finalBg;
if (img.complete && img.naturalWidth > 0) {
applyThumb(thumb, finalBg);
} else {
img.onload = () => applyThumb(thumb, finalBg);
img.onerror = () => {
const mime = thumb.dataset.mime || '';
if (mime.startsWith('audio/')) {
if (!thumb.querySelector('.sidebar-media-placeholder.audio')) {
const ph = document.createElement('div');
ph.className = 'sidebar-media-placeholder audio';
ph.innerHTML = '';
thumb.prepend(ph);
}
thumb.classList.add('thumb-fallback');
}
thumb.classList.remove('lazy-thumb');
};
}
thumb.dataset.lazyObserved = 'true';
}
}
});
if (!lazyObserver) {
lazyObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const thumb = entry.target;
if (thumb.dataset.bg && !thumb.classList.contains('loaded')) {
const finalBg = getFinalBg(thumb);
if (finalBg) {
const img = new Image();
img.src = finalBg;
if (img.complete && img.naturalWidth > 0) {
applyThumb(thumb, finalBg);
} else {
img.onload = () => applyThumb(thumb, finalBg);
img.onerror = () => {
const retries = parseInt(thumb.dataset.retries || '0');
if (retries < 3) {
thumb.dataset.retries = retries + 1;
setTimeout(() => { img.src = finalBg + '?r=' + Date.now(); }, 1000);
} else {
const mime = thumb.dataset.mime || '';
if (mime.startsWith('audio/')) {
if (!thumb.querySelector('.sidebar-media-placeholder.audio')) {
const ph = document.createElement('div');
ph.className = 'sidebar-media-placeholder audio';
ph.innerHTML = '';
thumb.prepend(ph);
}
thumb.classList.add('thumb-fallback');
}
thumb.classList.remove('lazy-thumb');
}
};
}
}
}
lazyObserver.unobserve(thumb);
}
});
}, { rootMargin: '200px 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') {
document.querySelectorAll('.lazy-thumb:not(.loaded)').forEach(t => {
delete t.dataset.lazyObserved;
});
window.initLazyLoading();
}
});
}
// Observe only off-screen thumbs — visible ones were already handled above
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
const fsEl = document.fullscreenElement;
const targetParent = fsEl || document.body;
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:100200;display:flex;flex-direction:column-reverse;gap:8px;pointer-events:none;';
targetParent.appendChild(container);
} else {
container.style.zIndex = '100200';
if (container.parentElement !== targetParent) {
targetParent.appendChild(container);
}
}
const flash = document.createElement('div');
if (typeof text === 'string' && text.includes(' {
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})
const itemCacheMap = new Map(); // Stale-while-revalidate cache for item AJAX HTML (cacheKey -> {html, ts})
const ITEM_CACHE_MAX = 30; // Max entries; evict oldest on overflow
window.activeMode = 0; // Default
let audioCtx = null;
let visualizerRafId = null;
let audioSource = null;
const isAnonUser = () => !(window.f0ckSession && window.f0ckSession.user && !window.f0ckSession.is_anon);
window.isAnonUser = isAnonUser;
const isGuestUser = () => {
if (!window.f0ckSession) return true;
if (window.f0ckSession.user && !window.f0ckSession.is_anon) return false;
if (window.f0ckSession.is_anon && (window.f0ckSession.logged_in || window.f0ckSession.user)) return false;
if (window.f0ckAnonSSH && (window.f0ckAnonSSH.isSessionReady || window.f0ckAnonSSH.pubkey)) return false;
if (typeof localStorage !== 'undefined' && localStorage.getItem('f0ck_anon_ssh_pub')) return false;
return !window.f0ckSession.logged_in;
};
window.isGuestUser = isGuestUser;
const getRatingsCookie = () => {
const raw = document.cookie.split('; ').find(r => r.startsWith('ratings='));
if (!raw) return [];
const val = raw.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));
};
window.getRatingsCookie = getRatingsCookie;
const setRatingsCookie = (ratings) => {
const val = ratings.join('|');
document.cookie = `ratings=${val}; Path=/; Max-Age=31536000; SameSite=Lax`;
};
window.setRatingsCookie = setRatingsCookie;
const clearRatingsCookie = () => {
document.cookie = 'ratings=; Path=/; Max-Age=0; SameSite=Lax';
};
window.clearRatingsCookie = clearRatingsCookie;
const reconcileAnonFilterPermissions = () => {
if (isGuestUser()) {
let changed = false;
if (window.activeMode !== 0) {
window.activeMode = 0;
document.cookie = 'mode=0; Path=/; Max-Age=31536000; SameSite=Lax';
changed = true;
}
const curRatings = getRatingsCookie();
if (curRatings.length !== 1 || curRatings[0] !== 'sfw') {
setRatingsCookie(['sfw']);
changed = true;
}
return changed;
}
if (!isAnonUser()) return false;
const perms = window.f0ckSession?.anon_permissions;
if (!perms) return false;
let changed = false;
const canFilter = perms.filter !== false;
const allowedModes = Array.isArray(perms.allowed_modes) ? perms.allowed_modes.map(m => String(m).toLowerCase()) : ['sfw', 'nsfw', 'untagged', 'all', 'nsfl'];
const allowedMimes = Array.isArray(perms.allowed_mimes) ? perms.allowed_mimes.map(m => String(m).toLowerCase()) : ['image', 'video', 'audio', 'flash', 'pdf'];
const modeNames = ['sfw', 'nsfw', 'untagged', 'all', 'nsfl'];
// 1. Check locked rating from permissions (single allowed mode only)
const isSingleAllowedMode = allowedModes.length === 1 && !allowedModes.includes('all');
const lockedRating = isSingleAllowedMode ? allowedModes[0] : null;
if (lockedRating) {
const targetMode = modeNames.indexOf(lockedRating);
if (targetMode >= 0 && window.activeMode !== targetMode) {
window.activeMode = targetMode;
document.cookie = `mode=${targetMode}; Path=/; Max-Age=31536000; SameSite=Lax`;
changed = true;
}
const raw = getRatingsCookie();
if (raw.length !== 1 || raw[0] !== lockedRating) {
setRatingsCookie([lockedRating]);
changed = true;
}
} else {
// Validate activeMode
const curModeName = modeNames[window.activeMode] || 'sfw';
if (!canFilter || !allowedModes.includes(curModeName)) {
const fallbackName = allowedModes.find(m => modeNames.includes(m)) || 'sfw';
const fallbackIdx = modeNames.indexOf(fallbackName);
window.activeMode = fallbackIdx >= 0 ? fallbackIdx : 0;
document.cookie = `mode=${window.activeMode}; Path=/; Max-Age=31536000; SameSite=Lax`;
changed = true;
}
// Validate ratings cookie
const curRatings = getRatingsCookie();
if (curRatings.length > 0) {
if (!canFilter) {
clearRatingsCookie();
changed = true;
} else {
const filtered = curRatings.filter(r => allowedModes.includes(r));
if (filtered.length !== curRatings.length) {
changed = true;
if (filtered.length > 0) {
setRatingsCookie(filtered);
} else {
if (allowedModes.length === 1 && !allowedModes.includes('all')) {
setRatingsCookie([allowedModes[0]]);
} else {
clearRatingsCookie();
}
}
}
}
}
}
// 2. Validate MIME cookie
const cookieMimeRaw = document.cookie.split('; ').find(row => row.startsWith('mime='));
const isSingleMime = Array.isArray(allowedMimes) && allowedMimes.length === 1;
const singleMime = isSingleMime ? allowedMimes[0] : null;
if (isSingleMime) {
const curVal = cookieMimeRaw ? cookieMimeRaw.split('=')[1] : null;
if (curVal !== singleMime) {
document.cookie = `mime=${singleMime}; path=/; max-age=31536000; SameSite=Lax`;
changed = true;
}
} else if (cookieMimeRaw) {
const curVal = cookieMimeRaw.split('=')[1] || '';
const curParts = curVal ? curVal.split(',').filter(m => ['video', 'audio', 'image', 'flash', 'pdf'].includes(m)) : [];
if (!canFilter) {
if (curVal !== '') {
document.cookie = 'mime=; path=/; max-age=0; SameSite=Lax';
changed = true;
}
} else {
const filteredParts = curParts.filter(m => allowedMimes.includes(m));
if (filteredParts.length !== curParts.length) {
changed = true;
if (filteredParts.length > 0) {
document.cookie = `mime=${filteredParts.join(',')}; path=/; max-age=31536000; SameSite=Lax`;
} else {
document.cookie = 'mime=; path=/; max-age=0; SameSite=Lax';
}
}
}
}
return changed;
};
window.reconcileAnonFilterPermissions = reconcileAnonFilterPermissions;
const updateMimeLabel = () => {
reconcileAnonFilterPermissions();
const isAnon = isAnonUser();
const allowedMimes = window.f0ckSession?.anon_permissions?.allowed_mimes;
const isSingleMime = isAnon && Array.isArray(allowedMimes) && allowedMimes.length === 1;
const singleMime = isSingleMime ? allowedMimes[0] : null;
let selected = [];
if (isSingleMime) {
selected = [singleMime];
const cookieMime = document.cookie.split('; ').find(row => row.startsWith('mime='));
const val = cookieMime ? cookieMime.split('=')[1] : null;
if (val !== singleMime) {
document.cookie = `mime=${singleMime}; path=/; max-age=31536000; SameSite=Lax`;
}
} else {
let mimeStr = null;
const cookieMime = document.cookie.split('; ').find(row => row.startsWith('mime='));
if (cookieMime) {
mimeStr = cookieMime.split('=')[1];
}
selected = mimeStr ? mimeStr.split(',').filter(m => ['video', 'audio', 'image', 'flash'].includes(m)) : [];
if (isAnon && Array.isArray(allowedMimes) && allowedMimes.length > 0) {
const filtered = selected.filter(m => allowedMimes.includes(m));
if (filtered.length !== selected.length) {
selected = filtered;
if (selected.length > 0) {
document.cookie = `mime=${selected.join(',')}; path=/; max-age=31536000; SameSite=Lax`;
} else {
document.cookie = `mime=; path=/; max-age=0; SameSite=Lax`;
}
}
}
}
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} ▾`;
if (isSingleMime) {
btn.classList.add('locked');
}
});
document.querySelectorAll('.nav-mime-menu').forEach(menu => {
menu.querySelectorAll('input[type="checkbox"]').forEach(cb => {
if (isSingleMime) {
cb.checked = (cb.value === singleMime);
cb.disabled = true;
const label = cb.closest('label') || cb.parentElement;
if (label) {
label.classList.add('locked');
label.style.cursor = 'not-allowed';
label.title = 'Locked by site permissions';
}
} else {
cb.checked = selected.includes(cb.value);
}
});
});
if (window.updateFilterBadge) window.updateFilterBadge();
};
window.updateMimeLabel = updateMimeLabel;
const updateFilterBadge = () => {
reconcileAnonFilterPermissions();
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.
// For logged-in users, the server's f0ckSession.mode (sourced from user_options DB) is the
// authoritative cross-device truth. A stale local `mode` cookie from a previous session on
// this device could disagree (e.g. PC had ALL, mobile set SFW — PC cookie still says 3).
// Fix: prefer f0ckSession.mode when logged in and reconcile the local cookie to match.
// For guests: fall back to cookie (no session = cookie is the only persistence).
if (window.f0ckSession && window.f0ckSession.logged_in && !window.f0ckSession.is_anon && window.f0ckSession.mode !== undefined) {
window.activeMode = +window.f0ckSession.mode;
// Reconcile the local mode cookie so future reads are consistent on this device
document.cookie = `mode=${window.activeMode}; Path=/; Max-Age=31536000`;
// If the authoritative mode is ALL (3), also clear any stale ratings cookie.
// Otherwise a ratings=sfw cookie left from a previous device's filter would make
// the badge and button UI show the wrong state (even though the server ignores
// the ratings cookie when mode=3).
if (window.activeMode === 3) {
document.cookie = 'ratings=; Path=/; Max-Age=0';
}
} else {
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];
}
}
const filterReconciled = reconcileAnonFilterPermissions();
if (filterReconciled) {
const postsEl = document.querySelector('.posts, .tags-grid');
if (postsEl && postsEl.children.length === 0 && window.loadPageAjax) {
window.loadPageAjax(window.location.href, { skipPush: true, skipCache: true });
}
}
}
// Track active mode on for notification thumbnail blur.
// data-notif-filter is set to the current mode string and also
// drives src-swapping to pre-blurred thumbnails (/t/{id}_blur.webp).
const _notifBlurShouldBlur = (itemMode, activeMode) => {
if (activeMode === 3) return false; // ALL mode: never blur
if (activeMode === 0) return itemMode === 'nsfw' || itemMode === 'nsfl'; // SFW filter
if (activeMode === 1) return itemMode === 'nsfl'; // NSFW filter
if (activeMode === 4) return itemMode === 'sfw' || itemMode === 'nsfw'; // NSFL filter
return false;
};
window.applyNotifThumbBlur = (container) => {
const root = container || document;
const mode = window.activeMode ?? 3;
root.querySelectorAll('.notif-thumb[data-mode]').forEach(thumb => {
const img = thumb.querySelector('img');
if (!img) return;
const itemMode = thumb.dataset.mode;
if (!itemMode || thumb.classList.contains('revealed')) return;
if (_notifBlurShouldBlur(itemMode, mode)) {
if (!thumb.classList.contains('notif-thumb-blurred')) {
const htmlOrigSrc = img.getAttribute('data-orig-src');
const idMatch = img.src.match(/\/t\/(\d+)(?:_blur)?\.webp/);
if (idMatch) {
// Ensure JS dataset has origSrc (server may have set it via HTML attribute)
if (!img.dataset.origSrc) {
img.dataset.origSrc = htmlOrigSrc || `/t/${idMatch[1]}.webp`;
}
// Only swap src if not already the blur version
if (!img.src.includes('_blur.webp')) {
img.src = `/t/${idMatch[1]}_blur.webp`;
}
thumb.classList.add('notif-thumb-blurred');
}
}
} else {
// Restore original thumbnail
const origSrc = img.dataset.origSrc || img.getAttribute('data-orig-src');
if (origSrc) {
img.src = origSrc;
delete img.dataset.origSrc;
thumb.classList.remove('notif-thumb-blurred');
}
}
});
};
const _updateNotifFilterClass = (mode) => {
const modeNames = { 0: 'sfw', 1: 'nsfw', 2: 'untagged', 3: 'all', 4: 'nsfl' };
htmlEl.setAttribute('data-notif-filter', modeNames[mode] || 'all');
if (mode === 3) htmlEl.classList.add('mode-all');
else htmlEl.classList.remove('mode-all');
// Re-apply blur src swaps whenever mode changes
window.applyNotifThumbBlur();
};
_updateNotifFilterClass(window.activeMode ?? 3);
document.addEventListener('f0ck:modeChanged', (e) => {
_updateNotifFilterClass(e.detail?.mode ?? 3);
});
// Apply on hard load (F5) and on every AJAX navigation (f0ck:contentLoaded)
const _applyNotifBlurOnNav = () => window.applyNotifThumbBlur();
document.addEventListener('DOMContentLoaded', _applyNotifBlurOnNav);
document.addEventListener('f0ck:contentLoaded', _applyNotifBlurOnNav);
// ---- Multi-select Rating Toggles ----
const syncRatingButtonUI = () => {
reconcileAnonFilterPermissions();
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;
const isGuest = isGuestUser();
if (isGuest) {
selector.classList.add('locked');
selector.dataset.guestLocked = 'true';
selector.dataset.lockedRating = 'sfw';
selector.querySelectorAll('.rating-toggle-btn').forEach(btn => {
if (btn.dataset.rating === 'sfw') {
btn.classList.add('active', 'locked');
btn.disabled = true;
btn.style.cursor = 'default';
btn.style.opacity = '1';
btn.style.display = '';
if (!btn.querySelector('.fa-lock')) {
const lockIcon = document.createElement('i');
lockIcon.className = 'fa-solid fa-lock';
lockIcon.style.marginLeft = '5px';
lockIcon.style.fontSize = '0.75em';
lockIcon.style.opacity = '0.7';
btn.appendChild(lockIcon);
}
} else {
btn.classList.remove('active');
btn.classList.add('locked');
btn.disabled = true;
btn.style.cursor = 'not-allowed';
btn.style.opacity = '0.45';
btn.style.display = '';
btn.title = (window.f0ckI18n && window.f0ckI18n.members_only) || 'Only available for members';
if (!btn.querySelector('.fa-lock')) {
const lockIcon = document.createElement('i');
lockIcon.className = 'fa-solid fa-lock';
lockIcon.style.marginLeft = '5px';
lockIcon.style.fontSize = '0.75em';
lockIcon.style.opacity = '0.7';
btn.appendChild(lockIcon);
}
}
});
return;
}
// User is NOT a guest (anonymous user or registered member)
delete selector.dataset.guestLocked;
selector.removeAttribute('data-guest-locked');
// Check single allowed mode restrictions
const anonPerms = window.f0ckSession?.anon_permissions;
const allowedModes = Array.isArray(anonPerms?.allowed_modes)
? anonPerms.allowed_modes.map(m => String(m).toLowerCase())
: ['sfw', 'nsfw', 'untagged', 'all', 'nsfl'];
const isSingleAllowedMode = isAnonUser() && allowedModes.length === 1 && !allowedModes.includes('all');
const singleLockedMode = isSingleAllowedMode ? allowedModes[0] : null;
if (singleLockedMode) {
selector.classList.add('locked');
selector.dataset.lockedRating = singleLockedMode;
selector.querySelectorAll('.rating-toggle-btn').forEach(btn => {
if (btn.dataset.rating === singleLockedMode) {
btn.classList.add('active', 'locked');
btn.disabled = true;
btn.style.cursor = 'not-allowed';
btn.style.opacity = '0.85';
if (!btn.querySelector('.fa-lock')) {
const lockIcon = document.createElement('i');
lockIcon.className = 'fa-solid fa-lock';
lockIcon.style.marginLeft = '5px';
lockIcon.style.fontSize = '0.75em';
lockIcon.style.opacity = '0.7';
btn.appendChild(lockIcon);
}
} else {
btn.classList.remove('active');
btn.disabled = true;
btn.style.display = 'none';
}
});
return;
}
// User is NOT locked into a single mode: unlock container and buttons
selector.classList.remove('locked');
delete selector.dataset.lockedRating;
selector.removeAttribute('data-locked-rating');
selector.querySelectorAll('.rating-toggle-btn').forEach(btn => {
const r = btn.dataset.rating;
btn.classList.remove('locked');
btn.disabled = false;
btn.style.cursor = '';
btn.style.opacity = '';
btn.style.display = '';
btn.querySelectorAll('.fa-lock').forEach(icon => icon.remove());
if (r) {
btn.classList.toggle('active', activeRatings.includes(r));
if (btn.title === 'Only available for members' || (window.f0ckI18n && btn.title === window.f0ckI18n.members_only)) {
btn.title = r.toUpperCase();
}
}
});
// 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.remove('locked');
allBtn.disabled = false;
allBtn.style.cursor = '';
allBtn.style.opacity = '';
allBtn.querySelectorAll('.fa-lock').forEach(icon => icon.remove());
allBtn.classList.toggle('active', (getRatingsCookie().length === 0 && window.activeMode === 3) || activeRatings.length === 0);
if (allBtn.title === 'Only available for members' || (window.f0ckI18n && allBtn.title === window.f0ckI18n.members_only)) {
allBtn.title = 'Show All Ratings';
}
}
};
// Wire up rating toggle buttons
document.addEventListener('click', (e) => {
const btn = e.target.closest('.rating-toggle-btn');
if (!btn) return;
if (isGuestUser() || btn.disabled || btn.classList.contains('locked') || btn.closest('#rating-selector[data-locked-rating]') || btn.closest('#rating-selector[data-guest-locked]') || btn.closest('#rating-selector.locked')) {
e.preventDefault();
e.stopPropagation();
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 isOnaraOpen = document.body.classList.contains('onara-modal-open');
const isGridView = document.querySelector('.posts, .tags-grid');
const isItemView = document.getElementById('prev') || document.getElementById('next') || /^\/\d+/.test(window.location.pathname);
let reloadPromise = null;
if (isOnaraOpen) {
const bgUrl = window._onaraReturnUrl || getOnaraBaseUrl();
const urlObj = new URL(bgUrl, window.location.origin);
urlObj.searchParams.delete('mode');
reloadPromise = window.loadPageAjax ? window.loadPageAjax(urlObj.toString(), true, { skipCache: true, skipPush: true }) : null;
} else if (isGridView) {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.delete('mode');
reloadPromise = window.loadPageAjax ? window.loadPageAjax(currentUrl.toString(), true, { skipCache: true }) : null;
} else if (isItemView) {
reloadPromise = window.loadItemAjax ? window.loadItemAjax(window.location.href, true, { skipCache: true }) : null;
}
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 isOnaraOpen = document.body.classList.contains('onara-modal-open');
const isGridView = document.querySelector('.posts, .tags-grid');
const isItemView = document.getElementById('prev') || document.getElementById('next') || /^\/\d+/.test(window.location.pathname);
let reloadPromise = null;
if (isOnaraOpen) {
const bgUrl = window._onaraReturnUrl || getOnaraBaseUrl();
const urlObj = new URL(bgUrl, window.location.origin);
urlObj.searchParams.delete('mode');
reloadPromise = window.loadPageAjax ? window.loadPageAjax(urlObj.toString(), true, { skipCache: true, skipPush: true }) : null;
} else if (isGridView) {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.delete('mode');
reloadPromise = window.loadPageAjax ? window.loadPageAjax(currentUrl.toString(), true, { skipCache: true }) : null;
} else if (isItemView) {
updateNavForMode(window.activeMode);
reloadPromise = window.loadItemAjax ? window.loadItemAjax(window.location.href, true, { skipCache: true }) : null;
}
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();
window.addEventListener('f0ck:anon_session_ready', (e) => {
if (window.f0ckSession) {
window.f0ckSession.user = 'anonymous';
window.f0ckSession.is_anon = true;
window.f0ckSession.logged_in = true;
if (e.detail?.userId) {
window.f0ckSession.id = e.detail.userId;
window.f0ckSession.user_id = e.detail.userId;
}
}
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();
if (e.target.closest('.nav-avatar-img, .nav-avatar-icon')) {
const username = window.f0ckSession?.is_anon ? (window.f0ckSession?.login || window.f0ckSession?.user) : window.f0ckSession?.user;
if (username) {
const url = `/user/${username.toLowerCase()}`;
if (typeof window.loadPageAjax === 'function') {
window.loadPageAjax(url, true);
} else {
window.location.href = url;
}
return;
}
}
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();
}
});
const fallbackCopy = (text, msg) => {
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
textarea.style.top = "0";
document.body.appendChild(textarea);
textarea.focus();
textarea.select();
try {
document.execCommand("copy");
if (typeof window.flashMessage === 'function') {
window.flashMessage(msg);
}
} catch (err) {
console.error("Fallback copy failed:", err);
}
document.body.removeChild(textarea);
};
const copyCurrentUrl = () => {
const url = window.location.href;
const msg = (window.f0ckI18n && window.f0ckI18n.copied) || 'URL copied to clipboard';
if (navigator.clipboard && navigator.clipboard.writeText) {
navigator.clipboard.writeText(url).then(() => {
if (typeof window.flashMessage === 'function') {
window.flashMessage(msg);
}
}).catch(() => {
fallbackCopy(url, msg);
});
} else {
fallbackCopy(url, msg);
}
};
window.copyCurrentUrl = copyCurrentUrl;
// Left-click on timestamp copies the current URL to clipboard and shows toast (same as pressing "y")
document.addEventListener('click', (e) => {
const tsTarget = e.target.closest('.timestamp-link, time.timeago');
if (tsTarget) {
if (e.button === 0) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
copyCurrentUrl();
}
}
}, true);
// 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', 'visibility-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.
});
};
// ── Onara Alternative Flavor: Fullscreen Modal Wrapper ─────────────────────
const isOnaraActive = () => !!(window.f0ckSession?.onara || window.onara);
const getOrCreateOnaraModal = () => {
let modal = document.getElementById('onara-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'onara-modal';
modal.className = 'onara-modal-wrapper';
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
modal.innerHTML = `
`;
document.body.appendChild(modal);
}
return modal;
};
const openOnaraModal = () => {
const modal = getOrCreateOnaraModal();
if (modal._closeTimeout) {
clearTimeout(modal._closeTimeout);
modal._closeTimeout = null;
}
modal._isClosing = false;
document.body.classList.add('onara-modal-open');
modal.style.display = 'flex';
modal.setAttribute('aria-hidden', 'false');
modal.classList.remove('onara-fade-out');
void modal.offsetWidth; // Force reflow so transition starts reliably
modal.classList.add('onara-fade-in');
return modal;
};
window.openOnaraModal = openOnaraModal;
const updateOnaraActiveItem = (itemid, url, forceScroll = false, slug = null) => {
if (!isOnaraActive()) return null;
// Clear onara-active from any and all elements to guarantee only 1 item is selected
document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active'));
let targetThumb = null;
if (url) {
try {
const parsedPath = new URL(url, window.location.origin).pathname;
targetThumb = document.querySelector(`.posts > a.thumb[href="${parsedPath}"], .posts > a.thumb[href="${url}"]`);
} catch {}
}
if (!targetThumb && slug) {
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${slug}"]`);
}
if (!targetThumb && itemid) {
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${itemid}"], .posts > a.thumb[data-bg*="/${itemid}."]`);
}
// Blur any other active element so browser native focus cannot highlight a second thumbnail
if (document.activeElement && document.activeElement !== targetThumb && typeof document.activeElement.blur === 'function') {
document.activeElement.blur();
}
if (targetThumb) {
targetThumb.classList.add('onara-active');
const rect = targetThumb.getBoundingClientRect();
const navbarH = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--navbar-h')) || 50;
const vh = window.innerHeight || document.documentElement.clientHeight;
// If any part of the thumbnail is already visible in the viewport, do not scroll
const isVisible = rect.bottom > navbarH && rect.top < vh;
if (!isVisible || forceScroll) {
targetThumb.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
}
}
return targetThumb;
};
window.updateOnaraActiveItem = updateOnaraActiveItem;
let _onaraSyncSeq = 0;
const syncOnaraBackgroundGrid = async (itemid, url, knownPage = null, slug = null) => {
if (!isOnaraActive()) return;
// Fast path: if target item is already in current background DOM, just highlight and ensure visibility
const existingThumb = updateOnaraActiveItem(itemid, url, false, slug);
if (existingThumb) {
return;
}
const currentSeq = ++_onaraSyncSeq;
const urlObj = new URL(url || window.location.href, window.location.origin);
const feedBasePath = urlObj.pathname.replace(/\/+$/, '').replace(/\/(?:\d+|[a-zA-Z0-9_-]{11})$/, '') || '/';
let targetPage = (typeof knownPage === 'number' && knownPage >= 1) ? knownPage : null;
if (!targetPage) {
try {
const pageParams = new URLSearchParams();
pageParams.set('id', itemid);
if (feedBasePath.startsWith('/tag/')) {
pageParams.set('tag', decodeURIComponent(feedBasePath.replace(/^\/tag\//, '')));
} else if (feedBasePath.startsWith('/h/')) {
pageParams.set('hall', decodeURIComponent(feedBasePath.replace(/^\/h\//, '')));
} else if (feedBasePath.match(/^\/user\/([^/]+)\/hall\/([^/]+)/)) {
const m = feedBasePath.match(/^\/user\/([^/]+)\/hall\/([^/]+)/);
pageParams.set('userHallOwner', decodeURIComponent(m[1]));
pageParams.set('userHall', decodeURIComponent(m[2]));
} else if (feedBasePath.match(/^\/user\/([^/]+)\/favs/)) {
const m = feedBasePath.match(/^\/user\/([^/]+)\/favs/);
pageParams.set('user', decodeURIComponent(m[1]));
pageParams.set('fav', 'true');
} else if (feedBasePath.match(/^\/user\/([^/]+)/)) {
const m = feedBasePath.match(/^\/user\/([^/]+)/);
pageParams.set('user', decodeURIComponent(m[1]));
} else if (feedBasePath === '/favs') {
pageParams.set('fav', 'true');
}
if (typeof window.activeMode !== 'undefined') {
pageParams.set('mode', window.activeMode);
}
const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true') || urlObj.searchParams.get('strict') === '1';
if (isStrict) {
pageParams.set('strict', '1');
}
const resp = await fetch(`/api/v2/item-page?${pageParams.toString()}`);
if (resp.ok) {
const pageData = await resp.json();
if (pageData && pageData.success && pageData.page) {
targetPage = pageData.page;
}
}
} catch (e) {
console.warn('[ONARA] Failed to fetch item-page:', e);
}
}
if (currentSeq !== _onaraSyncSeq) return;
if (!targetPage || targetPage < 1) targetPage = 1;
const pagePath = targetPage === 1 ? feedBasePath : (feedBasePath === '/' ? `/p/${targetPage}` : `${feedBasePath}/p/${targetPage}`);
const searchParams = new URLSearchParams(urlObj.search);
const searchStr = searchParams.toString() ? `?${searchParams.toString()}` : '';
const pageUrl = pagePath + searchStr;
try {
const pageRes = await fetch(pageUrl, {
headers: { 'X-Requested-With': 'XMLHttpRequest' }
});
if (currentSeq !== _onaraSyncSeq) return;
if (!pageRes.ok) return;
const html = await pageRes.text();
if (currentSeq !== _onaraSyncSeq) return;
const doc = new DOMParser().parseFromString(html, 'text/html');
const newPosts = doc.querySelector('.posts');
const oldPosts = document.querySelector('.posts');
if (newPosts && oldPosts) {
oldPosts.innerHTML = newPosts.innerHTML;
oldPosts.dataset.currentPage = targetPage;
if (newPosts.dataset.hasMore) {
oldPosts.dataset.hasMore = newPosts.dataset.hasMore;
}
if (typeof window.initLazyLoading === 'function') {
window.initLazyLoading();
}
}
const newPag = doc.querySelector('.pagination-wrapper');
const oldPag = document.querySelector('.pagination-wrapper');
if (newPag && oldPag) {
oldPag.innerHTML = newPag.innerHTML;
}
const newTitle = doc.querySelector('.page-title-wrap, .page-title, .user-profile-header, .hall-header');
const oldTitle = document.querySelector('.page-title-wrap, .page-title, .user-profile-header, .hall-header');
if (newTitle && oldTitle) {
oldTitle.replaceWith(newTitle);
}
window._onaraCurrentGridUrl = pageUrl;
window._onaraReturnUrl = pageUrl;
if (doc.title) {
window._onaraReturnTitle = doc.title;
}
const thumb = updateOnaraActiveItem(itemid, url, true, slug);
if (!thumb && oldPosts) {
const synthThumb = document.createElement('a');
synthThumb.href = url;
synthThumb.className = 'thumb lazy-thumb onara-active';
synthThumb.dataset.bg = `/t/${itemid}.webp`;
synthThumb.dataset.size = '1';
synthThumb.innerHTML = '';
oldPosts.prepend(synthThumb);
if (typeof window.initLazyLoading === 'function') window.initLazyLoading();
synthThumb.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
}
} catch (err) {
console.error('[ONARA] Failed to sync background grid:', err);
}
};
window.syncOnaraBackgroundGrid = syncOnaraBackgroundGrid;
const isItemPath = (pathOrUrl) => {
try {
const p = new URL(pathOrUrl, window.location.origin).pathname;
const parts = p.split('/').filter(Boolean);
return !p.match(/\/p\//) && (
/^\/\d+/.test(p) ||
/^\/[a-zA-Z0-9_-]{11}(?:[?#]|$)/.test(p) ||
(parts.length >= 3 && (parts[0] === 'tag' || parts[0] === 'user' || parts[0] === 'h') && (/^\d+$/.test(parts[parts.length - 1]) || /^[a-zA-Z0-9_-]{11}$/.test(parts[parts.length - 1])))
);
} catch {
return false;
}
};
window.isItemPath = isItemPath;
const getOnaraBaseUrl = (itemUrl = window.location.href) => {
try {
const urlObj = new URL(itemUrl, window.location.origin);
let path = urlObj.pathname.replace(/\/+$/, '');
path = path.replace(/\/(?:\d+|[a-zA-Z0-9_-]{11})$/, '');
// Check current grid URL if available and matching base path
if (window._onaraCurrentGridUrl) {
try {
const gridObj = new URL(window._onaraCurrentGridUrl, window.location.origin);
const gridBasePath = gridObj.pathname.replace(/\/p\/\d+/, '').replace(/\/+$/, '');
if (gridBasePath === path) {
return gridObj.pathname + (gridObj.search || '') + (gridObj.hash || '');
}
} catch {}
}
const postsEl = document.querySelector('.posts');
const activePagSpan = document.querySelector('.pagination span.btn.disabled, .pagination span.disabled');
const pagPage = activePagSpan ? parseInt(activePagSpan.textContent.trim(), 10) : 0;
const currentPage = (postsEl && postsEl.dataset.currentPage)
? parseInt(postsEl.dataset.currentPage, 10)
: (pagPage || postsEl?._infiniteState?.currentPage || postsEl?._infiniteState?.visiblePage || 1);
if (currentPage > 1 && !path.includes('/p/')) {
path = (path ? path : '') + '/p/' + currentPage;
}
if (!path) path = '/';
return path + (urlObj.search || '') + (urlObj.hash || '');
} catch {
return '/';
}
};
window.getOnaraBaseUrl = getOnaraBaseUrl;
const promoteOnaraModals = () => {
const mount = document.getElementById('onara-item-mount');
if (!mount) return;
const modals = mount.querySelectorAll('#info-modal, #visibility-modal, .modal-overlay');
modals.forEach(m => {
const existing = document.body.querySelector(`:scope > #${m.id}`);
if (existing && existing !== m) {
existing.remove();
}
document.body.appendChild(m);
});
};
window.promoteOnaraModals = promoteOnaraModals;
const closeOnaraModal = (options = {}) => {
const modal = document.getElementById('onara-modal');
if (!modal || modal.style.display === 'none' || modal._isClosing) return;
modal._isClosing = true;
// Close any auxiliary modals that may be open
const infoModal = document.getElementById('info-modal');
if (infoModal) infoModal.style.display = 'none';
const visModal = document.getElementById('visibility-modal');
if (visModal) visModal.style.display = 'none';
const reportModal = document.getElementById('report-modal');
if (reportModal) reportModal.style.display = 'none';
const hallsModal = document.getElementById('halls-modal');
if (hallsModal) hallsModal.style.display = 'none';
if (typeof window.closeImageModal === 'function') window.closeImageModal();
document.body.classList.remove('modal-open');
// Pause any playing media immediately
modal.querySelectorAll('video, audio').forEach(el => { try { el.pause(); } catch {} });
// Smooth fade out
modal.classList.remove('onara-fade-in');
modal.classList.add('onara-fade-out');
document.body.classList.remove('onara-modal-open');
// Restore background canvas if needed
const canvas = document.getElementById('bg');
if (canvas) {
canvas.classList.remove('fader-out', 'fast-fade');
if (window.initBackground) window.initBackground();
}
// Restore pagination container visibility on background grid
document.querySelectorAll('.pagination-container-fluid').forEach(el => {
const pagWrapper = el.querySelector('.pagination-wrapper');
if (!pagWrapper || pagWrapper.innerHTML.trim().length > 0) {
el.style.display = '';
}
});
if (!options.skipHistory) {
const returnUrl = window._onaraReturnUrl || getOnaraBaseUrl();
const returnTitle = window._onaraReturnTitle || window.f0ckDomain || '';
const resolvedReturn = new URL(returnUrl, window.location.origin).href;
if (window.location.href !== resolvedReturn) {
history.pushState({ onaraGrid: true }, '', returnUrl);
}
if (returnTitle) {
document.title = returnTitle;
}
}
window._onaraReturnUrl = null;
window._onaraReturnTitle = null;
// Maintain focus on active item so user TAB navigation seamlessly resumes from current progress
const activeThumb = document.querySelector('.posts > a.thumb.onara-active');
document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active'));
if (activeThumb) {
try {
activeThumb.focus({ preventScroll: true });
} catch {}
}
if (modal._closeTimeout) clearTimeout(modal._closeTimeout);
if (options.immediate) {
stopMedia();
const mount = document.getElementById('onara-item-mount');
if (mount) mount.innerHTML = '';
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
modal.classList.remove('onara-fade-out', 'onara-fade-in');
modal._isClosing = false;
modal._closeTimeout = null;
return;
}
modal._closeTimeout = setTimeout(() => {
if (!modal._isClosing) return;
stopMedia();
const mount = document.getElementById('onara-item-mount');
if (mount) mount.innerHTML = '';
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
modal.classList.remove('onara-fade-out');
modal._isClosing = false;
modal._closeTimeout = null;
}, 240);
};
window.closeOnaraModal = closeOnaraModal;
window.isOnaraActive = isOnaraActive;
// Content selector for protected elements inside onara modal
const _onaraContentSelector = [
'.media-object',
'.previous-post',
'.next-post',
'.arrow-prev',
'.arrow-next',
'#prev',
'#next',
'.nav-prev',
'.nav-next',
'#random',
'.steuerung',
'.location',
'.item_title',
'.blahlol',
'#comments-container',
'.tag-controls',
'.sidebar-tags-container',
'#tags',
'.rating-tag',
'.rating-label',
'.item-sidebar-left',
'.xd-score-wrapper',
'.user-infobox-block',
'a',
'button',
'input',
'textarea',
'select',
'label',
'form',
'video',
'audio',
'img',
'canvas',
'iframe',
'.iconset',
'.badge',
'.btn',
'time',
'[role="button"]',
'i[class*="fa-"]'
].join(', ');
let _onaraMouseDownInside = false;
document.addEventListener('mousedown', (e) => {
if (!document.body.classList.contains('onara-modal-open')) return;
if (e.target.closest('.modal-overlay:not(#onara-modal), .modal-content, .swal2-container, .tippy-box, #comments-container .emoji-picker, .dropdown-menu, .custom-confirm-overlay') ||
e.target.closest('.global-sidebar-right, nav.navbar, #sidebar-drag-zone') ||
e.target.closest(_onaraContentSelector)) {
_onaraMouseDownInside = true;
} else {
_onaraMouseDownInside = false;
}
}, { passive: true });
// Onara modal close click delegation: clicking on empty/free space closes the modal
document.addEventListener('click', (e) => {
if (!document.body.classList.contains('onara-modal-open')) return;
// If mouse press originated on a protected element or if text is currently selected, do not close
if (_onaraMouseDownInside) {
_onaraMouseDownInside = false;
return;
}
const sel = window.getSelection();
if (sel && sel.toString().trim().length > 0) {
return;
}
// Do not close if clicking auxiliary modals, dropdowns, tooltips, or emoji pickers
if (e.target.closest('.modal-overlay:not(#onara-modal), .modal-content, .swal2-container, .tippy-box, #comments-container .emoji-picker, .dropdown-menu, .custom-confirm-overlay')) return;
// Do not close if clicking the global sidebar right or navbar
if (e.target.closest('.global-sidebar-right, nav.navbar, #sidebar-drag-zone')) return;
// Do not close if clicking interactive content elements or actual item components
if (e.target.closest(_onaraContentSelector)) return;
// Any click on free space (backdrop, modal wrapper, container background, empty space in item-main-content) closes
if (e.target.closest('#onara-modal') || e.target.id === 'onara-backdrop') {
e.preventDefault();
closeOnaraModal();
}
});
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.banned) {
if (loginModal) closeModal(loginModal);
window.location.href = json.redirect || '/banned';
return;
}
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 imageModal = document.getElementById('image-modal');
if (imageModal && (imageModal.classList.contains('visible') || imageModal.style.display === 'flex' || imageModal.style.display === 'block')) {
if (typeof window.closeImageModal === 'function') window.closeImageModal();
else {
imageModal.classList.remove('visible');
imageModal.style.display = 'none';
document.body.classList.remove('modal-open');
}
return;
}
const infoModal = document.getElementById('info-modal');
if (infoModal && infoModal.style.display !== 'none') {
infoModal.style.display = 'none';
document.body.classList.remove('modal-open');
return;
}
const visModal = document.getElementById('visibility-modal');
if (visModal && visModal.style.display !== 'none') {
visModal.style.display = 'none';
document.body.classList.remove('modal-open');
return;
}
const reportModal = document.getElementById('report-modal');
if (reportModal && reportModal.style.display !== 'none') {
reportModal.style.display = 'none';
document.body.classList.remove('modal-open');
return;
}
const hallsModal = document.getElementById('halls-modal');
if (hallsModal && hallsModal.style.display !== 'none') {
hallsModal.style.display = 'none';
document.body.classList.remove('modal-open');
return;
}
const anyOtherModal = document.querySelector('.modal-overlay:not(#onara-modal):not([style*="display: none"]), .modal.show, .swal2-container');
if (anyOtherModal && window.getComputedStyle(anyOtherModal).display !== 'none') {
anyOtherModal.style.display = 'none';
document.body.classList.remove('modal-open');
return;
}
const tagAcWrapper = document.querySelector('.tag-ac-wrapper');
if (tagAcWrapper || (window.TagAutocomplete && typeof window.TagAutocomplete.isOpen === 'function' && window.TagAutocomplete.isOpen())) {
if (window.TagAutocomplete && typeof window.TagAutocomplete.destroy === 'function') {
window.TagAutocomplete.destroy();
} else if (tagAcWrapper) {
tagAcWrapper.remove();
}
return;
}
const searchOverlay = document.getElementById('search-overlay');
if (searchOverlay && searchOverlay.classList.contains('visible')) {
const closeBtn = document.getElementById('search-close');
if (closeBtn) closeBtn.click();
return;
}
const excludedTagsOverlay = document.getElementById('excluded-tags-overlay');
if (excludedTagsOverlay && excludedTagsOverlay.classList.contains('visible')) {
const closeBtn = document.getElementById('excluded-tags-close');
if (closeBtn) closeBtn.click();
return;
}
if (document.body.classList.contains('onara-modal-open')) {
closeOnaraModal();
}
}
});
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 _preloadedMediaUrls = new Set();
let _prefetchAbortController = null;
let _prefetchDebounceTimer = null;
const cancelPrefetch = () => {
if (_prefetchDebounceTimer) {
clearTimeout(_prefetchDebounceTimer);
_prefetchDebounceTimer = null;
}
if (_prefetchAbortController) {
try {
_prefetchAbortController.abort();
} catch (_) {}
_prefetchAbortController = null;
}
};
const preloadMediaFromHtml = (html) => {
if (!html) return;
try {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// 1. Preload Image only (standard image pipeline, non-blocking)
const img = doc.querySelector('#f0ck-image, .embed-responsive-image img, .media-object img');
if (img) {
const src = img.getAttribute('src');
if (src && !_preloadedMediaUrls.has(src)) {
_preloadedMediaUrls.add(src);
const preloadImg = new Image();
preloadImg.decoding = 'async';
preloadImg.src = src;
window.f0ckDebug('[prefetch] Preloaded image asset:', src);
}
}
// 2. Preload Audio Cover Art
const cover = doc.querySelector('#f0ck-audio-cover');
if (cover) {
const coverSrc = cover.getAttribute('src');
if (coverSrc && coverSrc !== '/s/img/200.gif' && !_preloadedMediaUrls.has(coverSrc)) {
_preloadedMediaUrls.add(coverSrc);
const preloadCover = new Image();
preloadCover.src = coverSrc;
}
}
// Note: Video & Audio binary streams are deliberately NOT fetched via fetch() in JS.
// HTML5 media elements stream natively upon mounting. Fetching multi-megabyte chunks
// in the background exhausts the browser's 6-connection HTTP pool, stalling navigation.
if (_preloadedMediaUrls.size > 150) {
_preloadedMediaUrls.clear();
}
} catch (e) {
console.error('[prefetch] Error preloading media from html:', e);
}
};
const prefetchNextPrevItems = () => {
// Abort prior in-flight prefetch requests
if (_prefetchAbortController) {
try { _prefetchAbortController.abort(); } catch (_) {}
}
_prefetchAbortController = new AbortController();
const signal = _prefetchAbortController.signal;
// Prioritize next item (most frequent user journey), then prev
const nextBtn = document.getElementById('next');
const prevBtn = document.getElementById('prev');
[nextBtn, prevBtn].forEach(btn => {
if (!btn || !btn.href || btn.href.endsWith('#')) return;
if (signal.aborted) return;
const pathSegments = new URL(btn.href, window.location.origin).pathname.split('/');
const keySegments = pathSegments.filter(s => /^\d+$/.test(s) || /^[a-zA-Z0-9_-]{11}$/.test(s));
if (keySegments.length === 0) return;
const itemId = keySegments.pop();
let ajaxUrl = `/ajax/item/${itemId}`;
const params = new URLSearchParams();
params.append('mode', window.activeMode);
const tagMatch = btn.href.match(/\/tag\/([^/?]+)/);
if (tagMatch) params.append('tag', decodeURIComponent(tagMatch[1]));
const userHallMatch = btn.href.match(/\/user\/([^/]+)\/hall\/([^/]+)/);
if (userHallMatch) {
params.append('userHall', decodeURIComponent(userHallMatch[2]));
params.append('userHallOwner', decodeURIComponent(userHallMatch[1]));
} else {
const userMatch = btn.href.match(/\/user\/([^/]+)/);
if (userMatch) {
params.append('user', decodeURIComponent(userMatch[1]));
if (btn.href.match(/\/user\/[^/]+\/favs(\/|$|\?)/)) params.append('fav', 'true');
}
}
const hallMatch = btn.href.match(/\/h\/([^/?]+)/);
if (hallMatch) params.append('hall', decodeURIComponent(hallMatch[1]));
const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true');
if (isStrict) params.append('strict', '1');
if (params.toString() !== '') {
ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + params.toString();
}
if (itemCacheMap.has(ajaxUrl)) {
const cached = itemCacheMap.get(ajaxUrl);
if (cached && cached.html) preloadMediaFromHtml(cached.html);
return;
}
fetch(ajaxUrl, { credentials: 'include', priority: 'low', signal })
.then(r => r.ok ? r.text() : null)
.then(rawText => {
if (!rawText || signal.aborted) return;
let html = rawText;
try {
const data = JSON.parse(rawText);
if (data && typeof data.html === 'string') html = data.html;
} catch (_) {}
if (html && !signal.aborted) {
itemCacheMap.set(ajaxUrl, { html, ts: Date.now() });
if (itemCacheMap.size > ITEM_CACHE_MAX) {
itemCacheMap.delete(itemCacheMap.keys().next().value);
}
window.f0ckDebug('[prefetch] Cached AJAX template for item', itemId);
preloadMediaFromHtml(html);
}
})
.catch(err => {
if (err.name !== 'AbortError') {
window.f0ckDebug('[prefetch] Prefetch error or cancelled:', err.message);
}
});
});
};
const schedulePrefetch = (delay = 600) => {
cancelPrefetch();
_prefetchDebounceTimer = setTimeout(() => {
_prefetchDebounceTimer = null;
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(() => prefetchNextPrevItems(), { timeout: 1500 });
} else {
prefetchNextPrevItems();
}
}, delay);
};
const setupMedia = () => {
const elem = document.querySelector("#my-video") || document.querySelector("audio#my-video");
if (elem) {
video = new v0ck(elem);
} else {
video = null;
}
};
const initOnaraInitialState = () => {
if (!isOnaraActive()) return;
promoteOnaraModals();
if (document.body.classList.contains('onara-modal-open')) {
const modal = openOnaraModal();
if (!window._onaraReturnUrl) {
window._onaraReturnUrl = getOnaraBaseUrl();
window._onaraReturnTitle = window.f0ckDomain || 'f0ck';
}
window._onaraCurrentGridUrl = window._onaraReturnUrl;
const pathSegments = window.location.pathname.split('/');
const keySegments = pathSegments.filter(s => /^\d+$/.test(s) || /^[a-zA-Z0-9_-]{11}$/.test(s));
const activeKey = keySegments.length ? keySegments[keySegments.length - 1] : null;
if (activeKey) {
updateOnaraActiveItem(activeKey, window.location.href);
if (typeof window.trackVisit === 'function') {
window.trackVisit(activeKey);
}
}
} else {
window._onaraCurrentGridUrl = window.location.pathname + window.location.search;
}
};
// Initial Load
document.addEventListener('DOMContentLoaded', () => {
setupMedia();
initOnaraInitialState();
schedulePrefetch(800);
});
initOnaraInitialState();
// 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');
// Draw at 1/4 resolution — the canvas is stretched to full-screen by CSS,
// so a smaller internal resolution is imperceptible for a blurred background.
// This reduces blur computation from O(W*H) to O(W/4 * H/4) = 1/16th the pixels.
const SCALE = 0.25;
const cw = canvas.width = Math.max(1, (canvas.clientWidth * SCALE) | 0);
const ch = canvas.height = Math.max(1, (canvas.clientHeight * SCALE) | 0);
// Blur radius scaled proportionally to the downsampled canvas size
const blurPx = Math.round(100 * SCALE) || 1;
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.filter = `blur(${blurPx}px)`;
context.drawImage(thumb, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
showCanvas();
// Step 2: upgrade with full image when it's ready (skip for AUDIO elements)
if (isDrawable) {
if (elem.complete) {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
} else {
elem.onload = () => {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
};
}
}
};
thumb.onerror = () => {
// Thumbnail failed — fall back to waiting for the main image (skip for AUDIO)
if (isDrawable) {
if (elem.complete) {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
showCanvas();
} else {
elem.onload = () => {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} 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.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
showCanvas();
} else {
elem.onload = () => {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} catch (e) {}
showCanvas();
};
}
}
};
const animationLoop = () => {
if (!elem || elem.tagName === 'AUDIO' || elem.paused || elem.ended || (!background && !canvas._bgFadingOut)) {
bgRafId = null;
return;
}
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(elem, 0, 0, cw, ch);
context.filter = 'none';
} 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 _SCALE = 0.25;
const cw = canvas.width = Math.max(1, (canvas.clientWidth * _SCALE) | 0);
const ch = canvas.height = Math.max(1, (canvas.clientHeight * _SCALE) | 0);
const blurPx = Math.round(100 * _SCALE) || 1;
const thumb = new Image();
thumb.onload = () => {
try {
context.filter = `blur(${blurPx}px)`;
context.drawImage(thumb, 0, 0, cw, ch);
context.filter = 'none';
} 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