${subSlug} (${currentIndex + 1}/${albumData.length}):`;
}
const subTags = Array.isArray(sub.tags) ? sub.tags : [];
if (typeof window.renderTags === 'function') {
window.renderTags(subTags);
}
};
const updateInfoModal = () => {
const modal = document.getElementById('info-modal');
if (!modal) return;
const sub = albumData[currentIndex];
if (!sub) return;
const subHeading = modal.querySelector('#info-modal-subheading');
if (subHeading) {
const parentId = subHeading.getAttribute('data-item-id') || container.getAttribute('data-album-id') || '';
const parentSlug = subHeading.getAttribute('data-item-slug') || '';
const subSlug = sub.slug || sub.subf0ck_id || sub.id;
const total = albumData.length;
const idx = currentIndex + 1;
subHeading.innerHTML = `Post ID: ${parentId}${parentSlug ? ` (${parentSlug})` : ''} • Subf0ck: ${subSlug} (${idx}/${total})`;
}
const specsTitle = modal.querySelector('#info-specs-header-title');
if (specsTitle) {
specsTitle.textContent = `Technical Specifications (Subf0ck ${currentIndex + 1}/${albumData.length})`;
}
const fileSizeEl = modal.querySelector('#info-file-size');
if (fileSizeEl) {
fileSizeEl.textContent = sub.size || '';
}
const dimsCard = modal.querySelector('#info-card-dimensions');
const dimsEl = modal.querySelector('#info-file-dimensions');
if (dimsCard && dimsEl) {
if (sub.width && sub.height) {
dimsEl.textContent = `${sub.width} × ${sub.height} px`;
dimsCard.style.display = '';
} else {
dimsCard.style.display = 'none';
}
}
const mimeEl = modal.querySelector('#info-file-mime');
if (mimeEl) {
mimeEl.textContent = sub.mime || '';
}
const directLink = modal.querySelector('#info-file-direct-link');
if (directLink) {
directLink.href = sub.dest || sub.src || '#';
}
const sourceCard = modal.querySelector('#info-card-source');
if (sourceCard) {
sourceCard.style.display = 'none';
}
const hashCard = modal.querySelector('#info-card-hash');
const hashEl = modal.querySelector('#info-file-hash');
const copyHashBtn = modal.querySelector('#info-copy-hash-btn');
if (hashCard) {
const cleanHash = sub.checksum ? String(sub.checksum).split('_bypass_')[0] : '';
if (cleanHash) {
if (hashEl) hashEl.textContent = cleanHash;
if (copyHashBtn) copyHashBtn.setAttribute('data-hash', cleanHash);
hashCard.style.display = '';
} else {
hashCard.style.display = 'none';
}
}
};
if (imgEl) {
imgEl.addEventListener('load', () => {
const sub = albumData[currentIndex];
if (sub && (!sub.width || !sub.height) && imgEl.naturalWidth) {
sub.width = imgEl.naturalWidth;
sub.height = imgEl.naturalHeight;
updateInfoModal();
}
});
}
if (videoEl) {
videoEl.addEventListener('loadedmetadata', () => {
const sub = albumData[currentIndex];
if (sub && (!sub.width || !sub.height) && videoEl.videoWidth) {
sub.width = videoEl.videoWidth;
sub.height = videoEl.videoHeight;
updateInfoModal();
}
});
}
// Ensure proper initial media display (especially if initial item is video or subf0ck hash was requested)
showImage(currentIndex, 'none', !!initialHash);
if (prevBtn) {
prevBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
showImage(currentIndex - 1, 'prev');
});
}
if (nextBtn) {
nextBtn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
showImage(currentIndex + 1, 'next');
});
}
thumbItems.forEach((btn) => {
btn.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
const targetIdx = parseInt(btn.getAttribute('data-index'), 10);
if (!isNaN(targetIdx)) {
showImage(targetIdx);
}
});
});
// Touch tap on container reveals thumbnail strip briefly on touch devices
let stripTimeout = null;
container.addEventListener('click', (e) => {
if (stripEl && !e.target.closest('.album-thumbnails-strip, .v0ck_player_controls, .v0ck_settings_menu, .v0ck_hud, .album-btn')) {
stripEl.classList.add('strip-peek');
if (stripTimeout) clearTimeout(stripTimeout);
stripTimeout = setTimeout(() => {
stripEl.classList.remove('strip-peek');
}, 3000);
}
});
// Handle touch swipe for mobile gallery navigation
let touchStartX = null;
let touchStartY = null;
container.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
}, { passive: true });
container.addEventListener('touchend', (e) => {
if (touchStartX === null || touchStartY === null) return;
const touchEndX = e.changedTouches[0].clientX;
const touchEndY = e.changedTouches[0].clientY;
const diffX = touchEndX - touchStartX;
const diffY = touchEndY - touchStartY;
// Minimum swipe distance of 50px and predominantly horizontal
if (Math.abs(diffX) > 50 && Math.abs(diffX) > Math.abs(diffY) * 1.5) {
if (diffX < 0) {
showImage(currentIndex + 1, 'next');
} else {
showImage(currentIndex - 1, 'prev');
}
}
touchStartX = null;
touchStartY = null;
}, { passive: true });
// Handle hash change if user navigates back/forward to another subf0ck
window.addEventListener('hashchange', () => {
const newHash = getHashSubf0ckId();
if (!newHash) return;
const foundIdx = albumData.findIndex((item) =>
String(item.slug || '') === newHash ||
String(item.subf0ck_id || '') === newHash ||
String(item.id) === newHash ||
String(item.order_index + 1) === newHash
);
if (foundIdx !== -1 && foundIdx !== currentIndex) {
showImage(foundIdx, 'none', false);
}
});
window._currentActiveAlbumGallery = {
prev: () => showImage(currentIndex - 1, 'prev'),
next: () => showImage(currentIndex + 1, 'next'),
showBySlug: (subKey) => {
if (!subKey || !Array.isArray(albumData)) return false;
const foundIdx = albumData.findIndex((item) =>
String(item.slug || '') === String(subKey) ||
String(item.subf0ck_id || '') === String(subKey) ||
String(item.id) === String(subKey) ||
String(item.order_index + 1) === String(subKey)
);
if (foundIdx !== -1) {
showImage(foundIdx, 'none', true);
return true;
}
return false;
},
updateInfoModal: updateInfoModal,
updateAlbumTags: updateAlbumTags,
getCurrentSubf0ck: () => albumData[currentIndex],
isHovered: false
};
window.albumGallery = window._currentActiveAlbumGallery;
container.addEventListener('mouseenter', () => {
if (window._currentActiveAlbumGallery) window._currentActiveAlbumGallery.isHovered = true;
});
container.addEventListener('mouseleave', () => {
if (window._currentActiveAlbumGallery) window._currentActiveAlbumGallery.isHovered = false;
});
};
const syncLocationSubf0ck = () => {
const layout = document.querySelector('.item-layout-container');
const reqSub = layout ? (layout.getAttribute('data-requested-subf0ck') || '') : '';
const hash = (window.location.hash || '').replace(/^#/, '').trim();
const targetSub = hash || reqSub;
if (targetSub) {
if (!hash && reqSub) {
history.replaceState(null, '', window.location.pathname + window.location.search + '#' + reqSub);
}
const locEl = document.querySelector('.location');
if (locEl && !locEl.textContent.includes('#')) {
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + targetSub;
}
}
};
const setupMedia = () => {
window._currentActiveAlbumGallery = null;
window.albumGallery = null;
const elem = document.querySelector("#my-video") || document.querySelector("audio#my-video");
if (elem) {
video = new v0ck(elem);
} else {
video = null;
}
initAlbumGallery();
syncLocationSubf0ck();
};
document.addEventListener('f0ck:contentLoaded', () => {
initAlbumGallery();
syncLocationSubf0ck();
});
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);
}
}
};
// Audio Visualizer Reactivity Tuner
const DEFAULT_AUDIO_TUNING = {
bassGain: 0.5,
bassPower: 2.0,
scaleBounce: 0.4,
bounceBoost: 2.5,
glowIntensity: 250,
attackSpeed: 1.0,
releaseSpeed: 0.6,
coverSize: 155,
barHeight: 0.4,
smoothing: 0.84
};
let savedTuning = null;
try {
const raw = localStorage.getItem('f0ck_audio_tuning');
if (raw) savedTuning = JSON.parse(raw);
} catch (e) {}
window.audioVisualizerTuning = Object.assign({}, DEFAULT_AUDIO_TUNING, savedTuning || {});
const initAudioTunerUI = () => {
if (document.getElementById('f0ck-audio-tuner-panel')) return;
const panel = document.createElement('div');
panel.id = 'f0ck-audio-tuner-panel';
panel.className = 'f0ck-audio-tuner-panel hidden';
const sliders = [
// Disc & Glow Section
{ section: 'Cover Art & Glow Reactivity', key: 'scaleBounce', label: 'Disc Bounce (Scale)', min: 0.00, max: 5.00, step: 0.05, unit: 'x' },
{ section: 'Cover Art & Glow Reactivity', key: 'bounceBoost', label: 'Bounce Reactivity Boost', min: 1.0, max: 30.0, step: 0.5, unit: 'x' },
{ section: 'Cover Art & Glow Reactivity', key: 'bassGain', label: 'Bass Gain (Multiplier)', min: 0.5, max: 6.0, step: 0.1, unit: 'x' },
{ section: 'Cover Art & Glow Reactivity', key: 'bassPower', label: 'Sensitivity Curve (Power)', min: 0.30, max: 2.00, step: 0.02, unit: '' },
{ section: 'Cover Art & Glow Reactivity', key: 'glowIntensity', label: 'Glow Aura Reach', min: 10, max: 250, step: 5, unit: 'px' },
{ section: 'Cover Art & Glow Reactivity', key: 'attackSpeed', label: 'Attack Speed (Snap)', min: 0.10, max: 1.00, step: 0.02, unit: '' },
{ section: 'Cover Art & Glow Reactivity', key: 'releaseSpeed', label: 'Release Speed (Decay)', min: 0.05, max: 0.60, step: 0.01, unit: '' },
{ section: 'Cover Art & Glow Reactivity', key: 'coverSize', label: 'Cover Disc Diameter', min: 120, max: 450, step: 5, unit: 'px' },
// Visualizer Bars Section
{ section: 'Bottom Visualizer Bars', key: 'barHeight', label: 'Visualizer Bar Height', min: 0.05, max: 2.00, step: 0.05, unit: 'x' },
{ section: 'Bottom Visualizer Bars', key: 'smoothing', label: 'Visualizer Bar Smoothing', min: 0.10, max: 0.95, step: 0.02, unit: '' }
];
let rowsHtml = '';
let currentSection = '';
sliders.forEach(s => {
if (s.section && s.section !== currentSection) {
currentSection = s.section;
rowsHtml += `