${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) {
let displaySize = sub.size || '';
if (displaySize === 'NaN B' || displaySize === 'NaN') displaySize = '';
if (displaySize && !isNaN(displaySize)) {
const num = Number(displaySize);
if (num > 0) {
const i = Math.min(4, Math.max(0, ~~(Math.log(num) / Math.log(1024))));
displaySize = (num / Math.pow(1024, i)).toFixed(2) * 1 + ' ' + ['B', 'kB', 'MB', 'GB', 'TB'][i];
}
}
fileSizeEl.textContent = displaySize;
}
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');
});
}
let hasDraggedStrip = false;
if (stripEl && !stripEl._dragInitialized) {
stripEl._dragInitialized = true;
let isDown = false;
let startX = 0;
let startScrollLeft = 0;
let momentumID = null;
let velX = 0;
let lastX = 0;
let lastTime = 0;
stripEl.addEventListener('mousedown', (e) => {
if (e.button !== 0) return;
isDown = true;
hasDraggedStrip = false;
startX = e.pageX;
startScrollLeft = stripEl.scrollLeft;
lastX = e.pageX;
lastTime = performance.now();
velX = 0;
cancelAnimationFrame(momentumID);
});
window.addEventListener('mousemove', (e) => {
if (!isDown) return;
const now = performance.now();
const walk = e.pageX - startX;
if (Math.abs(walk) > 6) {
hasDraggedStrip = true;
stripEl.classList.add('is-dragging');
e.preventDefault();
stripEl.scrollLeft = startScrollLeft - walk;
const dt = Math.max(1, now - lastTime);
velX = (e.pageX - lastX) / dt;
lastX = e.pageX;
lastTime = now;
}
});
window.addEventListener('mouseup', () => {
if (!isDown) return;
isDown = false;
stripEl.classList.remove('is-dragging');
showStripPeek(6000);
if (hasDraggedStrip && Math.abs(velX) > 0.15) {
let currentVel = velX * 18;
const glide = () => {
if (Math.abs(currentVel) > 0.5) {
stripEl.scrollLeft -= currentVel;
currentVel *= 0.93;
momentumID = requestAnimationFrame(glide);
}
};
momentumID = requestAnimationFrame(glide);
}
setTimeout(() => {
hasDraggedStrip = false;
}, 100);
});
// Touch drag / swipe support for mobile
let touchStartX = 0;
let touchStartScrollLeft = 0;
let isTouchingStrip = false;
stripEl.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
e.stopPropagation();
if (stripTimeout) clearTimeout(stripTimeout);
container.classList.add('strip-peek');
stripEl.classList.add('strip-peek');
isTouchingStrip = true;
hasDraggedStrip = false;
touchStartX = e.touches[0].clientX;
touchStartScrollLeft = stripEl.scrollLeft;
lastX = e.touches[0].clientX;
lastTime = performance.now();
velX = 0;
cancelAnimationFrame(momentumID);
}
}, { passive: true });
stripEl.addEventListener('touchmove', (e) => {
if (!isTouchingStrip || e.touches.length !== 1) return;
e.stopPropagation();
const currentX = e.touches[0].clientX;
const walk = currentX - touchStartX;
if (Math.abs(walk) > 3) {
hasDraggedStrip = true;
stripEl.classList.add('is-dragging');
stripEl.scrollLeft = touchStartScrollLeft - walk;
const now = performance.now();
const dt = Math.max(1, now - lastTime);
velX = (currentX - lastX) / dt;
lastX = currentX;
lastTime = now;
}
}, { passive: true });
stripEl.addEventListener('touchend', (e) => {
if (!isTouchingStrip) return;
e.stopPropagation();
isTouchingStrip = false;
stripEl.classList.remove('is-dragging');
showStripPeek(6000);
if (hasDraggedStrip) {
if (Math.abs(velX) > 0.05) {
let currentVel = velX * 24;
const glide = () => {
if (Math.abs(currentVel) > 0.5) {
stripEl.scrollLeft -= currentVel;
currentVel *= 0.93;
momentumID = requestAnimationFrame(glide);
}
};
momentumID = requestAnimationFrame(glide);
}
setTimeout(() => {
hasDraggedStrip = false;
}, 150);
}
}, { passive: true });
stripEl.addEventListener('touchcancel', (e) => {
e.stopPropagation();
isTouchingStrip = false;
stripEl.classList.remove('is-dragging');
showStripPeek(6000);
setTimeout(() => {
hasDraggedStrip = false;
}, 100);
}, { passive: true });
stripEl.addEventListener('wheel', (e) => {
if (e.deltaY !== 0) {
e.preventDefault();
stripEl.scrollLeft += e.deltaY;
}
}, { passive: false });
}
thumbItems.forEach((btn) => {
btn.addEventListener('click', (e) => {
if (hasDraggedStrip) {
e.preventDefault();
e.stopPropagation();
return;
}
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
container.addEventListener('click', (e) => {
if (!e.target.closest('.v0ck_settings_menu, .v0ck_hud, .album-btn')) {
showStripPeek(3000);
}
});
// Handle touch swipe for mobile gallery navigation
let touchStartX = null;
let touchStartY = null;
let touchIgnored = false;
container.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
touchIgnored = !!e.target.closest('.album-thumbnails-strip, .v0ck_player_controls, .v0ck_settings_menu, .v0ck_hud, .album-btn');
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
}, { passive: true });
container.addEventListener('touchend', (e) => {
if (touchStartX === null || touchStartY === null || touchIgnored) {
touchStartX = null;
touchStartY = null;
touchIgnored = false;
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;
touchIgnored = false;
}, { 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();
// Dynamic YouTube ambient color timeline manager
let ytAmbientState = {
rafId: null,
videoId: null,
colors: null,
duration: 0,
currentTime: 0,
isPlaying: false,
lastTimeUpdate: 0,
currentColor: [0, 0, 0],
targetColor: [0, 0, 0],
cleanup: null
};
const stopYoutubeAmbient = () => {
if (ytAmbientState.rafId) {
window.cancelAnimFrame(ytAmbientState.rafId);
ytAmbientState.rafId = null;
}
if (ytAmbientState.cleanup) {
ytAmbientState.cleanup();
ytAmbientState.cleanup = null;
}
ytAmbientState.videoId = null;
ytAmbientState.colors = null;
ytAmbientState.isPlaying = false;
};
const initYoutubeAmbient = (ytEmbed, canvas, backgroundEnabled) => {
stopYoutubeAmbient();
if (!ytEmbed || !canvas || !backgroundEnabled) return;
const videoId = ytEmbed.dataset.ytId || (ytEmbed.src && (ytEmbed.src.match(/embed\/([a-zA-Z0-9_-]+)/) || [])[1]);
if (!videoId) return;
ytAmbientState.videoId = videoId;
const context = canvas.getContext('2d');
const _SCALE = 0.5;
const updateCanvasSize = () => {
const cw = Math.max(1, (canvas.clientWidth * _SCALE) | 0);
const ch = Math.max(1, (canvas.clientHeight * _SCALE) | 0);
if (canvas.width !== cw || canvas.height !== ch) {
canvas.width = cw;
canvas.height = ch;
}
return { cw: canvas.width, ch: canvas.height };
};
const { cw, ch } = updateCanvasSize();
const blurPx = Math.round(80 * _SCALE) || 1;
// 1. Draw static thumbnail immediately for instant background (with robust fallback)
const itemId = window.getCurrentItemId();
const tryDrawThumb = (url, fallbackUrl) => {
const img = new Image();
img.crossOrigin = 'anonymous';
img.onload = () => {
try {
context.filter = `blur(${blurPx}px) brightness(1.2)`;
const sw = img.naturalWidth || img.width;
const sh = img.naturalHeight || img.height;
if (sw > 0 && sh > 0) {
const scale = Math.max(canvas.width / sw, canvas.height / sh);
const dw = sw * scale;
const dh = sh * scale;
context.drawImage(img, (canvas.width - dw) / 2, (canvas.height - dh) / 2, dw, dh);
} else {
context.drawImage(img, 0, 0, canvas.width, canvas.height);
}
context.filter = 'none';
} catch (e) {}
canvas.classList.remove('fader-out', 'fast-fade');
canvas.classList.add('fader-in');
};
img.onerror = () => {
if (fallbackUrl && url !== fallbackUrl) {
tryDrawThumb(fallbackUrl, null);
} else {
canvas.classList.remove('fader-out', 'fast-fade');
canvas.classList.add('fader-in');
}
};
img.src = url;
};
const localThumb = itemId ? `/t/${itemId}.webp` : null;
const remoteThumb = `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;
tryDrawThumb(localThumb || remoteThumb, remoteThumb);
// 2. Fetch storyboard ambient colors
fetch(`/api/v2/ambient/yt/${videoId}`)
.then(res => res.ok ? res.json() : null)
.then(data => {
if (!data || !Array.isArray(data.colors) || !data.colors.length || ytAmbientState.videoId !== videoId) return;
ytAmbientState.colors = data.colors;
ytAmbientState.duration = Number(data.duration) || 0;
if (ytAmbientState.colors.length > 0) {
ytAmbientState.currentColor = [...ytAmbientState.colors[0]];
ytAmbientState.targetColor = [...ytAmbientState.colors[0]];
// Immediately paint the first frame color as ambient glow!
renderAmbientFrame();
canvas.classList.remove('fader-out', 'fast-fade');
canvas.classList.add('fader-in');
}
if (ytAmbientState.isPlaying) {
startAmbientLoop();
}
})
.catch(() => {});
// 3. Render function for ambient lighting
const renderAmbientFrame = () => {
if (!ytAmbientState.colors || !ytAmbientState.colors.length || !ytAmbientState.duration) return;
const { cw: curW, ch: curH } = updateCanvasSize();
let t = ytAmbientState.currentTime;
if (ytAmbientState.isPlaying && ytAmbientState.lastTimeUpdate > 0) {
const dt = (performance.now() - ytAmbientState.lastTimeUpdate) / 1000;
t = Math.min(ytAmbientState.duration, t + dt);
}
const norm = Math.max(0, Math.min(1, t / ytAmbientState.duration));
const pos = norm * (ytAmbientState.colors.length - 1);
const idx = Math.floor(pos);
const next = Math.min(ytAmbientState.colors.length - 1, idx + 1);
const frac = pos - idx;
const c1 = ytAmbientState.colors[idx];
const c2 = ytAmbientState.colors[next];
ytAmbientState.targetColor = [
c1[0] + (c2[0] - c1[0]) * frac,
c1[1] + (c2[1] - c1[1]) * frac,
c1[2] + (c2[2] - c1[2]) * frac
];
// Smooth lerp
ytAmbientState.currentColor[0] += (ytAmbientState.targetColor[0] - ytAmbientState.currentColor[0]) * 0.15;
ytAmbientState.currentColor[1] += (ytAmbientState.targetColor[1] - ytAmbientState.currentColor[1]) * 0.15;
ytAmbientState.currentColor[2] += (ytAmbientState.targetColor[2] - ytAmbientState.currentColor[2]) * 0.15;
const r = Math.min(255, Math.max(0, Math.round(ytAmbientState.currentColor[0])));
const g = Math.min(255, Math.max(0, Math.round(ytAmbientState.currentColor[1])));
const b = Math.min(255, Math.max(0, Math.round(ytAmbientState.currentColor[2])));
const ctx = canvas.getContext('2d');
if (!ctx) return;
// Render radial ambient wash over canvas
ctx.save();
const cx = curW / 2;
const cy = curH / 2;
const maxR = Math.max(curW, curH) * 0.85;
const grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, maxR);
grad.addColorStop(0, `rgba(${r}, ${g}, ${b}, 0.95)`);
grad.addColorStop(0.4, `rgba(${Math.round(r * 0.75)}, ${Math.round(g * 0.75)}, ${Math.round(b * 0.75)}, 0.7)`);
grad.addColorStop(0.8, `rgba(${Math.round(r * 0.3)}, ${Math.round(g * 0.3)}, ${Math.round(b * 0.3)}, 0.4)`);
grad.addColorStop(1, 'rgba(5, 5, 10, 0.95)');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, curW, curH);
ctx.restore();
};
const startAmbientLoop = () => {
if (ytAmbientState.rafId) return;
const loop = () => {
if (!ytAmbientState.isPlaying || ytAmbientState.videoId !== videoId) {
ytAmbientState.rafId = null;
return;
}
renderAmbientFrame();
ytAmbientState.rafId = window.requestAnimFrame(loop);
};
ytAmbientState.rafId = window.requestAnimFrame(loop);
};
// 4. Connect to YouTube player via official API and postMessage
const sendPostMsg = (msg) => {
if (!ytEmbed.contentWindow) return;
try {
ytEmbed.contentWindow.postMessage(typeof msg === 'string' ? msg : JSON.stringify(msg), '*');
} catch (e) {}
};
const registerListeners = () => {
// YouTube postMessage handshake
sendPostMsg({ event: 'listening', id: ytEmbed.id || 1 });
sendPostMsg({ event: 'command', func: 'addEventListener', args: ['onStateChange'] });
};
ytEmbed.addEventListener('load', registerListeners);
registerListeners();
setTimeout(registerListeners, 500);
setTimeout(registerListeners, 1500);
// Also attach YouTube IFrame API if available
const tryAttachYTPlayer = () => {
if (window.YT && window.YT.Player) {
try {
new window.YT.Player(ytEmbed, {
events: {
onStateChange: (event) => {
if (event.data === 1) { // PLAYING
ytAmbientState.isPlaying = true;
startAmbientLoop();
} else if (event.data === 2 || event.data === 0) {
ytAmbientState.isPlaying = false;
}
}
}
});
} catch (e) {}
}
};
if (window.YT && window.YT.Player) {
tryAttachYTPlayer();
} else if (!document.querySelector('script[src*="youtube.com/iframe_api"]')) {
const tag = document.createElement('script');
tag.src = "https://www.youtube.com/iframe_api";
tag.onload = () => setTimeout(tryAttachYTPlayer, 200);
document.head.appendChild(tag);
} else {
setTimeout(tryAttachYTPlayer, 1000);
}
const onMessage = (event) => {
if (typeof event.origin !== 'string' || !event.origin.includes('youtube.com')) return;
let data;
try {
data = typeof event.data === 'string' ? JSON.parse(event.data) : event.data;
} catch (e) {
return;
}
if (!data) return;
let state = undefined;
if (data.event === 'onStateChange') {
state = data.info;
} else if (data.event === 'infoDelivery' && data.info && data.info.playerState !== undefined) {
state = data.info.playerState;
}
if (state === 1) { // PLAYING
ytAmbientState.isPlaying = true;
startAmbientLoop();
} else if (state === 2 || state === 0) { // PAUSED or ENDED
ytAmbientState.isPlaying = false;
}
if (data.event === 'infoDelivery' && data.info && typeof data.info.currentTime === 'number') {
ytAmbientState.currentTime = data.info.currentTime;
ytAmbientState.lastTimeUpdate = performance.now();
if (!ytAmbientState.isPlaying) {
renderAmbientFrame();
}
}
};
window.addEventListener('message', onMessage);
ytAmbientState.cleanup = () => {
window.removeEventListener('message', onMessage);
ytEmbed.removeEventListener('load', registerListeners);
};
};
// Destroy / stop background canvas instance and animation loops
window.destroyBackgroundInstance = () => {
if (bgRafId) {
window.cancelAnimFrame(bgRafId);
bgRafId = null;
}
if (visualizerRafId) {
window.cancelAnimFrame(visualizerRafId);
visualizerRafId = null;
}
stopYoutubeAmbient();
const canvas = document.getElementById('bg');
if (canvas) {
canvas._bgFadingOut = false;
canvas.classList.remove('fader-in');
canvas.classList.add('fader-out');
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
}
};
// Export init function for dynamic calls
window.initBackground = () => {
// Media selection priority
let elem = null;
if (document.body.classList.contains('onara-modal-open')) {
const mount = document.getElementById('onara-item-mount');
if (mount) {
// For albums: prioritize visible video/audio over the always-present