This commit is contained in:
2026-09-13 21:55:23 +02:00
parent 399f2be456
commit ba5bc18b98
32 changed files with 1739 additions and 270 deletions
+550 -88
View File
@@ -374,27 +374,49 @@ window.cancelAnimFrame = (function () {
// Update audio cover in player if viewing this audio item
const audioCover = document.getElementById('f0ck-audio-cover');
if (audioCover && currentId === idStr) {
const parent = audioCover.parentElement;
const audioEl = document.querySelector('audio#my-video');
let ph = parent ? parent.querySelector(':scope > .sidebar-media-placeholder.audio') : null;
if (parent && !ph) {
ph = document.createElement('div');
ph.className = 'sidebar-media-placeholder audio';
ph.innerHTML = '<i class="fa-solid fa-music"></i>';
parent.prepend(ph);
}
let coverCircle = ph ? ph.querySelector('.audio-cover-circle') : null;
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`;
parent.style.background = 'none';
if (ph) {
if (!coverCircle) {
coverCircle = document.createElement('div');
coverCircle.className = 'audio-cover-circle';
ph.insertBefore(coverCircle, ph.firstChild);
}
coverCircle.style.backgroundImage = `url('${coverUrl}')`;
ph.classList.add('has-cover');
ph.style.display = '';
}
}
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;
audioCover.removeAttribute('src');
if (parent) {
parent.style.background = `url('${fallbackUrl}') no-repeat center / contain black`;
parent.style.background = 'none';
if (ph) {
if (coverCircle) {
coverCircle.style.backgroundImage = 'none';
}
ph.classList.remove('has-cover');
ph.style.display = '';
}
}
const audioEl = document.querySelector('audio#my-video');
if (audioEl) {
audioEl.setAttribute('poster', fallbackUrl);
audioEl.removeAttribute('poster');
}
}
}
@@ -1684,10 +1706,10 @@ window.cancelAnimFrame = (function () {
} catch {}
}
if (!targetThumb && slug) {
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${slug}"]`);
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${slug}"], .posts > a.thumb[href*="/${slug}#"]`);
}
if (!targetThumb && itemid) {
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${itemid}"], .posts > a.thumb[data-bg*="/${itemid}."]`);
targetThumb = document.querySelector(`.posts > a.thumb[data-item-id="${itemid}"], .posts > a.thumb[href$="/${itemid}"], .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
@@ -1856,7 +1878,8 @@ window.cancelAnimFrame = (function () {
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 || '');
const safeGridHash = (gridObj.hash && !gridObj.hash.match(/^#[a-zA-Z0-9_-]{1,60}$/)) ? gridObj.hash : '';
return gridObj.pathname + (gridObj.search || '') + safeGridHash;
}
} catch {}
}
@@ -1872,7 +1895,8 @@ window.cancelAnimFrame = (function () {
path = (path ? path : '') + '/p/' + currentPage;
}
if (!path) path = '/';
return path + (urlObj.search || '') + (urlObj.hash || '');
const safeHash = (urlObj.hash && !urlObj.hash.match(/^#[a-zA-Z0-9_-]{1,60}$/)) ? urlObj.hash : '';
return path + (urlObj.search || '') + safeHash;
} catch {
return '/';
}
@@ -2731,7 +2755,18 @@ window.cancelAnimFrame = (function () {
window._currentActiveAlbumGallery = null;
return;
}
if (container._f0ckAlbumInit) return;
const getHashSubf0ckId = () => {
return (window.location.hash || '').replace(/^#/, '').trim();
};
if (container._f0ckAlbumInit) {
const initialHash = getHashSubf0ckId() || container.getAttribute('data-requested-subf0ck') || '';
if (initialHash && window._currentActiveAlbumGallery && typeof window._currentActiveAlbumGallery.showBySlug === 'function') {
window._currentActiveAlbumGallery.showBySlug(initialHash);
}
return;
}
container._f0ckAlbumInit = true;
let albumData = null;
@@ -2789,12 +2824,15 @@ window.cancelAnimFrame = (function () {
let currentIndex = 0;
const getHashSubf0ckId = () => {
return (window.location.hash || '').replace(/^#/, '').trim();
};
const initialHash = getHashSubf0ckId() || container.getAttribute('data-requested-subf0ck') || '';
if (initialHash) {
if (!getHashSubf0ckId()) {
history.replaceState(null, '', window.location.pathname + window.location.search + '#' + initialHash);
}
const locEl = document.querySelector('.location');
if (locEl && !locEl.textContent.includes('#')) {
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + initialHash;
}
const foundIdx = albumData.findIndex(item =>
String(item.slug || '') === initialHash ||
String(item.subf0ck_id || '') === initialHash ||
@@ -2904,6 +2942,10 @@ window.cancelAnimFrame = (function () {
if (updateHash && subKey) {
history.replaceState(null, '', window.location.pathname + window.location.search + '#' + subKey);
}
const locEl = document.querySelector('.location');
if (locEl && subKey) {
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + subKey;
}
// Pause any playing audio or video
if (videoEl && !videoEl.paused) {
@@ -2962,16 +3004,42 @@ window.cancelAnimFrame = (function () {
if (audioWrapper) {
audioWrapper.style.display = 'block';
const coverUrl = item.coverart || item.thumb || '/s/img/audio.webp';
audioWrapper.style.backgroundImage = `url('${coverUrl}')`;
audioWrapper.style.backgroundRepeat = 'no-repeat';
audioWrapper.style.backgroundPosition = 'center';
audioWrapper.style.backgroundSize = 'contain';
audioWrapper.style.backgroundColor = 'black';
const hasCover = item.has_coverart && item.coverart && !item.coverart.includes('audio.webp');
const coverUrl = hasCover ? (item.coverart || item.thumb) : null;
let ph = audioWrapper.querySelector(':scope > .sidebar-media-placeholder.audio');
if (!ph) {
ph = document.createElement('div');
ph.className = 'sidebar-media-placeholder audio';
ph.innerHTML = '<i class="fa-solid fa-music"></i>';
audioWrapper.prepend(ph);
}
let coverCircle = ph.querySelector('.audio-cover-circle');
if (!coverCircle) {
coverCircle = document.createElement('div');
coverCircle.className = 'audio-cover-circle';
ph.insertBefore(coverCircle, ph.firstChild);
}
audioWrapper.style.backgroundImage = 'none';
audioWrapper.style.backgroundColor = 'transparent';
if (coverUrl) {
coverCircle.style.backgroundImage = `url('${coverUrl}')`;
ph.classList.add('has-cover');
} else {
coverCircle.style.backgroundImage = 'none';
ph.classList.remove('has-cover');
}
ph.style.display = '';
}
if (audioEl) {
audioEl.style.display = 'block';
const hasCover = item.has_coverart && item.coverart && !item.coverart.includes('audio.webp');
const coverUrl = hasCover ? (item.coverart || item.thumb) : null;
if (coverUrl) {
audioEl.setAttribute('poster', coverUrl);
} else {
audioEl.removeAttribute('poster');
}
if (audioEl.src !== newSrc && !audioEl.src.endsWith(newSrc)) {
audioEl.src = newSrc;
@@ -2981,6 +3049,12 @@ window.cancelAnimFrame = (function () {
initAlbumAudioV0ck();
video = audioEl;
if (window.initVisualizer) window.initVisualizer();
audioEl.addEventListener('play', () => audioWrapper?.classList.add('is-playing'));
audioEl.addEventListener('pause', () => audioWrapper?.classList.remove('is-playing'));
audioEl.addEventListener('ended', () => audioWrapper?.classList.remove('is-playing'));
if (item.size && audioWrapper) {
const dlBtn = audioWrapper.querySelector('#v0ck_download');
if (dlBtn) dlBtn.textContent = `Download (${item.size})`;
@@ -3041,11 +3115,44 @@ window.cancelAnimFrame = (function () {
});
updateInfoModal();
updateAlbumTags();
preload(currentIndex + 1);
preload(currentIndex - 1);
};
const updateAlbumTags = () => {
const sub = albumData[currentIndex];
if (!sub) return;
const tagsContainer = document.querySelector('#tags');
if (!tagsContainer) return;
const subSlug = sub.slug || sub.subf0ck_id || sub.id;
tagsContainer.dataset.subf0ckId = String(sub.id);
tagsContainer.dataset.subf0ckSlug = String(subSlug);
tagsContainer.dataset.subf0ckIndex = String(currentIndex);
let tagScopeEl = document.querySelector('#subf0ck-tag-scope');
if (!tagScopeEl) {
const sidebarCont = document.querySelector('.sidebar-tags-container');
if (sidebarCont) {
tagScopeEl = document.createElement('div');
tagScopeEl.id = 'subf0ck-tag-scope';
tagScopeEl.className = 'subf0ck-tag-scope';
sidebarCont.insertBefore(tagScopeEl, tagsContainer);
}
}
if (tagScopeEl) {
tagScopeEl.innerHTML = `<i class="fa-solid fa-layer-group"></i> Subf0ck <code>${subSlug}</code> (${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;
@@ -3168,63 +3275,81 @@ window.cancelAnimFrame = (function () {
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('is-visible');
clearTimeout(stripTimeout);
stripEl.classList.add('strip-peek');
if (stripTimeout) clearTimeout(stripTimeout);
stripTimeout = setTimeout(() => {
stripEl.classList.remove('is-visible');
}, 3500);
stripEl.classList.remove('strip-peek');
}, 3000);
}
});
// Hashchange listener for forward/backward browser navigation
const hashChangeHandler = () => {
const newHash = getHashSubf0ckId();
if (!newHash) return;
const targetIdx = albumData.findIndex(item =>
String(item.slug || '') === newHash ||
String(item.subf0ck_id || '') === newHash ||
String(item.id) === newHash ||
String(item.order_index + 1) === newHash
);
if (targetIdx !== -1 && targetIdx !== currentIndex) {
showImage(targetIdx, 'none', false);
}
};
window.addEventListener('hashchange', hashChangeHandler);
// Touch swipe support on album container
let touchStartX = 0;
let touchStartY = 0;
// Handle touch swipe for mobile gallery navigation
let touchStartX = null;
let touchStartY = null;
container.addEventListener('touchstart', (e) => {
if (e.target.closest('.v0ck_player_controls, .v0ck_settings_menu, input[type="range"]')) return;
if (e.touches && e.touches.length === 1) {
if (e.touches.length === 1) {
touchStartX = e.touches[0].clientX;
touchStartY = e.touches[0].clientY;
}
}, { passive: true });
container.addEventListener('touchend', (e) => {
if (e.target.closest('.v0ck_player_controls, .v0ck_settings_menu, input[type="range"]')) return;
if (e.changedTouches && e.changedTouches.length === 1) {
const diffX = e.changedTouches[0].clientX - touchStartX;
const diffY = e.changedTouches[0].clientY - touchStartY;
if (Math.abs(diffX) > 40 && Math.abs(diffX) > Math.abs(diffY) * 1.5) {
if (diffX > 0) {
showImage(currentIndex - 1, 'prev');
} else {
showImage(currentIndex + 1, 'next');
}
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;
@@ -3234,8 +3359,25 @@ window.cancelAnimFrame = (function () {
});
};
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);
@@ -3243,8 +3385,12 @@ window.cancelAnimFrame = (function () {
video = null;
}
initAlbumGallery();
syncLocationSubf0ck();
};
document.addEventListener('f0ck:contentLoaded', initAlbumGallery);
document.addEventListener('f0ck:contentLoaded', () => {
initAlbumGallery();
syncLocationSubf0ck();
});
const initOnaraInitialState = () => {
if (!isOnaraActive()) return;
@@ -3524,13 +3670,155 @@ window.cancelAnimFrame = (function () {
};
// 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 += `<div class="f0ck-tuner-section-title">${currentSection}</div>`;
}
const val = window.audioVisualizerTuning[s.key] !== undefined ? window.audioVisualizerTuning[s.key] : DEFAULT_AUDIO_TUNING[s.key];
rowsHtml += `
<div class="f0ck-tuner-row" data-key="${s.key}">
<div class="f0ck-tuner-row-label">
<span>${s.label}</span>
<span id="val-${s.key}">${val}${s.unit}</span>
</div>
<input type="range" id="input-${s.key}" min="${s.min}" max="${s.max}" step="${s.step}" value="${val}" />
</div>
`;
});
panel.innerHTML = `
<div class="f0ck-tuner-header">
<span class="f0ck-tuner-title"><i class="fa-solid fa-sliders"></i> Audio Reactivity Tuner</span>
<button type="button" class="f0ck-tuner-close" id="f0ck-tuner-close" title="Close"></button>
</div>
<div class="f0ck-tuner-body">
${rowsHtml}
</div>
<div class="f0ck-tuner-footer">
<button type="button" id="f0ck-tuner-copy" class="f0ck-tuner-btn-copy"><i class="fa-solid fa-copy"></i> Copy Settings</button>
<button type="button" id="f0ck-tuner-reset" class="f0ck-tuner-btn-reset"><i class="fa-solid fa-rotate-left"></i> Reset</button>
</div>
`;
const toggleBtn = document.createElement('button');
toggleBtn.id = 'f0ck-tuner-toggle';
toggleBtn.type = 'button';
toggleBtn.className = 'f0ck-tuner-toggle';
toggleBtn.innerHTML = '<i class="fa-solid fa-sliders"></i> Tuner';
toggleBtn.title = 'Open Live Audio Reactivity Tuner';
document.body.appendChild(panel);
document.body.appendChild(toggleBtn);
toggleBtn.addEventListener('click', () => {
panel.classList.toggle('hidden');
});
panel.querySelector('#f0ck-tuner-close').addEventListener('click', () => {
panel.classList.add('hidden');
});
sliders.forEach(s => {
const input = panel.querySelector(`#input-${s.key}`);
const valEl = panel.querySelector(`#val-${s.key}`);
input.addEventListener('input', () => {
const parsed = parseFloat(input.value);
window.audioVisualizerTuning[s.key] = parsed;
valEl.textContent = `${parsed}${s.unit}`;
try {
localStorage.setItem('f0ck_audio_tuning', JSON.stringify(window.audioVisualizerTuning));
} catch (e) {}
});
});
panel.querySelector('#f0ck-tuner-copy').addEventListener('click', () => {
const copyBtn = panel.querySelector('#f0ck-tuner-copy');
const text = JSON.stringify(window.audioVisualizerTuning, null, 2);
navigator.clipboard.writeText(text).then(() => {
copyBtn.innerHTML = '<i class="fa-solid fa-check"></i> Copied!';
if (typeof window.flashMessage === 'function') {
window.flashMessage('Settings copied to clipboard! Paste them into the chat.', 3500, 'success');
}
setTimeout(() => {
copyBtn.innerHTML = '<i class="fa-solid fa-copy"></i> Copy Settings';
}, 2000);
});
});
panel.querySelector('#f0ck-tuner-reset').addEventListener('click', () => {
Object.assign(window.audioVisualizerTuning, DEFAULT_AUDIO_TUNING);
try {
localStorage.removeItem('f0ck_audio_tuning');
} catch (e) {}
sliders.forEach(s => {
const input = panel.querySelector(`#input-${s.key}`);
const valEl = panel.querySelector(`#val-${s.key}`);
input.value = DEFAULT_AUDIO_TUNING[s.key];
valEl.textContent = `${DEFAULT_AUDIO_TUNING[s.key]}${s.unit}`;
});
if (typeof window.flashMessage === 'function') {
window.flashMessage('Tuner reset to defaults', 2000, 'info');
}
});
};
window.initVisualizer = () => {
const audioElement = document.querySelector("audio");
const audioElement = document.querySelector("audio#my-video, audio#f0ck-album-audio, audio");
if (audioElement) {
// Ensure Tuner UI is mounted
initAudioTunerUI();
// Cleanup existing visualizer
if (visualizerRafId) window.cancelAnimFrame(visualizerRafId);
const existingCanvas = document.querySelector(".v0ck > canvas.audio-visualizer");
if (existingCanvas) existingCanvas.remove();
document.querySelectorAll("canvas.audio-visualizer").forEach(c => c.remove());
const canvas = document.createElement("canvas");
canvas.className = "audio-visualizer";
@@ -3538,39 +3826,195 @@ window.cancelAnimFrame = (function () {
canvas.width = 1920;
canvas.height = 1080;
setTimeout(() => {
const v0ckContainer = document.querySelector(".v0ck");
if (v0ckContainer) v0ckContainer.insertAdjacentElement("afterbegin", canvas);
}, 400);
const attachCanvas = () => {
const v0ckContainer = audioElement.closest('.v0ck') || audioElement.parentElement || document.querySelector('.v0ck');
if (v0ckContainer && !v0ckContainer.contains(canvas)) {
const controls = v0ckContainer.querySelector('.v0ck_player_controls');
if (controls) {
v0ckContainer.insertBefore(canvas, controls);
} else {
v0ckContainer.appendChild(canvas);
}
}
};
attachCanvas();
setTimeout(attachCanvas, 50);
setTimeout(attachCanvas, 250);
setTimeout(attachCanvas, 600);
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 source = audioElement._mediaElementSource;
let analyser = audioElement._audioAnalyser;
if (!source) {
try {
source = audioCtx.createMediaElementSource(audioElement);
audioElement._mediaElementSource = source;
} catch (e) {
console.warn("Visualizer Source creation failed:", e);
}
}
const cfgInit = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
if (!analyser) {
analyser = audioCtx.createAnalyser();
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = cfgInit.smoothing !== undefined ? cfgInit.smoothing : 0.80;
audioElement._audioAnalyser = analyser;
if (source) {
source.connect(analyser);
source.connect(audioCtx.destination);
}
} else {
analyser.fftSize = 2048;
analyser.smoothingTimeConstant = cfgInit.smoothing !== undefined ? cfgInit.smoothing : 0.80;
}
let data = new Uint8Array(analyser.frequencyBinCount);
let noteIcon = null;
let smoothScale = 1;
let smoothGlow = 0;
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 accent = getComputedStyle(document.body).getPropertyValue("--accent")?.trim() || "#99ff00";
ctx.fillStyle = accent;
const count = analyser.frequencyBinCount;
const barWidth = canvas.width / count;
const cfg = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
const barMult = cfg.barHeight !== undefined ? cfg.barHeight : 0.85;
for (let i = 0; i < count; i++) {
const value = data[i];
if (value > 0) {
const percent = (value / 256) * barMult;
const height = (canvas.height * percent / 2) - 30;
if (height > 0) {
const offset = canvas.height - height - 1;
ctx.fillRect(i * barWidth, offset, barWidth, height);
}
}
}
if (cfg.smoothing !== undefined && analyser.smoothingTimeConstant !== cfg.smoothing) {
analyser.smoothingTimeConstant = cfg.smoothing;
}
// Animate the music note and/or cover art circle to react dynamically to the music
const v0ckContainer = audioElement.closest('.v0ck') || audioElement.parentElement || document.querySelector('.v0ck');
const ph = v0ckContainer ? v0ckContainer.querySelector('.sidebar-media-placeholder.audio') : null;
if (!noteIcon || !noteIcon.isConnected) {
noteIcon = ph ? ph.querySelector('i') : null;
}
const coverCircle = ph ? ph.querySelector('.audio-cover-circle') : null;
if (coverCircle && cfg.coverSize) {
coverCircle.style.width = cfg.coverSize + 'px';
coverCircle.style.height = cfg.coverSize + 'px';
}
if (noteIcon || coverCircle) {
if (!audioElement.paused && audioCtx.state === 'running') {
// Bass energy from kick/sub-bass bins (1 to 10): peak transient + body
let bassMax = 0;
let bassSum = 0;
const bassBins = Math.min(10, count);
for (let b = 1; b <= bassBins; b++) {
const val = data[b];
if (val > bassMax) bassMax = val;
bassSum += val;
}
const bassPeak = bassMax / 255;
const bassAvg = bassSum / (bassBins * 255);
// Kick-driven punch: transient peak dominant for instant beat response
const kickEnergy = Math.min(1, (bassPeak * 0.75 + bassAvg * 0.25) * (cfg.bassGain || 3.0));
// Midrange melodic energy
let midMax = 0;
let midSum = 0;
const midStart = 11;
const midEnd = Math.min(32, count);
for (let m = midStart; m < midEnd; m++) {
const val = data[m];
if (val > midMax) midMax = val;
midSum += val;
}
const midEnergy = Math.min(1, ((midMax / 255) * 0.65 + (midSum / ((midEnd - midStart) * 255)) * 0.35) * 2.2);
// High-frequency snap (snares, hi-hats, percs)
let highMax = 0;
const highStart = 33;
const highEnd = Math.min(70, count);
for (let h = highStart; h < highEnd; h++) {
if (data[h] > highMax) highMax = data[h];
}
const highEnergy = Math.min(1, (highMax / 255) * 2.0);
// Dynamic punch with power curve
const punch = Math.pow(kickEnergy, (cfg.bassPower !== undefined ? cfg.bassPower : 1.5));
const bounceMultiplier = cfg.bounceBoost !== undefined ? cfg.bounceBoost : 1.5;
// Target scale jumps based on slider and bounce boost
const targetScale = 1 + (punch * (cfg.scaleBounce !== undefined ? cfg.scaleBounce : 0.50) * bounceMultiplier) + (midEnergy * 0.05);
const targetGlow = punch * (cfg.glowIntensity !== undefined ? cfg.glowIntensity : 20) + midEnergy * 20;
// Attack & release smoothing
const cfgAttack = cfg.attackSpeed !== undefined ? cfg.attackSpeed : 0.85;
const cfgRelease = cfg.releaseSpeed !== undefined ? cfg.releaseSpeed : 0.16;
const attackSpeed = targetScale > smoothScale ? cfgAttack : cfgRelease;
smoothScale += (targetScale - smoothScale) * attackSpeed;
smoothGlow += (targetGlow - smoothGlow) * attackSpeed;
// Rhythmic tilt based on balance
const tilt = (midEnergy - kickEnergy * 0.6) * 18;
if (noteIcon) {
noteIcon.style.transform = `scale(${smoothScale.toFixed(3)}) rotate(${tilt.toFixed(2)}deg)`;
noteIcon.style.filter = `drop-shadow(0 0 ${Math.round(14 + smoothGlow * 0.5)}px rgba(var(--accent-rgb, 153, 255, 0), ${(0.45 + (smoothScale - 1) * 1.5).toFixed(2)}))`;
}
if (coverCircle) {
// Direct scale bounce: the disc visibly thumps like a speaker cone
const circleScale = smoothScale;
const glow1 = Math.round(16 + smoothGlow * 1.1);
const glow2 = Math.round(38 + smoothGlow * 2.3);
const glow3 = Math.round(75 + smoothGlow * 3.6);
const alpha1 = Math.min(0.98, (0.42 + (smoothScale - 1) * 2.4)).toFixed(2);
const alpha2 = Math.min(0.85, (0.24 + (smoothScale - 1) * 1.8)).toFixed(2);
const alpha3 = Math.min(0.60, (0.10 + (smoothScale - 1) * 1.2)).toFixed(2);
coverCircle.style.transform = `translate(-50%, -50%) scale(${circleScale.toFixed(3)})`;
coverCircle.style.boxShadow = `0 14px 45px rgba(0, 0, 0, 0.9), 0 0 ${glow1}px rgba(var(--accent-rgb, 153, 255, 0), ${alpha1}), 0 0 ${glow2}px rgba(var(--accent-rgb, 153, 255, 0), ${alpha2}), 0 0 ${glow3}px rgba(var(--accent-rgb, 153, 255, 0), ${alpha3}), inset 0 0 20px rgba(0, 0, 0, 0.65)`;
coverCircle.style.borderColor = `rgba(var(--accent-rgb, 153, 255, 0), ${(0.42 + (smoothScale - 1) * 2.0).toFixed(2)})`;
}
} else {
// Decay smoothly back to neutral state when paused or idle
if (smoothScale > 1.002) {
smoothScale += (1 - smoothScale) * 0.16;
if (noteIcon) {
noteIcon.style.transform = `scale(${smoothScale.toFixed(3)})`;
noteIcon.style.filter = '';
}
if (coverCircle) {
coverCircle.style.transform = `translate(-50%, -50%) scale(${smoothScale.toFixed(3)})`;
}
} else if (smoothScale !== 1) {
smoothScale = 1;
if (noteIcon) {
noteIcon.style.transform = '';
noteIcon.style.filter = '';
}
if (coverCircle) {
coverCircle.style.transform = 'translate(-50%, -50%)';
coverCircle.style.boxShadow = '';
coverCircle.style.borderColor = '';
}
}
}
}
};
const loopingFunction = () => {
@@ -3581,11 +4025,19 @@ window.cancelAnimFrame = (function () {
visualizerRafId = requestAnimationFrame(loopingFunction);
audioElement.onplay = () => {
if (audioCtx.state === 'suspended') {
const resumeAudio = () => {
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
};
audioElement.addEventListener('play', resumeAudio);
audioElement.addEventListener('playing', resumeAudio);
const playerWrap = audioElement.closest('.v0ck') || audioElement.parentElement;
if (playerWrap) {
playerWrap.addEventListener('click', resumeAudio, { passive: true });
playerWrap.addEventListener('pointerdown', resumeAudio, { passive: true });
}
}
};
@@ -5339,6 +5791,8 @@ window.cancelAnimFrame = (function () {
const params = new URLSearchParams();
params.append('mode', window.activeMode);
const targetHash = new URL(url, window.location.origin).hash.replace(/^#/, '').trim();
if (targetHash) params.append('subf0ck', targetHash);
if (tag) params.append('tag', tag);
if (hall) params.append('hall', hall);
if (userHall && userHallOwner) {
@@ -6586,13 +7040,18 @@ window.cancelAnimFrame = (function () {
? window.getCsrfToken()
: ((window.f0ckSession && window.f0ckSession.csrf_token) || document.querySelector('meta[name="csrf-token"]')?.getAttribute('content') || '');
const currentSub = (window.albumGallery && typeof window.albumGallery.getCurrentSubf0ck === 'function')
? window.albumGallery.getCurrentSubf0ck()
: null;
const subf0ck_id = currentSub ? (currentSub.slug || currentSub.id) : (ratingEl.dataset.subf0ckSlug || ratingEl.dataset.subf0ckId || document.querySelector('#tags')?.dataset?.subf0ckSlug || null);
fetch(`/api/v2/item/${postid}/rating`, {
method: 'POST',
headers: {
"Content-Type": "application/json",
"X-CSRF-Token": csrf
},
body: JSON.stringify({ rating: targetRating, csrf_token: csrf })
body: JSON.stringify({ rating: targetRating, subf0ck_id, csrf_token: csrf })
})
.then(r => r.json())
.then(res => {
@@ -6611,6 +7070,9 @@ window.cancelAnimFrame = (function () {
if (ratingEl._lastCycleReqId !== reqId) return; // ignore stale responses
if (res.success) {
if (res.tags && currentSub) {
currentSub.tags = res.tags;
}
if (res.tags && window.renderTags) {
window.renderTags(res.tags);
}