huo
This commit is contained in:
@@ -3812,7 +3812,8 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.f0ck-tuner-subtab-btn {
|
.f0ck-tuner-subtab-btn,
|
||||||
|
.f0ck-tuner-mode-btn {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -3830,12 +3831,14 @@ body.sidebar-right-hidden #sidebar-drag-zone {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.f0ck-tuner-subtab-btn:hover {
|
.f0ck-tuner-subtab-btn:hover,
|
||||||
|
.f0ck-tuner-mode-btn:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background: rgba(255, 255, 255, 0.06);
|
background: rgba(255, 255, 255, 0.06);
|
||||||
}
|
}
|
||||||
|
|
||||||
.f0ck-tuner-subtab-btn.active {
|
.f0ck-tuner-subtab-btn.active,
|
||||||
|
.f0ck-tuner-mode-btn.active {
|
||||||
color: #000;
|
color: #000;
|
||||||
background: var(--accent, #99ff00);
|
background: var(--accent, #99ff00);
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
|||||||
+586
-123
@@ -104,6 +104,123 @@ window.cancelAnimFrame = (function () {
|
|||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Get current item's slug from the URL (for pool-based random exclusion)
|
||||||
|
window.getCurrentItemSlug = () => {
|
||||||
|
const path = window.location.pathname;
|
||||||
|
const match = path.match(/\/([^/]+)\/?$/);
|
||||||
|
if (match && match[1] !== 'random' && match[1] !== 'favs') return match[1];
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
// ── Random Pool Manager (Fisher-Yates) ────────────────────────────
|
||||||
|
// Preloads all candidate item IDs for the current context (tag/hall/user).
|
||||||
|
// Items are shuffled once; a cursor walks through sequentially → zero duplicates
|
||||||
|
// until every item has been seen, then re-shuffles automatically.
|
||||||
|
window._randomPool = null;
|
||||||
|
window._randomPoolContext = null;
|
||||||
|
window._randomPoolCursor = 0;
|
||||||
|
|
||||||
|
// Fisher-Yates (Durstenfeld) in-place shuffle
|
||||||
|
const _shuffleArray = (arr) => {
|
||||||
|
for (let i = arr.length - 1; i > 0; i--) {
|
||||||
|
const j = Math.floor(Math.random() * (i + 1));
|
||||||
|
[arr[i], arr[j]] = [arr[j], arr[i]];
|
||||||
|
}
|
||||||
|
return arr;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Pick next item from the shuffled pool, skipping current
|
||||||
|
window.pickFromPool = (currentSlug, currentId) => {
|
||||||
|
const pool = window._randomPool;
|
||||||
|
if (!pool || !pool.items || pool.items.length < 2) return null;
|
||||||
|
|
||||||
|
// Walk forward from cursor, skipping current item
|
||||||
|
const len = pool.items.length;
|
||||||
|
for (let attempts = 0; attempts < len; attempts++) {
|
||||||
|
const pick = pool.items[window._randomPoolCursor];
|
||||||
|
window._randomPoolCursor++;
|
||||||
|
|
||||||
|
// Exhausted — re-shuffle and reset
|
||||||
|
if (window._randomPoolCursor >= len) {
|
||||||
|
_shuffleArray(pool.items);
|
||||||
|
window._randomPoolCursor = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pick !== currentSlug && pick !== currentId) return pick;
|
||||||
|
}
|
||||||
|
// Fallback (pool has only 1 unique item)
|
||||||
|
return pool.items[0];
|
||||||
|
};
|
||||||
|
|
||||||
|
window.fetchRandomPool = (forceRefresh = false) => {
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
const loc = window.location.href;
|
||||||
|
|
||||||
|
const tagMatch = loc.match(/\/tag\/([^/]+)/);
|
||||||
|
if (tagMatch) params.append('tag', decodeURIComponent(tagMatch[1]));
|
||||||
|
|
||||||
|
const hallMatch = loc.match(/\/h\/([^/]+)/);
|
||||||
|
if (hallMatch) params.append('hall', decodeURIComponent(hallMatch[1]));
|
||||||
|
|
||||||
|
const userHallMatch = loc.match(/\/user\/([^/]+)\/hall\/([^/]+)/);
|
||||||
|
if (userHallMatch) {
|
||||||
|
params.append('userHall', decodeURIComponent(userHallMatch[2]));
|
||||||
|
params.append('userHallOwner', decodeURIComponent(userHallMatch[1]));
|
||||||
|
} else {
|
||||||
|
const userMatch = loc.match(/\/user\/([^/]+)/);
|
||||||
|
if (userMatch) {
|
||||||
|
params.append('user', decodeURIComponent(userMatch[1]));
|
||||||
|
if (loc.match(/\/favs(\/|$|\?)/)) params.append('fav', 'true');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const qMime = urlParams.get('mime');
|
||||||
|
const cookieMimeMatch = document.cookie.match(/(?:^|;\s*)mime=([^;]*)/);
|
||||||
|
const cookieMime = cookieMimeMatch ? decodeURIComponent(cookieMimeMatch[1]).trim() : null;
|
||||||
|
if (qMime) params.append('mime', qMime);
|
||||||
|
else if (cookieMime) params.append('mime', cookieMime);
|
||||||
|
else {
|
||||||
|
const mimeMatch = loc.match(/\/((?:video|audio|image|,)+)(\/|$|\?)/);
|
||||||
|
if (mimeMatch) params.append('mime', mimeMatch[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isStrict = window.f0ckSession?.strict_mode || window.location.search.includes('strict=1') || (localStorage.getItem('search_strict') === 'true');
|
||||||
|
if (isStrict) params.append('strict', '1');
|
||||||
|
|
||||||
|
const contextKey = params.toString() + '|mode=' + (window.activeMode ?? 0);
|
||||||
|
if (!forceRefresh && window._randomPoolContext === contextKey && window._randomPool) return;
|
||||||
|
|
||||||
|
const url = '/api/v2/random-pool' + ([...params].length > 0 ? '?' + params.toString() : '');
|
||||||
|
fetch(url, { credentials: 'include' })
|
||||||
|
.then(r => r.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data && Array.isArray(data.items)) {
|
||||||
|
_shuffleArray(data.items);
|
||||||
|
window._randomPool = { items: data.items, total: data.total, sampled: data.sampled };
|
||||||
|
window._randomPoolContext = contextKey;
|
||||||
|
window._randomPoolCursor = 0;
|
||||||
|
console.log(`[RANDOM-POOL] Cached ${data.items.length} items (shuffled) for context: ${contextKey}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(err => console.warn('[RANDOM-POOL] Failed to fetch:', err));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Invalidate pool on mode change
|
||||||
|
document.addEventListener('f0ck:modeChanged', () => {
|
||||||
|
window._randomPool = null;
|
||||||
|
window._randomPoolContext = null;
|
||||||
|
setTimeout(() => window.fetchRandomPool(true), 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fetch pool on AJAX navigation (context may change e.g. /tag/babymetal → /tag/music)
|
||||||
|
document.addEventListener('f0ck:contentLoaded', () => {
|
||||||
|
// Slight delay so URL has updated via pushState before we parse it
|
||||||
|
setTimeout(() => window.fetchRandomPool(), 50);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initial pool fetch on page load (non-blocking)
|
||||||
|
setTimeout(() => window.fetchRandomPool(), 300);
|
||||||
|
|
||||||
// <guest-favs> - disabled for clean guest mode
|
// <guest-favs> - disabled for clean guest mode
|
||||||
const f0ckGuestFavs = {
|
const f0ckGuestFavs = {
|
||||||
get: () => [],
|
get: () => [],
|
||||||
@@ -4081,12 +4198,12 @@ window.cancelAnimFrame = (function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const top = countTop > 0
|
|
||||||
? [Math.round(rTop / countTop), Math.round(gTop / countTop), Math.round(bTop / countTop)]
|
|
||||||
: [25, 25, 35];
|
|
||||||
const bot = countBot > 0
|
const bot = countBot > 0
|
||||||
? [Math.round(rBot / countBot), Math.round(gBot / countBot), Math.round(bBot / countBot)]
|
? [Math.round(rBot / countBot), Math.round(gBot / countBot), Math.round(bBot / countBot)]
|
||||||
: [25, 25, 35];
|
: (countTop > 0 ? [Math.round(rTop / countTop), Math.round(gTop / countTop), Math.round(bTop / countTop)] : [25, 25, 35]);
|
||||||
|
const top = countTop > 0
|
||||||
|
? [Math.round(rTop / countTop), Math.round(gTop / countTop), Math.round(bTop / countTop)]
|
||||||
|
: (countBot > 0 ? [...bot] : [25, 25, 35]);
|
||||||
|
|
||||||
return { top, bottom: bot };
|
return { top, bottom: bot };
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -4094,6 +4211,39 @@ window.cancelAnimFrame = (function () {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const parseCssColorToRgb = (str) => {
|
||||||
|
if (!str) return [153, 255, 0];
|
||||||
|
if (str.startsWith('#')) {
|
||||||
|
let hex = str.slice(1);
|
||||||
|
if (hex.length === 3) hex = hex.split('').map(c => c + c).join('');
|
||||||
|
const num = parseInt(hex, 16);
|
||||||
|
return [(num >> 16) & 255, (num >> 8) & 255, num & 255];
|
||||||
|
}
|
||||||
|
const m = str.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
|
||||||
|
if (m) return [Number(m[1]), Number(m[2]), Number(m[3])];
|
||||||
|
const hm = str.match(/hsl\(\s*([\d.]+)\s*,\s*([\d.]+)%?\s*,\s*([\d.]+)%?\s*\)/);
|
||||||
|
if (hm) {
|
||||||
|
const h = Number(hm[1]) / 360, s = Number(hm[2]) / 100, l = Number(hm[3]) / 100;
|
||||||
|
let r, g, b;
|
||||||
|
if (s === 0) { r = g = b = l; } else {
|
||||||
|
const hue2rgb = (p, q, t) => {
|
||||||
|
if (t < 0) t += 1; if (t > 1) t -= 1;
|
||||||
|
if (t < 1/6) return p + (q - p) * 6 * t;
|
||||||
|
if (t < 1/2) return q;
|
||||||
|
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6;
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||||
|
const p = 2 * l - q;
|
||||||
|
r = hue2rgb(p, q, h + 1/3);
|
||||||
|
g = hue2rgb(p, q, h);
|
||||||
|
b = hue2rgb(p, q, h - 1/3);
|
||||||
|
}
|
||||||
|
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||||
|
}
|
||||||
|
return [153, 255, 0];
|
||||||
|
};
|
||||||
|
|
||||||
const boostColorSaturation = (rgb, boost = 1.35) => {
|
const boostColorSaturation = (rgb, boost = 1.35) => {
|
||||||
if (!rgb || !Array.isArray(rgb) || rgb.length < 3) return [30, 30, 40];
|
if (!rgb || !Array.isArray(rgb) || rgb.length < 3) return [30, 30, 40];
|
||||||
const [r, g, b] = rgb;
|
const [r, g, b] = rgb;
|
||||||
@@ -4135,12 +4285,17 @@ window.cancelAnimFrame = (function () {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderAmbientLightingWash = (ctx, targetW, targetH, colors, alpha = 1.0) => {
|
const renderAmbientLightingWash = (ctx, targetW, targetH, colors, alpha = 1.0, reactiveOpts = null) => {
|
||||||
if (!ctx || !colors) return;
|
if (!ctx || !colors) return;
|
||||||
const cfg = (typeof window.getEffectiveBackgroundTuning === 'function' ? window.getEffectiveBackgroundTuning() : null) || window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
|
const cfg = (typeof window.getEffectiveBackgroundTuning === 'function' ? window.getEffectiveBackgroundTuning() : null) || window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
|
||||||
const satBoost = cfg.bgAmbientSaturation !== undefined ? Number(cfg.bgAmbientSaturation) : 1.35;
|
const satBoost = cfg.bgAmbientSaturation !== undefined ? Number(cfg.bgAmbientSaturation) : 1.35;
|
||||||
const intensity = cfg.bgAmbientIntensity !== undefined ? Number(cfg.bgAmbientIntensity) : 1.0;
|
let intensity = cfg.bgAmbientIntensity !== undefined ? Number(cfg.bgAmbientIntensity) : 1.0;
|
||||||
const spread = cfg.bgAmbientSpread !== undefined ? Number(cfg.bgAmbientSpread) : 0.85;
|
let spread = cfg.bgAmbientSpread !== undefined ? Number(cfg.bgAmbientSpread) : 0.85;
|
||||||
|
|
||||||
|
if (reactiveOpts) {
|
||||||
|
if (reactiveOpts.intensityMult !== undefined) intensity *= reactiveOpts.intensityMult;
|
||||||
|
if (reactiveOpts.spreadMult !== undefined) spread *= reactiveOpts.spreadMult;
|
||||||
|
}
|
||||||
|
|
||||||
const rawTop = colors.top || colors;
|
const rawTop = colors.top || colors;
|
||||||
const rawBot = colors.bottom || colors.top || colors;
|
const rawBot = colors.bottom || colors.top || colors;
|
||||||
@@ -4185,6 +4340,25 @@ window.cancelAnimFrame = (function () {
|
|||||||
ctx.fillStyle = gradBot;
|
ctx.fillStyle = gradBot;
|
||||||
ctx.fillRect(0, 0, targetW, targetH);
|
ctx.fillRect(0, 0, targetW, targetH);
|
||||||
|
|
||||||
|
// 4. Reactive Center Beat Bloom (Dynamic Audio Pulse)
|
||||||
|
if (reactiveOpts && reactiveOpts.pulse > 0.03) {
|
||||||
|
const p = Math.min(1.0, reactiveOpts.pulse);
|
||||||
|
const coreR = maxR * (0.30 + p * 0.45);
|
||||||
|
const rc = reactiveOpts.coreColor ? reactiveOpts.coreColor[0] : Math.round((rt + rb) / 2);
|
||||||
|
const gc = reactiveOpts.coreColor ? reactiveOpts.coreColor[1] : Math.round((gt + gb) / 2);
|
||||||
|
const bc = reactiveOpts.coreColor ? reactiveOpts.coreColor[2] : Math.round((bt + bb) / 2);
|
||||||
|
const pAlpha = Math.min(1.0, Math.max(0.0, p * 0.75 * intensity)).toFixed(3);
|
||||||
|
const pAlphaHalf = (pAlpha * 0.45).toFixed(3);
|
||||||
|
|
||||||
|
const gradCore = ctx.createRadialGradient(cx, targetH * 0.50, 0, cx, targetH * 0.50, coreR);
|
||||||
|
gradCore.addColorStop(0, `rgba(${rc}, ${gc}, ${bc}, ${pAlpha})`);
|
||||||
|
gradCore.addColorStop(0.45, `rgba(${rc}, ${gc}, ${bc}, ${pAlphaHalf})`);
|
||||||
|
gradCore.addColorStop(1.0, 'rgba(6, 6, 12, 0.0)');
|
||||||
|
|
||||||
|
ctx.fillStyle = gradCore;
|
||||||
|
ctx.fillRect(0, 0, targetW, targetH);
|
||||||
|
}
|
||||||
|
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -4375,7 +4549,10 @@ window.cancelAnimFrame = (function () {
|
|||||||
if (elem) {
|
if (elem) {
|
||||||
if (canvas) {
|
if (canvas) {
|
||||||
// Restore visual state on re-init
|
// Restore visual state on re-init
|
||||||
if (background && elem.tagName !== 'AUDIO') {
|
const bgCfg = (typeof window.getEffectiveBackgroundTuning === 'function' ? window.getEffectiveBackgroundTuning() : null) || window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
|
||||||
|
const feedAudio = Number(bgCfg.feedAudioToBg !== undefined ? bgCfg.feedAudioToBg : 1) === 1;
|
||||||
|
|
||||||
|
if (background && (elem.tagName !== 'AUDIO' || feedAudio)) {
|
||||||
canvas._bgFadingOut = false;
|
canvas._bgFadingOut = false;
|
||||||
canvas.classList.add('fader-in');
|
canvas.classList.add('fader-in');
|
||||||
canvas.classList.remove('fader-out', 'fast-fade');
|
canvas.classList.remove('fader-out', 'fast-fade');
|
||||||
@@ -4775,9 +4952,26 @@ window.cancelAnimFrame = (function () {
|
|||||||
// IMG: draw from thumbnail.
|
// IMG: draw from thumbnail.
|
||||||
drawOnce();
|
drawOnce();
|
||||||
} else if (elem.tagName === 'AUDIO') {
|
} else if (elem.tagName === 'AUDIO') {
|
||||||
// Audio items: fade out the previous item's background, then clear canvas
|
const bgCfg = (typeof window.getEffectiveBackgroundTuning === 'function' ? window.getEffectiveBackgroundTuning() : null) || window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
|
||||||
// The audio visualizer uses its own separate canvas (.audio-visualizer), not #bg
|
const feedAudio = Number(bgCfg.feedAudioToBg !== undefined ? bgCfg.feedAudioToBg : 1) === 1;
|
||||||
if (canvas) {
|
|
||||||
|
if (feedAudio) {
|
||||||
|
if (background && canvas) {
|
||||||
|
canvas._bgFadingOut = false;
|
||||||
|
canvas.classList.remove('fader-out', 'fast-fade');
|
||||||
|
canvas.classList.add('fader-in');
|
||||||
|
}
|
||||||
|
const coverEl = document.querySelector("#f0ck-audio-cover");
|
||||||
|
if (coverEl && coverEl.src && !coverEl.src.endsWith('#') && !coverEl.src.includes('audio.webp') && !coverEl.src.includes('200.gif')) {
|
||||||
|
if (coverEl.complete && coverEl.naturalWidth > 0) {
|
||||||
|
crossfadeToSource(coverEl);
|
||||||
|
} else {
|
||||||
|
coverEl.onload = () => crossfadeToSource(coverEl);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
drawOnce();
|
||||||
|
}
|
||||||
|
} else if (canvas) {
|
||||||
canvas.classList.add('fader-out');
|
canvas.classList.add('fader-out');
|
||||||
canvas.classList.remove('fader-in', 'fast-fade');
|
canvas.classList.remove('fader-in', 'fast-fade');
|
||||||
const clearOnFade = (ev) => {
|
const clearOnFade = (ev) => {
|
||||||
@@ -4995,7 +5189,8 @@ window.cancelAnimFrame = (function () {
|
|||||||
bgAmbientIntensity: 1.9,
|
bgAmbientIntensity: 1.9,
|
||||||
bgAmbientSpread: 0.5,
|
bgAmbientSpread: 0.5,
|
||||||
bgAmbientSaturation: 1.5,
|
bgAmbientSaturation: 1.5,
|
||||||
bgAmbientSmoothness: 0.06
|
bgAmbientSmoothness: 0.06,
|
||||||
|
bgAudioReactivity: 1.50
|
||||||
};
|
};
|
||||||
|
|
||||||
const TUNING_CONFIG_VERSION = '2026-09-16_tuner_v12';
|
const TUNING_CONFIG_VERSION = '2026-09-16_tuner_v12';
|
||||||
@@ -5019,7 +5214,8 @@ window.cancelAnimFrame = (function () {
|
|||||||
bgAmbientIntensity: 1.0,
|
bgAmbientIntensity: 1.0,
|
||||||
bgAmbientSpread: 0.85,
|
bgAmbientSpread: 0.85,
|
||||||
bgAmbientSaturation: 1.35,
|
bgAmbientSaturation: 1.35,
|
||||||
bgAmbientSmoothness: 0.05
|
bgAmbientSmoothness: 0.05,
|
||||||
|
bgAudioReactivity: 1.50
|
||||||
};
|
};
|
||||||
|
|
||||||
const ONARA_TUNING_CONFIG_VERSION = '2026-09-16_onara_v3';
|
const ONARA_TUNING_CONFIG_VERSION = '2026-09-16_onara_v3';
|
||||||
@@ -5039,17 +5235,79 @@ window.cancelAnimFrame = (function () {
|
|||||||
window.DEFAULT_ONARA_TUNING = DEFAULT_ONARA_TUNING;
|
window.DEFAULT_ONARA_TUNING = DEFAULT_ONARA_TUNING;
|
||||||
window.onaraTuning = Object.assign({}, DEFAULT_ONARA_TUNING, savedOnaraTuning || {});
|
window.onaraTuning = Object.assign({}, DEFAULT_ONARA_TUNING, savedOnaraTuning || {});
|
||||||
|
|
||||||
|
const DEFAULT_AUDIO_BG_TUNING = {
|
||||||
|
bgCanvasColor: "#000000",
|
||||||
|
bgCanvasColorOpacity: 0.15,
|
||||||
|
bgCanvasOpacity: 0.90,
|
||||||
|
bgBlurMethod: 0,
|
||||||
|
bgCanvasBlur: 60,
|
||||||
|
bgCanvasBrightness: 0.75,
|
||||||
|
bgCanvasSaturate: 1.9,
|
||||||
|
bgCanvasContrast: 1.15,
|
||||||
|
bgCanvasZoom: 1.38,
|
||||||
|
bgCanvasScale: 2.15,
|
||||||
|
feedAudioToBg: 1,
|
||||||
|
bgAmbientLighting: 1,
|
||||||
|
bgAmbientIntensity: 1.45,
|
||||||
|
bgAmbientSpread: 0.85,
|
||||||
|
bgAmbientSaturation: 1.65,
|
||||||
|
bgAmbientSmoothness: 0.08,
|
||||||
|
bgAudioReactivity: 1.50,
|
||||||
|
onaraBgOpacity: 0.4,
|
||||||
|
onaraBackdropBlur: 5,
|
||||||
|
onaraGridDim: 0.38
|
||||||
|
};
|
||||||
|
|
||||||
|
const AUDIO_BG_TUNING_CONFIG_VERSION = '2026-09-16_audio_bg_v2';
|
||||||
|
let savedAudioBgTuning = null;
|
||||||
|
try {
|
||||||
|
const appliedAudioBgVer = localStorage.getItem('f0ck_audio_bg_tuning_ver');
|
||||||
|
if (appliedAudioBgVer !== AUDIO_BG_TUNING_CONFIG_VERSION) {
|
||||||
|
localStorage.setItem('f0ck_audio_bg_tuning', JSON.stringify(DEFAULT_AUDIO_BG_TUNING));
|
||||||
|
localStorage.setItem('f0ck_audio_bg_tuning_ver', AUDIO_BG_TUNING_CONFIG_VERSION);
|
||||||
|
savedAudioBgTuning = Object.assign({}, DEFAULT_AUDIO_BG_TUNING);
|
||||||
|
} else {
|
||||||
|
const raw = localStorage.getItem('f0ck_audio_bg_tuning');
|
||||||
|
if (raw) savedAudioBgTuning = JSON.parse(raw);
|
||||||
|
}
|
||||||
|
} catch (e) {}
|
||||||
|
|
||||||
|
window.DEFAULT_AUDIO_BG_TUNING = DEFAULT_AUDIO_BG_TUNING;
|
||||||
|
window.audioBgTuning = Object.assign({}, DEFAULT_AUDIO_BG_TUNING, savedAudioBgTuning || {});
|
||||||
|
|
||||||
const isCurrentlyInOnara = () => {
|
const isCurrentlyInOnara = () => {
|
||||||
return document.body.classList.contains('onara-modal-open') ||
|
return document.body.classList.contains('onara-modal-open') ||
|
||||||
(typeof isOnaraActive === 'function' && isOnaraActive() && !!document.getElementById('onara-item-mount'));
|
(typeof isOnaraActive === 'function' && isOnaraActive() && !!document.getElementById('onara-item-mount'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const getEffectiveBackgroundTuning = (forceMode) => {
|
const isCurrentItemAudio = () => {
|
||||||
const useOnara = forceMode !== undefined ? (forceMode === 'onara' || forceMode === true) : isCurrentlyInOnara();
|
const mount = document.getElementById('onara-item-mount');
|
||||||
if (useOnara) {
|
if (mount) {
|
||||||
return Object.assign({}, window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING, window.onaraTuning || DEFAULT_ONARA_TUNING);
|
const a = mount.querySelector('#f0ck-album-audio, audio#my-video, audio');
|
||||||
|
if (a && a.isConnected) return true;
|
||||||
}
|
}
|
||||||
return window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
|
const aud = document.querySelector('#f0ck-album-audio, audio#my-video, audio');
|
||||||
|
if (aud && aud.isConnected) {
|
||||||
|
const albumAudioWrap = document.getElementById('f0ck-album-audio-wrapper');
|
||||||
|
if (albumAudioWrap && albumAudioWrap.style.display !== 'none') return true;
|
||||||
|
if (aud.tagName === 'AUDIO') return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
window.isCurrentItemAudio = isCurrentItemAudio;
|
||||||
|
|
||||||
|
const getEffectiveBackgroundTuning = (forceMode) => {
|
||||||
|
const isAudio = forceMode !== undefined ? (forceMode === 'audio') : isCurrentItemAudio();
|
||||||
|
const useOnara = forceMode !== undefined ? (forceMode === 'onara' || forceMode === true) : isCurrentlyInOnara();
|
||||||
|
|
||||||
|
let base = Object.assign({}, DEFAULT_AUDIO_TUNING, window.audioVisualizerTuning || {});
|
||||||
|
if (useOnara) {
|
||||||
|
base = Object.assign(base, DEFAULT_ONARA_TUNING, window.onaraTuning || {});
|
||||||
|
}
|
||||||
|
if (isAudio) {
|
||||||
|
base = Object.assign(base, DEFAULT_AUDIO_BG_TUNING, window.audioBgTuning || {});
|
||||||
|
}
|
||||||
|
return base;
|
||||||
};
|
};
|
||||||
window.getEffectiveBackgroundTuning = getEffectiveBackgroundTuning;
|
window.getEffectiveBackgroundTuning = getEffectiveBackgroundTuning;
|
||||||
|
|
||||||
@@ -5092,17 +5350,18 @@ window.cancelAnimFrame = (function () {
|
|||||||
window.audioVisualizerTuning = Object.assign({}, getEffectiveDefaultTuning(), savedTuning || {});
|
window.audioVisualizerTuning = Object.assign({}, getEffectiveDefaultTuning(), savedTuning || {});
|
||||||
|
|
||||||
const applyBackgroundOpacitySettings = (cfg, forceMode) => {
|
const applyBackgroundOpacitySettings = (cfg, forceMode) => {
|
||||||
|
const isAudio = forceMode !== undefined ? (forceMode === 'audio') : isCurrentItemAudio();
|
||||||
const inOnara = forceMode !== undefined ? (forceMode === 'onara' || forceMode === true) : isCurrentlyInOnara();
|
const inOnara = forceMode !== undefined ? (forceMode === 'onara' || forceMode === true) : isCurrentlyInOnara();
|
||||||
const c = cfg || getEffectiveBackgroundTuning(forceMode);
|
const c = cfg || getEffectiveBackgroundTuning(forceMode);
|
||||||
const onaraOp = c.onaraBgOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.onaraBgOpacity))) : (inOnara ? 0.0 : 0.40);
|
const onaraOp = c.onaraBgOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.onaraBgOpacity))) : (inOnara ? 0.0 : 0.40);
|
||||||
const bgOp = c.bgCanvasOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.bgCanvasOpacity))) : (inOnara ? 0.95 : 0.81);
|
const bgOp = c.bgCanvasOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.bgCanvasOpacity))) : (isAudio ? 0.90 : (inOnara ? 0.95 : 0.81));
|
||||||
const gridThumbOp = c.onaraGridDim !== undefined ? Math.min(1, Math.max(0, Number(c.onaraGridDim))) : (inOnara ? 0.0 : 0.38);
|
const gridThumbOp = c.onaraGridDim !== undefined ? Math.min(1, Math.max(0, Number(c.onaraGridDim))) : (inOnara ? 0.0 : 0.38);
|
||||||
const onaraBlur = c.onaraBackdropBlur !== undefined ? Math.max(0, Number(c.onaraBackdropBlur)) : (inOnara ? 10 : 5);
|
const onaraBlur = c.onaraBackdropBlur !== undefined ? Math.max(0, Number(c.onaraBackdropBlur)) : (inOnara ? 10 : 5);
|
||||||
const bgBlur = c.bgCanvasBlur !== undefined ? Math.max(0, Number(c.bgCanvasBlur)) : (inOnara ? 31 : 151);
|
const bgBlur = c.bgCanvasBlur !== undefined ? Math.max(0, Number(c.bgCanvasBlur)) : (isAudio ? 60 : (inOnara ? 31 : 151));
|
||||||
const method = c.bgBlurMethod !== undefined ? Number(c.bgBlurMethod) : (inOnara ? 2 : 0);
|
const method = c.bgBlurMethod !== undefined ? Number(c.bgBlurMethod) : (inOnara ? 2 : 0);
|
||||||
const zoom = c.bgCanvasZoom !== undefined ? Math.max(1.0, Number(c.bgCanvasZoom)) : 1.38;
|
const zoom = c.bgCanvasZoom !== undefined ? Math.max(1.0, Number(c.bgCanvasZoom)) : 1.38;
|
||||||
const bgColor = c.bgCanvasColor || '#000000';
|
const bgColor = c.bgCanvasColor || '#000000';
|
||||||
const bgColOp = c.bgCanvasColorOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.bgCanvasColorOpacity))) : (inOnara ? 0.8 : 0.09);
|
const bgColOp = c.bgCanvasColorOpacity !== undefined ? Math.min(1, Math.max(0, Number(c.bgCanvasColorOpacity))) : (isAudio ? 0.15 : (inOnara ? 0.8 : 0.09));
|
||||||
const computedBgCol = bgColOp <= 0.001 ? 'transparent' : (bgColOp >= 0.999 ? bgColor : `color-mix(in srgb, ${bgColor} ${Math.round(bgColOp * 100)}%, transparent)`);
|
const computedBgCol = bgColOp <= 0.001 ? 'transparent' : (bgColOp >= 0.999 ? bgColor : `color-mix(in srgb, ${bgColor} ${Math.round(bgColOp * 100)}%, transparent)`);
|
||||||
|
|
||||||
document.documentElement.style.setProperty('--onara-bg-opacity', onaraOp.toFixed(3));
|
document.documentElement.style.setProperty('--onara-bg-opacity', onaraOp.toFixed(3));
|
||||||
@@ -5427,6 +5686,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
{ section: 'Video & Ambient Background Canvas', key: 'bgCanvasZoom', label: 'Edge Overscan Zoom', min: 1.00, max: 1.80, step: 0.01, unit: 'x' },
|
{ section: 'Video & Ambient Background Canvas', key: 'bgCanvasZoom', label: 'Edge Overscan Zoom', min: 1.00, max: 1.80, step: 0.01, unit: 'x' },
|
||||||
{ section: 'Video & Ambient Background Canvas', key: 'bgCanvasScale', label: 'Visualizer Bar Scale', min: 0.20, max: 4.00, step: 0.05, unit: 'x' },
|
{ section: 'Video & Ambient Background Canvas', key: 'bgCanvasScale', label: 'Visualizer Bar Scale', min: 0.20, max: 4.00, step: 0.05, unit: 'x' },
|
||||||
{ section: 'Video & Ambient Background Canvas', key: 'feedAudioToBg', label: 'Feed Visualizer to Background Canvas (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
|
{ section: 'Video & Ambient Background Canvas', key: 'feedAudioToBg', label: 'Feed Visualizer to Background Canvas (1=On, 0=Off)', min: 0, max: 1, step: 1, unit: '' },
|
||||||
|
{ section: 'Video & Ambient Background Canvas', key: 'bgAudioReactivity', label: 'Audio Beat Reactivity (Pulse Strength)', min: 0.00, max: 4.00, step: 0.05, unit: 'x' },
|
||||||
|
|
||||||
// Onara Mode Background Section
|
// Onara Mode Background Section
|
||||||
{ section: 'Onara Mode Background', key: 'onaraBgOpacity', label: 'Onara Modal Backdrop Darkness', min: 0.00, max: 1.00, step: 0.01, unit: '' },
|
{ section: 'Onara Mode Background', key: 'onaraBgOpacity', label: 'Onara Modal Backdrop Darkness', min: 0.00, max: 1.00, step: 0.01, unit: '' },
|
||||||
@@ -5699,8 +5959,9 @@ window.cancelAnimFrame = (function () {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="f0ck-tuner-mode-bar" style="display: flex; gap: 6px; padding: 6px 12px; background: rgba(255,255,255,0.04); border-bottom: 1px solid rgba(255,255,255,0.08);">
|
<div class="f0ck-tuner-mode-bar" style="display: flex; gap: 6px; padding: 6px 12px; background: rgba(255,255,255,0.04); border-bottom: 1px solid rgba(255,255,255,0.08);">
|
||||||
<button type="button" id="bg-mode-btn-standard" class="f0ck-tuner-subtab-btn active" style="flex: 1; padding: 6px 10px; font-size: 11px; text-align: center;">Standard View</button>
|
<button type="button" id="bg-mode-btn-standard" class="f0ck-tuner-mode-btn active" style="flex: 1; padding: 6px 10px; font-size: 11px; text-align: center;">Standard View</button>
|
||||||
<button type="button" id="bg-mode-btn-onara" class="f0ck-tuner-subtab-btn" style="flex: 1; padding: 6px 10px; font-size: 11px; text-align: center;"><i class="fa-solid fa-expand"></i> Onara Mode Only</button>
|
<button type="button" id="bg-mode-btn-onara" class="f0ck-tuner-mode-btn" style="flex: 1; padding: 6px 10px; font-size: 11px; text-align: center;"><i class="fa-solid fa-expand"></i> Onara Mode Only</button>
|
||||||
|
<button type="button" id="bg-mode-btn-audio" class="f0ck-tuner-mode-btn" style="flex: 1; padding: 6px 10px; font-size: 11px; text-align: center;"><i class="fa-solid fa-music"></i> Audio</button>
|
||||||
</div>
|
</div>
|
||||||
${bgRowsHtml}
|
${bgRowsHtml}
|
||||||
</div>
|
</div>
|
||||||
@@ -5708,8 +5969,9 @@ window.cancelAnimFrame = (function () {
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
// ── Sub-Tab Switching Logic ─────────────────────────────────────────────
|
// ── Sub-Tab Switching Logic ─────────────────────────────────────────────
|
||||||
const subtabButtons = panel.querySelectorAll('.f0ck-tuner-subtab-btn');
|
const subtabButtons = panel.querySelectorAll('.f0ck-tuner-subtabs-nav .f0ck-tuner-subtab-btn');
|
||||||
const switchSubtab = (targetTab) => {
|
const switchSubtab = (targetTab) => {
|
||||||
|
if (!targetTab) return;
|
||||||
subtabButtons.forEach(btn => {
|
subtabButtons.forEach(btn => {
|
||||||
btn.classList.toggle('active', btn.dataset.subtab === targetTab);
|
btn.classList.toggle('active', btn.dataset.subtab === targetTab);
|
||||||
});
|
});
|
||||||
@@ -5724,7 +5986,9 @@ window.cancelAnimFrame = (function () {
|
|||||||
};
|
};
|
||||||
|
|
||||||
subtabButtons.forEach(btn => {
|
subtabButtons.forEach(btn => {
|
||||||
btn.addEventListener('click', () => switchSubtab(btn.dataset.subtab));
|
btn.addEventListener('click', () => {
|
||||||
|
if (btn.dataset.subtab) switchSubtab(btn.dataset.subtab);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const savedSubtab = localStorage.getItem('f0ck_tuner_active_subtab');
|
const savedSubtab = localStorage.getItem('f0ck_tuner_active_subtab');
|
||||||
@@ -6323,18 +6587,29 @@ window.cancelAnimFrame = (function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── Background Sliders Event Listeners (Standard vs Onara mode) ────────
|
// ── Background Sliders Event Listeners (Standard vs Onara vs Audio mode) ────────
|
||||||
let activeBgTunerMode = isCurrentlyInOnara() ? 'onara' : 'standard';
|
let activeBgTunerMode = isCurrentItemAudio() ? 'audio' : (isCurrentlyInOnara() ? 'onara' : 'standard');
|
||||||
|
|
||||||
const syncBgSlidersDisplay = () => {
|
const syncBgSlidersDisplay = () => {
|
||||||
const isO = activeBgTunerMode === 'onara';
|
const mode = activeBgTunerMode;
|
||||||
const sourceObj = isO ? (window.onaraTuning || DEFAULT_ONARA_TUNING) : (window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING);
|
let sourceObj, defaultObj;
|
||||||
const defaultObj = isO ? DEFAULT_ONARA_TUNING : DEFAULT_AUDIO_TUNING;
|
if (mode === 'audio') {
|
||||||
|
sourceObj = window.audioBgTuning || DEFAULT_AUDIO_BG_TUNING;
|
||||||
|
defaultObj = DEFAULT_AUDIO_BG_TUNING;
|
||||||
|
} else if (mode === 'onara') {
|
||||||
|
sourceObj = window.onaraTuning || DEFAULT_ONARA_TUNING;
|
||||||
|
defaultObj = DEFAULT_ONARA_TUNING;
|
||||||
|
} else {
|
||||||
|
sourceObj = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
|
||||||
|
defaultObj = DEFAULT_AUDIO_TUNING;
|
||||||
|
}
|
||||||
|
|
||||||
const stdBtn = panel.querySelector('#bg-mode-btn-standard');
|
const stdBtn = panel.querySelector('#bg-mode-btn-standard');
|
||||||
const onaraBtn = panel.querySelector('#bg-mode-btn-onara');
|
const onaraBtn = panel.querySelector('#bg-mode-btn-onara');
|
||||||
if (stdBtn) stdBtn.classList.toggle('active', !isO);
|
const audioBtn = panel.querySelector('#bg-mode-btn-audio');
|
||||||
if (onaraBtn) onaraBtn.classList.toggle('active', isO);
|
if (stdBtn) stdBtn.classList.toggle('active', mode === 'standard');
|
||||||
|
if (onaraBtn) onaraBtn.classList.toggle('active', mode === 'onara');
|
||||||
|
if (audioBtn) audioBtn.classList.toggle('active', mode === 'audio');
|
||||||
|
|
||||||
backgroundSliders.forEach(s => {
|
backgroundSliders.forEach(s => {
|
||||||
const input = panel.querySelector(`#input-bg-${s.key}`);
|
const input = panel.querySelector(`#input-bg-${s.key}`);
|
||||||
@@ -6370,14 +6645,32 @@ window.cancelAnimFrame = (function () {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
panel.querySelector('#bg-mode-btn-audio')?.addEventListener('click', () => {
|
||||||
|
activeBgTunerMode = 'audio';
|
||||||
|
syncBgSlidersDisplay();
|
||||||
|
applyBackgroundOpacitySettings(window.audioBgTuning, 'audio');
|
||||||
|
if (typeof window.flashMessage === 'function') {
|
||||||
|
window.flashMessage('Tuning Audio Background', 1500, 'info');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
backgroundSliders.forEach(s => {
|
backgroundSliders.forEach(s => {
|
||||||
const input = panel.querySelector(`#input-bg-${s.key}`);
|
const input = panel.querySelector(`#input-bg-${s.key}`);
|
||||||
const valEl = panel.querySelector(`#val-bg-${s.key}`);
|
const valEl = panel.querySelector(`#val-bg-${s.key}`);
|
||||||
if (!input || !valEl) return;
|
if (!input || !valEl) return;
|
||||||
input.addEventListener('input', () => {
|
input.addEventListener('input', () => {
|
||||||
const isO = activeBgTunerMode === 'onara';
|
const mode = activeBgTunerMode;
|
||||||
const targetObj = isO ? window.onaraTuning : window.audioVisualizerTuning;
|
let targetObj, storageKey;
|
||||||
const storageKey = isO ? 'f0ck_onara_tuning' : 'f0ck_audio_tuning';
|
if (mode === 'audio') {
|
||||||
|
targetObj = window.audioBgTuning;
|
||||||
|
storageKey = 'f0ck_audio_bg_tuning';
|
||||||
|
} else if (mode === 'onara') {
|
||||||
|
targetObj = window.onaraTuning;
|
||||||
|
storageKey = 'f0ck_onara_tuning';
|
||||||
|
} else {
|
||||||
|
targetObj = window.audioVisualizerTuning;
|
||||||
|
storageKey = 'f0ck_audio_tuning';
|
||||||
|
}
|
||||||
|
|
||||||
if (s.type === 'color') {
|
if (s.type === 'color') {
|
||||||
targetObj[s.key] = input.value;
|
targetObj[s.key] = input.value;
|
||||||
@@ -6389,7 +6682,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
valEl.textContent = `${displayVal}${s.unit || ''}`;
|
valEl.textContent = `${displayVal}${s.unit || ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
applyBackgroundOpacitySettings(targetObj, isO);
|
applyBackgroundOpacitySettings(targetObj, mode === 'audio' ? 'audio' : (mode === 'onara'));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
localStorage.setItem(storageKey, JSON.stringify(targetObj));
|
localStorage.setItem(storageKey, JSON.stringify(targetObj));
|
||||||
@@ -6398,8 +6691,18 @@ window.cancelAnimFrame = (function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
panel.querySelector('#bg-tuner-reset')?.addEventListener('click', () => {
|
panel.querySelector('#bg-tuner-reset')?.addEventListener('click', () => {
|
||||||
const isO = activeBgTunerMode === 'onara';
|
const mode = activeBgTunerMode;
|
||||||
if (isO) {
|
if (mode === 'audio') {
|
||||||
|
window.audioBgTuning = Object.assign({}, DEFAULT_AUDIO_BG_TUNING);
|
||||||
|
try {
|
||||||
|
localStorage.setItem('f0ck_audio_bg_tuning', JSON.stringify(window.audioBgTuning));
|
||||||
|
} catch (e) {}
|
||||||
|
syncBgSlidersDisplay();
|
||||||
|
applyBackgroundOpacitySettings(window.audioBgTuning, 'audio');
|
||||||
|
if (typeof window.flashMessage === 'function') {
|
||||||
|
window.flashMessage('Audio background settings reset to defaults', 2000, 'info');
|
||||||
|
}
|
||||||
|
} else if (mode === 'onara') {
|
||||||
window.onaraTuning = Object.assign({}, DEFAULT_ONARA_TUNING);
|
window.onaraTuning = Object.assign({}, DEFAULT_ONARA_TUNING);
|
||||||
try {
|
try {
|
||||||
localStorage.setItem('f0ck_onara_tuning', JSON.stringify(window.onaraTuning));
|
localStorage.setItem('f0ck_onara_tuning', JSON.stringify(window.onaraTuning));
|
||||||
@@ -6426,15 +6729,15 @@ window.cancelAnimFrame = (function () {
|
|||||||
});
|
});
|
||||||
|
|
||||||
panel.querySelector('#bg-tuner-copy')?.addEventListener('click', () => {
|
panel.querySelector('#bg-tuner-copy')?.addEventListener('click', () => {
|
||||||
const isO = activeBgTunerMode === 'onara';
|
const mode = activeBgTunerMode;
|
||||||
const targetObj = isO ? window.onaraTuning : window.audioVisualizerTuning;
|
const targetObj = mode === 'audio' ? window.audioBgTuning : (mode === 'onara' ? window.onaraTuning : window.audioVisualizerTuning);
|
||||||
const bgCfg = {};
|
const bgCfg = {};
|
||||||
backgroundSliders.forEach(s => {
|
backgroundSliders.forEach(s => {
|
||||||
bgCfg[s.key] = targetObj[s.key];
|
bgCfg[s.key] = targetObj[s.key];
|
||||||
});
|
});
|
||||||
navigator.clipboard.writeText(JSON.stringify(bgCfg, null, 2)).then(() => {
|
navigator.clipboard.writeText(JSON.stringify(bgCfg, null, 2)).then(() => {
|
||||||
if (typeof window.flashMessage === 'function') {
|
if (typeof window.flashMessage === 'function') {
|
||||||
window.flashMessage(`${isO ? 'Onara' : 'Standard'} background settings copied to clipboard!`, 2500, 'success');
|
window.flashMessage(`${mode === 'audio' ? 'Audio' : (mode === 'onara' ? 'Onara' : 'Standard')} background settings copied to clipboard!`, 2500, 'success');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -7359,7 +7662,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
const draw = (data) => {
|
const draw = (data) => {
|
||||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||||
const count = analyser.frequencyBinCount;
|
const count = analyser.frequencyBinCount;
|
||||||
const cfg = window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
|
const cfg = (typeof window.getEffectiveBackgroundTuning === 'function' ? window.getEffectiveBackgroundTuning() : null) || window.audioVisualizerTuning || DEFAULT_AUDIO_TUNING;
|
||||||
|
|
||||||
if (!audioElement.paused && Number(cfg.enableBeatHue) === 1) {
|
if (!audioElement.paused && Number(cfg.enableBeatHue) === 1) {
|
||||||
const now = performance.now();
|
const now = performance.now();
|
||||||
@@ -9034,9 +9337,12 @@ window.cancelAnimFrame = (function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Feed the inner eye canvas (.audio-eye-inner-canvas) as background (#bg)
|
// Feed visualizer canvas (.audio-visualizer) or inner eye canvas as background (#bg)
|
||||||
const bgCanvas = document.getElementById('bg');
|
const bgCanvas = document.getElementById('bg');
|
||||||
const innerEyeCanvas = document.querySelector('.audio-eye-inner-canvas');
|
const innerEyeCanvas = document.querySelector('.audio-eye-inner-canvas');
|
||||||
|
const sourceCanvas = (innerEyeCanvas && innerEyeCanvas.width > 0 && innerEyeCanvas.height > 0)
|
||||||
|
? innerEyeCanvas
|
||||||
|
: (canvas && canvas.width > 0 && canvas.height > 0 ? canvas : null);
|
||||||
const bgEnabled = window.background;
|
const bgEnabled = window.background;
|
||||||
if (bgCanvas) {
|
if (bgCanvas) {
|
||||||
if (!bgEnabled) {
|
if (!bgEnabled) {
|
||||||
@@ -9045,7 +9351,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
bgCanvas.classList.remove('fader-in');
|
bgCanvas.classList.remove('fader-in');
|
||||||
bgCanvas.classList.add('fader-out');
|
bgCanvas.classList.add('fader-out');
|
||||||
}
|
}
|
||||||
} else if (feedToBg && innerEyeCanvas && innerEyeCanvas.width > 0 && innerEyeCanvas.height > 0) {
|
} else if (feedToBg && sourceCanvas && sourceCanvas.width > 0 && sourceCanvas.height > 0) {
|
||||||
const bgCtx = bgCanvas.getContext('2d');
|
const bgCtx = bgCanvas.getContext('2d');
|
||||||
if (bgCtx) {
|
if (bgCtx) {
|
||||||
const SCALE = 0.5;
|
const SCALE = 0.5;
|
||||||
@@ -9067,35 +9373,136 @@ window.cancelAnimFrame = (function () {
|
|||||||
|
|
||||||
bgCtx.clearRect(0, 0, bw, bh);
|
bgCtx.clearRect(0, 0, bw, bh);
|
||||||
|
|
||||||
|
// Compute real-time audio dynamics from frequency spectrum
|
||||||
|
const reactMult = cfg.bgAudioReactivity !== undefined ? Number(cfg.bgAudioReactivity) : 1.50;
|
||||||
|
const reactScale = reactMult / 1.50;
|
||||||
|
|
||||||
|
let dynBassMax = 0, dynBassSum = 0;
|
||||||
|
const dynBassBins = Math.min(10, count);
|
||||||
|
for (let b = 1; b <= dynBassBins; b++) {
|
||||||
|
const v = data[b] || 0;
|
||||||
|
if (v > dynBassMax) dynBassMax = v;
|
||||||
|
dynBassSum += v;
|
||||||
|
}
|
||||||
|
const dynBassPeak = dynBassMax / 255;
|
||||||
|
const dynBassAvg = dynBassSum / (dynBassBins * 255);
|
||||||
|
const dynKick = Math.min(1.0, (dynBassPeak * 0.70 + dynBassAvg * 0.30) * 1.85 * reactScale);
|
||||||
|
|
||||||
|
let dynMidMax = 0, dynMidSum = 0;
|
||||||
|
const dynMidStart = 11, dynMidEnd = Math.min(36, count);
|
||||||
|
for (let m = dynMidStart; m < dynMidEnd; m++) {
|
||||||
|
const v = data[m] || 0;
|
||||||
|
if (v > dynMidMax) dynMidMax = v;
|
||||||
|
dynMidSum += v;
|
||||||
|
}
|
||||||
|
const dynMid = Math.min(1.0, ((dynMidMax / 255) * 0.6 + (dynMidSum / ((dynMidEnd - dynMidStart) * 255)) * 0.4) * 1.85 * reactScale);
|
||||||
|
|
||||||
|
let dynHighMax = 0;
|
||||||
|
const dynHighStart = 37, dynHighEnd = Math.min(80, count);
|
||||||
|
for (let h = dynHighStart; h < dynHighEnd; h++) {
|
||||||
|
if ((data[h] || 0) > dynHighMax) dynHighMax = data[h];
|
||||||
|
}
|
||||||
|
const dynHigh = Math.min(1.0, (dynHighMax / 255) * 1.6 * reactScale);
|
||||||
|
|
||||||
|
if (!bgCanvas._audioEnvelope) {
|
||||||
|
bgCanvas._audioEnvelope = { kick: 0, mid: 0, high: 0, pulse: 0 };
|
||||||
|
}
|
||||||
|
const aEnv = bgCanvas._audioEnvelope;
|
||||||
|
const isPaused = audioElement.paused;
|
||||||
|
if (isPaused) {
|
||||||
|
aEnv.kick *= 0.85;
|
||||||
|
aEnv.mid *= 0.85;
|
||||||
|
aEnv.high *= 0.85;
|
||||||
|
aEnv.pulse *= 0.85;
|
||||||
|
} else {
|
||||||
|
aEnv.kick += (dynKick - aEnv.kick) * (dynKick > aEnv.kick ? 0.65 : 0.12);
|
||||||
|
aEnv.mid += (dynMid - aEnv.mid) * (dynMid > aEnv.mid ? 0.55 : 0.10);
|
||||||
|
aEnv.high += (dynHigh - aEnv.high) * (dynHigh > aEnv.high ? 0.70 : 0.14);
|
||||||
|
const rawPulse = Math.min(1.0, aEnv.kick * 0.70 + aEnv.mid * 0.30);
|
||||||
|
aEnv.pulse += (rawPulse - aEnv.pulse) * (rawPulse > aEnv.pulse ? 0.60 : 0.10);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check ambient lighting mode
|
||||||
|
const useAudioAmbient = Number(cfg.bgAmbientLighting) === 1;
|
||||||
|
|
||||||
|
if (useAudioAmbient) {
|
||||||
|
// Reactive YouTube Glow mode: sample colors + modulate dynamically with frequencies & beats
|
||||||
|
const pulseVal = Math.min(1.0, aEnv.pulse * reactScale);
|
||||||
|
const reactiveOpts = {
|
||||||
|
pulse: isPaused ? 0 : pulseVal,
|
||||||
|
intensityMult: isPaused ? 0.40 : (0.75 + (aEnv.pulse * 0.85 + aEnv.kick * 0.55) * reactScale),
|
||||||
|
spreadMult: isPaused ? 0.75 : (0.85 + aEnv.pulse * 0.45 * reactScale),
|
||||||
|
coreColor: null
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!bgCanvas._audioAmbientCount) bgCanvas._audioAmbientCount = 0;
|
||||||
|
bgCanvas._audioAmbientCount++;
|
||||||
|
if (bgCanvas._audioAmbientCount % 2 === 0 || !mediaAmbientState.hasSampled) {
|
||||||
|
let sampled = null;
|
||||||
|
if (audioCoverImg && audioCoverImg.complete && audioCoverImg.naturalWidth > 0) {
|
||||||
|
sampled = sampleMediaSpatialColors(audioCoverImg);
|
||||||
|
}
|
||||||
|
if (!sampled && sourceCanvas) {
|
||||||
|
sampled = sampleMediaSpatialColors(sourceCanvas);
|
||||||
|
}
|
||||||
|
if (sampled && (sampled.top[0] > 30 || sampled.top[1] > 30 || sampled.top[2] > 45 || sampled.bottom[0] > 30 || sampled.bottom[1] > 30 || sampled.bottom[2] > 45)) {
|
||||||
|
// Modulate sampled colors dynamically with real-time frequency energy
|
||||||
|
const modTop = [
|
||||||
|
Math.min(255, Math.max(0, Math.round(sampled.top[0] * (1.0 + (aEnv.mid + aEnv.high) * 0.50 * reactScale)))),
|
||||||
|
Math.min(255, Math.max(0, Math.round(sampled.top[1] * (1.0 + (aEnv.mid + aEnv.high) * 0.50 * reactScale)))),
|
||||||
|
Math.min(255, Math.max(0, Math.round(sampled.top[2] * (1.0 + (aEnv.mid + aEnv.high) * 0.50 * reactScale))))
|
||||||
|
];
|
||||||
|
const modBot = [
|
||||||
|
Math.min(255, Math.max(0, Math.round(sampled.bottom[0] * (1.0 + aEnv.kick * 0.65 * reactScale)))),
|
||||||
|
Math.min(255, Math.max(0, Math.round(sampled.bottom[1] * (1.0 + aEnv.kick * 0.65 * reactScale)))),
|
||||||
|
Math.min(255, Math.max(0, Math.round(sampled.bottom[2] * (1.0 + aEnv.kick * 0.65 * reactScale))))
|
||||||
|
];
|
||||||
|
setAmbientTargetColors({ top: modTop, bottom: modBot });
|
||||||
|
mediaAmbientState.hasSampled = true;
|
||||||
|
} else {
|
||||||
|
// Synthesize harmonic audio colors from beat hue or accent
|
||||||
|
const baseHue = (Number(cfg.enableBeatHue) === 1 ? currentBeatHue : (currentBeatHue || 110)) % 360;
|
||||||
|
const topHue = (baseHue + 45) % 360;
|
||||||
|
const botHue = baseHue;
|
||||||
|
const modTop = parseCssColorToRgb(`hsl(${Math.round(topHue)}, 100%, ${Math.round(50 + (aEnv.mid + aEnv.high) * 20 * reactScale)}%)`);
|
||||||
|
const modBot = parseCssColorToRgb(`hsl(${Math.round(botHue)}, 100%, ${Math.round(45 + aEnv.kick * 25 * reactScale)}%)`);
|
||||||
|
setAmbientTargetColors({ top: modTop, bottom: modBot });
|
||||||
|
mediaAmbientState.hasSampled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const current = updateAmbientLerp(0.08);
|
||||||
|
renderAmbientLightingWash(bgCtx, bw, bh, current, 1.0, reactiveOpts);
|
||||||
|
bgCanvas.style.filter = '';
|
||||||
|
} else {
|
||||||
|
// Filter mode: copy visualizer canvas to bg with reactive blur/brightness & pulse scale
|
||||||
try {
|
try {
|
||||||
// Draw inner eye canvas centered behind the item content (respecting sidebar width)
|
const srcW = sourceCanvas.width;
|
||||||
const srcW = innerEyeCanvas.width;
|
const srcH = sourceCanvas.height;
|
||||||
const srcH = innerEyeCanvas.height;
|
const baseBgScale = cfg.bgCanvasScale !== undefined ? Number(cfg.bgCanvasScale) : 2.15;
|
||||||
const bgScale = cfg.bgCanvasScale !== undefined ? Number(cfg.bgCanvasScale) : 2.15;
|
const bgScale = baseBgScale * (1.0 + (isPaused ? 0 : aEnv.pulse * 0.15 * reactScale));
|
||||||
|
|
||||||
// #bg canvas is already positioned to the free space respecting the sidebar
|
|
||||||
const contentW = bw;
|
const contentW = bw;
|
||||||
|
|
||||||
const fitScale = Math.min(contentW / srcW, bh / srcH);
|
const fitScale = Math.min(contentW / srcW, bh / srcH);
|
||||||
const dw = srcW * fitScale * bgScale;
|
const dw = srcW * fitScale * bgScale;
|
||||||
const dh = srcH * fitScale * bgScale;
|
const dh = srcH * fitScale * bgScale;
|
||||||
const dx = (contentW - dw) / 2;
|
const dx = (contentW - dw) / 2;
|
||||||
const dy = (bh - dh) / 2;
|
const dy = (bh - dh) / 2;
|
||||||
|
|
||||||
// Apply blur & brightness via CSS filter (smooth at display resolution)
|
|
||||||
const blurPx = cfg.bgCanvasBlur !== undefined ? Number(cfg.bgCanvasBlur) : 31;
|
const blurPx = cfg.bgCanvasBlur !== undefined ? Number(cfg.bgCanvasBlur) : 31;
|
||||||
const brightVal = cfg.bgCanvasBrightness !== undefined ? Number(cfg.bgCanvasBrightness) : 0.6;
|
const baseBright = cfg.bgCanvasBrightness !== undefined ? Number(cfg.bgCanvasBrightness) : 0.6;
|
||||||
|
const brightVal = Math.min(2.0, baseBright * (1.0 + (isPaused ? 0 : aEnv.kick * 0.45 * reactScale)));
|
||||||
const filterParts = [];
|
const filterParts = [];
|
||||||
if (blurPx > 0) filterParts.push(`blur(${blurPx}px)`);
|
if (blurPx > 0) filterParts.push(`blur(${blurPx}px)`);
|
||||||
if (brightVal !== 1) filterParts.push(`brightness(${brightVal})`);
|
if (brightVal !== 1) filterParts.push(`brightness(${brightVal.toFixed(2)})`);
|
||||||
bgCanvas.style.filter = filterParts.length > 0 ? filterParts.join(' ') : '';
|
bgCanvas.style.filter = filterParts.length > 0 ? filterParts.join(' ') : '';
|
||||||
|
|
||||||
bgCtx.drawImage(innerEyeCanvas, dx, dy, dw, dh);
|
bgCtx.drawImage(sourceCanvas, dx, dy, dw, dh);
|
||||||
} catch (e) {}
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const loopingFunction = () => {
|
const loopingFunction = () => {
|
||||||
@@ -11133,6 +11540,26 @@ window.cancelAnimFrame = (function () {
|
|||||||
|
|
||||||
window.f0ckDebug("[itemCache] MISS — fetching:", ajaxUrl);
|
window.f0ckDebug("[itemCache] MISS — fetching:", ajaxUrl);
|
||||||
const tStart = performance.now();
|
const tStart = performance.now();
|
||||||
|
|
||||||
|
let html, paginationHtml, responseSlug = null, responsePage = null;
|
||||||
|
|
||||||
|
// ── Pre-fetched data path (merged random + item load) ──────────────
|
||||||
|
if (options.prefetchedData) {
|
||||||
|
const data = options.prefetchedData;
|
||||||
|
if (data && data.success === false) {
|
||||||
|
console.warn("loadItemAjax: Pre-fetched data returned failure:", data.message);
|
||||||
|
window.location.href = url;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (data && typeof data.html === 'string') {
|
||||||
|
html = data.html;
|
||||||
|
paginationHtml = data.pagination;
|
||||||
|
responseSlug = data.slug || data.item?.slug || null;
|
||||||
|
responsePage = data.page || null;
|
||||||
|
}
|
||||||
|
window.f0ckDebug(`[CLIENT_DEBUG] Using pre-fetched data (skipped network fetch)`);
|
||||||
|
} else {
|
||||||
|
// ── Normal fetch path ────────────────────────────────────────────
|
||||||
const response = await fetch(ajaxUrl, { credentials: 'include' });
|
const response = await fetch(ajaxUrl, { credentials: 'include' });
|
||||||
const tHeaders = performance.now();
|
const tHeaders = performance.now();
|
||||||
|
|
||||||
@@ -11147,8 +11574,6 @@ window.cancelAnimFrame = (function () {
|
|||||||
- Total Network: ${(tBody - tStart).toFixed(2)}ms
|
- Total Network: ${(tBody - tStart).toFixed(2)}ms
|
||||||
- Content Size: ${(rawText.length / 1024).toFixed(2)} KB`);
|
- Content Size: ${(rawText.length / 1024).toFixed(2)} KB`);
|
||||||
|
|
||||||
let html, paginationHtml, responseSlug = null, responsePage = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Optimistically try to parse as JSON first
|
// Optimistically try to parse as JSON first
|
||||||
const data = JSON.parse(rawText);
|
const data = JSON.parse(rawText);
|
||||||
@@ -11169,6 +11594,7 @@ window.cancelAnimFrame = (function () {
|
|||||||
// If JSON parse fails, assume it's HTML text
|
// If JSON parse fails, assume it's HTML text
|
||||||
html = rawText;
|
html = rawText;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ── Store in item cache (stale-while-revalidate) ───────────────────────
|
// ── Store in item cache (stale-while-revalidate) ───────────────────────
|
||||||
if (html && !options.noCacheStore) {
|
if (html && !options.noCacheStore) {
|
||||||
@@ -11557,24 +11983,98 @@ window.cancelAnimFrame = (function () {
|
|||||||
const nav = document.querySelector("nav.navbar");
|
const nav = document.querySelector("nav.navbar");
|
||||||
if (nav) nav.classList.add("pbwork");
|
if (nav) nav.classList.add("pbwork");
|
||||||
|
|
||||||
// Extract current context from window location
|
// ── Client-side random pool: pick locally, load normally ──────
|
||||||
let randomUrl = '/api/v2/random';
|
|
||||||
const params = new URLSearchParams();
|
|
||||||
|
|
||||||
const wTagMatch = window.location.href.match(/\/tag\/([^/]+)/);
|
const wTagMatch = window.location.href.match(/\/tag\/([^/]+)/);
|
||||||
if (wTagMatch) params.append('tag', decodeURIComponent(wTagMatch[1]));
|
const wHallMatch = window.location.href.match(/\/h\/([^/]+)/);
|
||||||
|
|
||||||
// Check for user hall FIRST — if we're in a user hall, don't also send user=
|
|
||||||
// (the /user/:name part of the URL would otherwise incorrectly trigger user= filter)
|
|
||||||
let wUserHall = null, wUserHallOwner = null;
|
let wUserHall = null, wUserHallOwner = null;
|
||||||
const wUserHallMatch = window.location.href.match(/\/user\/([^/]+)\/hall\/([^/]+)/);
|
const wUserHallMatch = window.location.href.match(/\/user\/([^/]+)\/hall\/([^/]+)/);
|
||||||
if (wUserHallMatch) {
|
if (wUserHallMatch) {
|
||||||
wUserHallOwner = decodeURIComponent(wUserHallMatch[1]);
|
wUserHallOwner = decodeURIComponent(wUserHallMatch[1]);
|
||||||
wUserHall = decodeURIComponent(wUserHallMatch[2]);
|
wUserHall = decodeURIComponent(wUserHallMatch[2]);
|
||||||
|
}
|
||||||
|
let wFavsUser = null;
|
||||||
|
if (!wUserHall) {
|
||||||
|
const wUserM = window.location.href.match(/\/user\/([^/]+)/);
|
||||||
|
if (wUserM && window.location.href.match(/\/favs(\/|$|\?)/)) {
|
||||||
|
wFavsUser = decodeURIComponent(wUserM[1]);
|
||||||
|
} else if (window.location.href.match(/\/favs(\/|$|\?)/)) {
|
||||||
|
const guestIds = window.f0ckGuestFavs ? window.f0ckGuestFavs.get() : [];
|
||||||
|
if (guestIds.length > 0) {
|
||||||
|
const currentId = window.getCurrentItemId();
|
||||||
|
const candidates = guestIds.filter(id => id !== parseInt(currentId, 10));
|
||||||
|
const chosen = candidates.length > 0 ? candidates[Math.floor(Math.random() * candidates.length)] : guestIds[0];
|
||||||
|
loadItemAjax(`/favs/${chosen}`, true, { transition: 'fade-zoom' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try pool-based random (instant pick from cached IDs)
|
||||||
|
const pool = window._randomPool;
|
||||||
|
const currentSlug = window.getCurrentItemSlug ? window.getCurrentItemSlug() : null;
|
||||||
|
const currentId = window.getCurrentItemId ? String(window.getCurrentItemId()) : null;
|
||||||
|
|
||||||
|
// Pool loaded but empty — no items match current filters
|
||||||
|
if (pool && pool.items && pool.items.length === 0) {
|
||||||
|
if (nav) nav.classList.remove("pbwork");
|
||||||
|
outEls.forEach(el => {
|
||||||
|
el.classList.remove('fade-out-zoom');
|
||||||
|
el.classList.add('fade-in-zoom');
|
||||||
|
});
|
||||||
|
if (canvas) {
|
||||||
|
canvas.classList.remove('fader-out', 'fast-fade');
|
||||||
|
canvas.classList.add('fader-in');
|
||||||
|
}
|
||||||
|
if (window.flashMessage) window.flashMessage('No items found for this filter');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pool && pool.items && pool.items.length > 1) {
|
||||||
|
// Pick next from shuffled pool — zero duplicates until all seen
|
||||||
|
const pick = window.pickFromPool(currentSlug, currentId);
|
||||||
|
if (!pick) { // shouldn't happen, but safety
|
||||||
|
if (window.flashMessage) window.flashMessage('No items found for this filter');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Flag for sidebar: skip tag feed re-centering on random nav
|
||||||
|
window._isPoolRandomNav = true;
|
||||||
|
|
||||||
|
// Build URL preserving context
|
||||||
|
let targetUrl;
|
||||||
|
if (wUserHall && wUserHallOwner) {
|
||||||
|
targetUrl = `/user/${encodeURIComponent(wUserHallOwner)}/hall/${encodeURIComponent(wUserHall)}/${pick}`;
|
||||||
|
} else if (wFavsUser) {
|
||||||
|
targetUrl = `/user/${encodeURIComponent(wFavsUser)}/favs/${pick}`;
|
||||||
|
} else if (wTagMatch) {
|
||||||
|
const wTag = decodeURIComponent(wTagMatch[1]);
|
||||||
|
targetUrl = `/tag/${encodeURIComponent(wTag).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${pick}`;
|
||||||
|
} else if (wHallMatch) {
|
||||||
|
const wHall = decodeURIComponent(wHallMatch[1]);
|
||||||
|
targetUrl = `/h/${encodeURIComponent(wHall).replace(/%20/g, ' ')}/${pick}`;
|
||||||
|
} else {
|
||||||
|
targetUrl = `/${pick}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadItemAjax(targetUrl, true, { transition: 'fade-zoom' });
|
||||||
|
|
||||||
|
// Background grid sync
|
||||||
|
if (isOnaraActive()) {
|
||||||
|
syncOnaraBackgroundGrid(pick, `/${pick}`, 1, pick);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: pool not ready — use server-side random
|
||||||
|
let randomUrl = '/ajax/item/random';
|
||||||
|
const params = new URLSearchParams();
|
||||||
|
params.append('mode', window.activeMode);
|
||||||
|
|
||||||
|
if (wTagMatch) params.append('tag', decodeURIComponent(wTagMatch[1]));
|
||||||
|
|
||||||
|
if (wUserHallMatch) {
|
||||||
params.append('userHall', wUserHall);
|
params.append('userHall', wUserHall);
|
||||||
params.append('userHallOwner', wUserHallOwner);
|
params.append('userHallOwner', wUserHallOwner);
|
||||||
} else {
|
} else {
|
||||||
// Only add user= when NOT in a user hall context
|
|
||||||
const wUserMatch = window.location.href.match(/\/user\/([^/]+)/);
|
const wUserMatch = window.location.href.match(/\/user\/([^/]+)/);
|
||||||
if (wUserMatch) {
|
if (wUserMatch) {
|
||||||
params.append('user', decodeURIComponent(wUserMatch[1]));
|
params.append('user', decodeURIComponent(wUserMatch[1]));
|
||||||
@@ -11584,7 +12084,6 @@ window.cancelAnimFrame = (function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const wHallMatch = window.location.href.match(/\/h\/([^/]+)/);
|
|
||||||
if (wHallMatch) params.append('hall', decodeURIComponent(wHallMatch[1]));
|
if (wHallMatch) params.append('hall', decodeURIComponent(wHallMatch[1]));
|
||||||
|
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
@@ -11610,78 +12109,42 @@ window.cancelAnimFrame = (function () {
|
|||||||
randomUrl += '?' + params.toString();
|
randomUrl += '?' + params.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Capture favs user context before fetch so it's accessible in .then()
|
fetch(randomUrl, { credentials: 'include' })
|
||||||
// Without this, random in /user/foo/favs would navigate to /:id (losing context),
|
|
||||||
// causing mode mismatches between the AJAX pick and any subsequent full-page reload.
|
|
||||||
let wFavsUser = null;
|
|
||||||
if (!wUserHall) {
|
|
||||||
const wUserM = window.location.href.match(/\/user\/([^/]+)/);
|
|
||||||
if (wUserM && window.location.href.match(/\/favs(\/|$|\?)/)) {
|
|
||||||
wFavsUser = decodeURIComponent(wUserM[1]);
|
|
||||||
} else if (window.location.href.match(/\/favs(\/|$|\?)/)) {
|
|
||||||
const guestIds = window.f0ckGuestFavs ? window.f0ckGuestFavs.get() : [];
|
|
||||||
if (guestIds.length > 0) {
|
|
||||||
const currentId = window.getCurrentItemId();
|
|
||||||
const candidates = guestIds.filter(id => id !== parseInt(currentId, 10));
|
|
||||||
const chosen = candidates.length > 0 ? candidates[Math.floor(Math.random() * candidates.length)] : guestIds[0];
|
|
||||||
loadItemAjax(`/favs/${chosen}`, true, { transition: 'fade-zoom' });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(randomUrl)
|
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
if (data.success && data.items && (data.items.slug || data.items.id)) {
|
if (data && typeof data.html === 'string' && !data.error) {
|
||||||
const targetKey = data.items.slug || data.items.id;
|
const targetKey = data.slug || data.id;
|
||||||
const targetId = data.items.id;
|
let targetUrl;
|
||||||
const targetPage = data.items.page || 1;
|
|
||||||
|
|
||||||
// Navigate immediately
|
|
||||||
if (wUserHall && wUserHallOwner) {
|
if (wUserHall && wUserHallOwner) {
|
||||||
loadItemAjax(`/user/${encodeURIComponent(wUserHallOwner)}/hall/${encodeURIComponent(wUserHall)}/${targetKey}`, true, { transition: 'fade-zoom' });
|
targetUrl = `/user/${encodeURIComponent(wUserHallOwner)}/hall/${encodeURIComponent(wUserHall)}/${targetKey}`;
|
||||||
} else if (wFavsUser) {
|
} else if (wFavsUser) {
|
||||||
loadItemAjax(`/user/${encodeURIComponent(wFavsUser)}/favs/${targetKey}`, true, { transition: 'fade-zoom' });
|
targetUrl = `/user/${encodeURIComponent(wFavsUser)}/favs/${targetKey}`;
|
||||||
} else if (wTagMatch) {
|
} else if (wTagMatch) {
|
||||||
const wTag = decodeURIComponent(wTagMatch[1]);
|
const wTag = decodeURIComponent(wTagMatch[1]);
|
||||||
loadItemAjax(`/tag/${encodeURIComponent(wTag).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${targetKey}`, true, { transition: 'fade-zoom' });
|
targetUrl = `/tag/${encodeURIComponent(wTag).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${targetKey}`;
|
||||||
} else if (wHallMatch) {
|
} else if (wHallMatch) {
|
||||||
const wHall = decodeURIComponent(wHallMatch[1]);
|
const wHall = decodeURIComponent(wHallMatch[1]);
|
||||||
loadItemAjax(`/h/${encodeURIComponent(wHall).replace(/%20/g, ' ')}/${targetKey}`, true, { transition: 'fade-zoom' });
|
targetUrl = `/h/${encodeURIComponent(wHall).replace(/%20/g, ' ')}/${targetKey}`;
|
||||||
} else {
|
} else {
|
||||||
loadItemAjax(`/${targetKey}`, true, { transition: 'fade-zoom' });
|
targetUrl = `/${targetKey}`;
|
||||||
}
|
}
|
||||||
|
loadItemAjax(targetUrl, true, { transition: 'fade-zoom', prefetchedData: data });
|
||||||
// Background grid sync — page number already in response, no extra fetch needed
|
if (isOnaraActive() && data.id) {
|
||||||
if (isOnaraActive() && targetId) {
|
syncOnaraBackgroundGrid(data.id, `/${targetKey}`, data.page || 1, targetKey);
|
||||||
syncOnaraBackgroundGrid(targetId, `/${targetKey}`, targetPage, targetKey);
|
|
||||||
}
|
}
|
||||||
} else if (params.has('tag') || params.has('hall') || params.has('user') || params.has('userHall')) {
|
|
||||||
// Context had no matching items with the active MIME filter — try global random with the same filter
|
|
||||||
const fallbackParams = new URLSearchParams();
|
|
||||||
const effectiveMime = params.get('mime');
|
|
||||||
if (effectiveMime) fallbackParams.append('mime', effectiveMime);
|
|
||||||
if (params.get('strict')) fallbackParams.append('strict', '1');
|
|
||||||
const fallbackUrl = '/api/v2/random' + ([...fallbackParams].length > 0 ? ('?' + fallbackParams.toString()) : '');
|
|
||||||
fetch(fallbackUrl)
|
|
||||||
.then(r => r.json())
|
|
||||||
.then(fbData => {
|
|
||||||
if (fbData.success && fbData.items && (fbData.items.slug || fbData.items.id)) {
|
|
||||||
const targetKey = fbData.items.slug || fbData.items.id;
|
|
||||||
loadItemAjax(`/${targetKey}`, true, { transition: 'fade-zoom' });
|
|
||||||
} else {
|
} else {
|
||||||
window.location.href = link.href;
|
// No items found — restore UI, don't redirect
|
||||||
}
|
if (nav) nav.classList.remove("pbwork");
|
||||||
})
|
outEls.forEach(el => { el.classList.remove('fade-out-zoom'); el.classList.add('fade-in-zoom'); });
|
||||||
.catch(() => { window.location.href = link.href; });
|
if (canvas) { canvas.classList.remove('fader-out', 'fast-fade'); canvas.classList.add('fader-in'); }
|
||||||
} else {
|
if (window.flashMessage) window.flashMessage('No items found for this filter');
|
||||||
window.location.href = link.href;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
console.error("Random fetch failed:", err);
|
console.error("Random fetch failed:", err);
|
||||||
window.location.href = link.href;
|
if (nav) nav.classList.remove("pbwork");
|
||||||
|
outEls.forEach(el => { el.classList.remove('fade-out-zoom'); el.classList.add('fade-in-zoom'); });
|
||||||
|
if (canvas) { canvas.classList.remove('fader-out', 'fast-fade'); canvas.classList.add('fader-in'); }
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1615,8 +1615,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (!matchedCard && identifiers.length > 0 && currentTag && !isReCenteringFeed && !tagFeedLoading) {
|
} else if (!matchedCard && identifiers.length > 0 && currentTag && !isReCenteringFeed && !tagFeedLoading) {
|
||||||
// Active item belongs to current tag context but is not in currently rendered slice
|
// Active item belongs to current tag context but is not in currently rendered slice.
|
||||||
// (e.g. after pressing Random in tag). Center feed around this active item!
|
// If this was a pool-based random navigation, skip reload — keep existing items visible.
|
||||||
|
if (window._isPoolRandomNav) {
|
||||||
|
window._isPoolRandomNav = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Non-pool navigation (next/prev): center feed around the active item
|
||||||
isReCenteringFeed = true;
|
isReCenteringFeed = true;
|
||||||
loadTagFeed(true, identifiers[0]).finally(() => {
|
loadTagFeed(true, identifiers[0]).finally(() => {
|
||||||
isReCenteringFeed = false;
|
isReCenteringFeed = false;
|
||||||
@@ -2030,6 +2035,17 @@
|
|||||||
|
|
||||||
// Reload recommendations or sync tag feed when user triggers Random (#random, #nav-random, or 'r' key)
|
// Reload recommendations or sync tag feed when user triggers Random (#random, #nav-random, or 'r' key)
|
||||||
const handleRandomAction = () => {
|
const handleRandomAction = () => {
|
||||||
|
// If pool-based random is active, the context hasn't changed — skip reload
|
||||||
|
if (window._randomPool && window._randomPool.items && window._randomPool.items.length > 1) {
|
||||||
|
// Context is stable (same tag/hall/user) — just highlight the new item in tag feed
|
||||||
|
const tag = getCurrentTag();
|
||||||
|
if (tag && !userManuallySelectedNonTagTab) {
|
||||||
|
// Refresh highlight in tag feed without re-fetching
|
||||||
|
setTimeout(() => highlightActiveTagCard(true), 100);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Fallback: server-side random may change context, so reload
|
||||||
recommendationsLoaded = false;
|
recommendationsLoaded = false;
|
||||||
const tag = getCurrentTag();
|
const tag = getCurrentTag();
|
||||||
if (tag) {
|
if (tag) {
|
||||||
|
|||||||
+215
-64
@@ -1236,7 +1236,7 @@ const f0cklib = {
|
|||||||
|
|
||||||
// 1. Fetch the main item
|
// 1. Fetch the main item
|
||||||
const items = await db`
|
const items = await db`
|
||||||
select distinct on (items.id)
|
select
|
||||||
items.*,
|
items.*,
|
||||||
items.username as username,
|
items.username as username,
|
||||||
uo.username_color as author_color,
|
uo.username_color as author_color,
|
||||||
@@ -1252,9 +1252,7 @@ const f0cklib = {
|
|||||||
|
|
||||||
${user_id ? db`coalesce(uvv.view_count, 0) as my_views` : db`0 as my_views`}
|
${user_id ? db`coalesce(uvv.view_count, 0) as my_views` : db`0 as my_views`}
|
||||||
from items
|
from items
|
||||||
left join favorites on favorites.item_id = items.id
|
left join "user" author_u on author_u."user" = items.username
|
||||||
left join "user" fav_u on fav_u.id = favorites.user_id
|
|
||||||
left join "user" author_u on author_u."user" = items.username or author_u.login = items.username
|
|
||||||
left join "user_options" uo on uo.user_id = author_u.id
|
left join "user_options" uo on uo.user_id = author_u.id
|
||||||
${user_id ? db`left join user_video_views uvv on uvv.video_id = items.id and uvv.user_id = ${user_id}` : db``}
|
${user_id ? db`left join user_video_views uvv on uvv.video_id = items.id and uvv.user_id = ${user_id}` : db``}
|
||||||
where
|
where
|
||||||
@@ -1408,24 +1406,65 @@ const f0cklib = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const runTimings = startTime;
|
const runTimings = startTime;
|
||||||
|
|
||||||
|
// 2. Neighbor queries — skip useless ones in random mode
|
||||||
const [nextItem, prevItem, startItem, endItem, cheatItems] = await Promise.all([
|
const [nextItem, prevItem, startItem, endItem, cheatItems] = await Promise.all([
|
||||||
random ? optimizedBaseQuery(db`and items.id != ${itemid}`, db`order by random()`) : optimizedBaseQuery(db`and items.id > ${itemid}`, db`order by items.id asc`),
|
random
|
||||||
random ? optimizedBaseQuery(db`and items.id != ${itemid}`, db`order by random()`) : optimizedBaseQuery(db`and items.id < ${itemid}`, db`order by items.id desc`),
|
? optimizedBaseQuery(db`and items.id != ${itemid}`, db`order by random()`)
|
||||||
optimizedBaseQuery(db``, db`order by items.id asc`),
|
: optimizedBaseQuery(db`and items.id > ${itemid}`, db`order by items.id asc`),
|
||||||
optimizedBaseQuery(db``, db`order by items.id desc`),
|
random
|
||||||
// Cheat items - try to get a few neighbors. Simplified: just get some newer ones
|
? Promise.resolve(null) // reuse nextItem for prev in random mode
|
||||||
optimizedBaseQuery(db`and items.id != ${itemid}`, db`order by abs(items.id - ${itemid}) asc`, 7)
|
: optimizedBaseQuery(db`and items.id < ${itemid}`, db`order by items.id desc`),
|
||||||
|
random ? Promise.resolve([]) : optimizedBaseQuery(db``, db`order by items.id asc`),
|
||||||
|
random ? Promise.resolve([]) : optimizedBaseQuery(db``, db`order by items.id desc`),
|
||||||
|
random ? Promise.resolve([]) : optimizedBaseQuery(db`and items.id != ${itemid}`, db`order by abs(items.id - ${itemid}) asc`, 7)
|
||||||
]);
|
]);
|
||||||
console.log(`[GETF0CK_OPT] Neighbor queries finished in ${Date.now() - runTimings}ms`);
|
console.log(`[GETF0CK_OPT] Neighbor queries finished in ${Date.now() - runTimings}ms (random=${!!random})`);
|
||||||
|
|
||||||
|
// For random: prev reuses nextItem result (both are random, no point running 2 ORDER BY random())
|
||||||
|
const effectivePrev = random ? nextItem : prevItem;
|
||||||
|
|
||||||
// Cheat array should include current item and neighbors, sorted
|
// Cheat array should include current item and neighbors, sorted
|
||||||
const cheat = [itemid, ...cheatItems.map(i => i.id)].sort((a, b) => a - b);
|
const cheat = [itemid, ...cheatItems.map(i => i.id)].sort((a, b) => a - b);
|
||||||
|
|
||||||
const tags = await lib.getTags(itemid, session);
|
// 3. Metadata queries — sequential to avoid pool saturation
|
||||||
const itemHalls = await db`select h.name, h.slug from halls h join halls_assign ha on ha.hall_id = h.id where ha.item_id = ${itemid}`;
|
const repostBaseChecksum = actitem.checksum
|
||||||
const userHallsForItem = user_id
|
? (actitem.checksum.includes('_bypass_') ? actitem.checksum.split('_bypass_')[0] : actitem.checksum)
|
||||||
? await db`select uh.name, uh.slug from user_halls uh join user_halls_assign uha on uha.hall_id = uh.id where uha.item_id = ${itemid} and uh.user_id = ${user_id}`
|
: null;
|
||||||
: [];
|
const hasBypass = actitem.checksum && actitem.checksum.includes('_bypass_');
|
||||||
|
|
||||||
|
const [tags, itemHalls, userHallsForItem, favorites, repostRows, phashMatches] = await Promise.all([
|
||||||
|
lib.getTags(itemid, session),
|
||||||
|
db`select h.name, h.slug from halls h join halls_assign ha on ha.hall_id = h.id where ha.item_id = ${itemid}`,
|
||||||
|
user_id
|
||||||
|
? db`select uh.name, uh.slug from user_halls uh join user_halls_assign uha on uha.hall_id = uh.id where uha.item_id = ${itemid} and uh.user_id = ${user_id}`
|
||||||
|
: [],
|
||||||
|
db`
|
||||||
|
select "favorites".user_id, "user".user, "user".login, "user_options".avatar, "user_options".avatar_file, "user_options".username_color, "user_options".display_name, "user_options".hide_fav_badge, "anon_identities".fingerprint as anon_fingerprint
|
||||||
|
from "favorites"
|
||||||
|
left join "user" on "user".id = "favorites".user_id
|
||||||
|
left join "user_options" on "user_options".user_id = "favorites".user_id
|
||||||
|
left join "anon_identities" on "anon_identities".user_id = "favorites".user_id
|
||||||
|
where "favorites".item_id = ${itemid}
|
||||||
|
`,
|
||||||
|
repostBaseChecksum
|
||||||
|
? db`
|
||||||
|
SELECT id, slug, username, stamp FROM items
|
||||||
|
WHERE active = true
|
||||||
|
AND id != ${itemid}
|
||||||
|
AND ${hasBypass
|
||||||
|
? db`(checksum = ${repostBaseChecksum} OR checksum LIKE ${repostBaseChecksum + '_bypass_%'})`
|
||||||
|
: db`checksum LIKE ${repostBaseChecksum + '_bypass_%'}`
|
||||||
|
}
|
||||||
|
ORDER BY id ASC
|
||||||
|
`
|
||||||
|
: [],
|
||||||
|
(actitem.phash && actitem.phash !== 'ERROR' && actitem.phash !== 'MISSING')
|
||||||
|
? queue.findallrepostphash(actitem.phash, itemid).catch(() => [])
|
||||||
|
: []
|
||||||
|
]);
|
||||||
|
console.log(`[GETF0CK_OPT] All queries finished in ${Date.now() - runTimings}ms`);
|
||||||
|
|
||||||
const link = lib.genLink({ user, tag, hall: (hall && typeof hall === 'object') ? hall.slug : hall, mime, type: fav ? 'favs' : 'uploads', path: '', strict: false });
|
const link = lib.genLink({ user, tag, hall: (hall && typeof hall === 'object') ? hall.slug : hall, mime, type: fav ? 'favs' : 'uploads', path: '', strict: false });
|
||||||
// Override link for title searches — pagination must use the /tag/title:... prefix
|
// Override link for title searches — pagination must use the /tag/title:... prefix
|
||||||
if (isTitleSearch && titleQuery) {
|
if (isTitleSearch && titleQuery) {
|
||||||
@@ -1442,14 +1481,6 @@ const f0cklib = {
|
|||||||
link.path = '';
|
link.path = '';
|
||||||
link.suffix = '';
|
link.suffix = '';
|
||||||
}
|
}
|
||||||
const favorites = await db`
|
|
||||||
select "favorites".user_id, "user".user, "user".login, "user_options".avatar, "user_options".avatar_file, "user_options".username_color, "user_options".display_name, "user_options".hide_fav_badge, "anon_identities".fingerprint as anon_fingerprint
|
|
||||||
from "favorites"
|
|
||||||
left join "user" on "user".id = "favorites".user_id
|
|
||||||
left join "user_options" on "user_options".user_id = "favorites".user_id
|
|
||||||
left join "anon_identities" on "anon_identities".user_id = "favorites".user_id
|
|
||||||
where "favorites".item_id = ${itemid}
|
|
||||||
`;
|
|
||||||
|
|
||||||
for (const f of favorites) {
|
for (const f of favorites) {
|
||||||
if (f.anon_fingerprint || f.user === 'anonymous' || (typeof f.user === 'string' && f.user.startsWith('anon_'))) {
|
if (f.anon_fingerprint || f.user === 'anonymous' || (typeof f.user === 'string' && f.user.startsWith('anon_'))) {
|
||||||
@@ -1464,36 +1495,9 @@ const f0cklib = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect reposts: items uploaded with bypass_duplicate_check have checksum = `{hash}_bypass_{ts}`
|
// Merge checksum + phash repost results
|
||||||
// Find all items (including this one) that share the same base checksum.
|
let repostItems = repostRows.map(r => ({ id: r.id, slug: r.slug, username: r.username, stamp: r.stamp, match_type: 'checksum' }));
|
||||||
let repostItems = [];
|
if (phashMatches.length > 0) {
|
||||||
if (actitem.checksum && actitem.checksum.includes('_bypass_')) {
|
|
||||||
const baseChecksum = actitem.checksum.split('_bypass_')[0];
|
|
||||||
const repostRows = await db`
|
|
||||||
SELECT id, slug, username, stamp FROM items
|
|
||||||
WHERE active = true
|
|
||||||
AND id != ${itemid}
|
|
||||||
AND (checksum = ${baseChecksum} OR checksum LIKE ${baseChecksum + '_bypass_%'})
|
|
||||||
ORDER BY id ASC
|
|
||||||
`;
|
|
||||||
repostItems = repostRows.map(r => ({ id: r.id, slug: r.slug, username: r.username, stamp: r.stamp, match_type: 'checksum' }));
|
|
||||||
} else if (actitem.checksum) {
|
|
||||||
// Even without bypass, check if other bypass-entries exist with this same hash
|
|
||||||
const baseChecksum = actitem.checksum;
|
|
||||||
const repostRows = await db`
|
|
||||||
SELECT id, slug, username, stamp FROM items
|
|
||||||
WHERE active = true
|
|
||||||
AND id != ${itemid}
|
|
||||||
AND checksum LIKE ${baseChecksum + '_bypass_%'}
|
|
||||||
ORDER BY id ASC
|
|
||||||
`;
|
|
||||||
repostItems = repostRows.map(r => ({ id: r.id, slug: r.slug, username: r.username, stamp: r.stamp, match_type: 'checksum' }));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Also find visually-similar items via phash, merging with checksum results
|
|
||||||
if (actitem.phash && actitem.phash !== 'ERROR' && actitem.phash !== 'MISSING') {
|
|
||||||
try {
|
|
||||||
const phashMatches = await queue.findallrepostphash(actitem.phash, itemid);
|
|
||||||
const existingIds = new Set(repostItems.map(r => r.id));
|
const existingIds = new Set(repostItems.map(r => r.id));
|
||||||
for (const pm of phashMatches) {
|
for (const pm of phashMatches) {
|
||||||
if (!existingIds.has(pm.id)) {
|
if (!existingIds.has(pm.id)) {
|
||||||
@@ -1502,9 +1506,6 @@ const f0cklib = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
repostItems.sort((a, b) => a.id - b.id);
|
repostItems.sort((a, b) => a.id - b.id);
|
||||||
} catch (e) {
|
|
||||||
console.error('[GETF0CK] phash repost lookup failed:', e.message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let hasCoverart = false;
|
let hasCoverart = false;
|
||||||
@@ -1844,7 +1845,7 @@ const f0cklib = {
|
|||||||
let anonBlocked = false;
|
let anonBlocked = false;
|
||||||
if (isNsfw && !allowedModes.includes('nsfw')) anonBlocked = true;
|
if (isNsfw && !allowedModes.includes('nsfw')) anonBlocked = true;
|
||||||
else if (isNsfl && !allowedModes.includes('nsfl')) anonBlocked = true;
|
else if (isNsfl && !allowedModes.includes('nsfl')) anonBlocked = true;
|
||||||
else if (isUntagged && !allowedModes.includes('untagged')) anonBlocked = true;
|
else if (isUntagged && !allowedModes.includes('untagged') && Number(mode ?? 0) !== 2) anonBlocked = true;
|
||||||
else if (isSfw && !allowedModes.includes('sfw')) anonBlocked = true;
|
else if (isSfw && !allowedModes.includes('sfw')) anonBlocked = true;
|
||||||
|
|
||||||
if (anonBlocked) {
|
if (anonBlocked) {
|
||||||
@@ -1867,6 +1868,7 @@ const f0cklib = {
|
|||||||
// Mode-mismatch visibility check:
|
// Mode-mismatch visibility check:
|
||||||
// Mode 0=sfw, 1=nsfw, 2=untagged, 3=all
|
// Mode 0=sfw, 1=nsfw, 2=untagged, 3=all
|
||||||
const userMode = Number(mode ?? 0);
|
const userMode = Number(mode ?? 0);
|
||||||
|
if (userMode === 2) console.log(`[MODE2-DEBUG] item=${itemid} isTagged=${isTagged} isUntagged=${isUntagged} isSfw=${isSfw} isNsfw=${isNsfw} isNsfl=${isNsfl} tags=${effectiveItemTags.map(t => t.id + ':' + t.normalized).join(',')}`);
|
||||||
if (!bypass_filter) {
|
if (!bypass_filter) {
|
||||||
let modeBlocked = false;
|
let modeBlocked = false;
|
||||||
const untaggedDisallowed = (!session && !cfg.websrv.public_untagged) || (isAnonSession(session) && !getAnonAllowedModes().includes('untagged'));
|
const untaggedDisallowed = (!session && !cfg.websrv.public_untagged) || (isAnonSession(session) && !getAnonAllowedModes().includes('untagged'));
|
||||||
@@ -1877,7 +1879,7 @@ const f0cklib = {
|
|||||||
if (userMode === 0 && (isNsfw || isNsfl || (isUntagged && untaggedDisallowed))) modeBlocked = true; // SFW mode, item is NSFW or NSFL
|
if (userMode === 0 && (isNsfw || isNsfl || (isUntagged && untaggedDisallowed))) modeBlocked = true; // SFW mode, item is NSFW or NSFL
|
||||||
else if (userMode === 1 && !isNsfw) modeBlocked = true; // NSFW mode, item is not NSFW
|
else if (userMode === 1 && !isNsfw) modeBlocked = true; // NSFW mode, item is not NSFW
|
||||||
else if (userMode === 4 && (!cfg.enable_nsfl || !isNsfl)) modeBlocked = true; // NSFL mode, item is not NSFL
|
else if (userMode === 4 && (!cfg.enable_nsfl || !isNsfl)) modeBlocked = true; // NSFL mode, item is not NSFL
|
||||||
else if (userMode === 2 && (isTagged || untaggedDisallowed)) modeBlocked = true; // Untagged mode, item has tags
|
else if (userMode === 2 && !isUntagged) modeBlocked = true; // Unrated mode, item has a rating (sfw/nsfw/nsfl)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modeBlocked) {
|
if (modeBlocked) {
|
||||||
@@ -1993,7 +1995,7 @@ const f0cklib = {
|
|||||||
end: (getEnableItemSlugs() && endItem[0]?.slug) ? endItem[0].slug : (endItem[0]?.id || itemid),
|
end: (getEnableItemSlugs() && endItem[0]?.slug) ? endItem[0].slug : (endItem[0]?.id || itemid),
|
||||||
start: (getEnableItemSlugs() && startItem[0]?.slug) ? startItem[0].slug : (startItem[0]?.id || itemid),
|
start: (getEnableItemSlugs() && startItem[0]?.slug) ? startItem[0].slug : (startItem[0]?.id || itemid),
|
||||||
next: (getEnableItemSlugs() && nextItem[0]?.slug) ? nextItem[0].slug : (nextItem[0]?.id || null),
|
next: (getEnableItemSlugs() && nextItem[0]?.slug) ? nextItem[0].slug : (nextItem[0]?.id || null),
|
||||||
prev: (getEnableItemSlugs() && prevItem[0]?.slug) ? prevItem[0].slug : (prevItem[0]?.id || null),
|
prev: (getEnableItemSlugs() && effectivePrev[0]?.slug) ? effectivePrev[0].slug : (effectivePrev[0]?.id || null),
|
||||||
page: (getEnableItemSlugs() && actitem.slug) ? actitem.slug : actitem.id,
|
page: (getEnableItemSlugs() && actitem.slug) ? actitem.slug : actitem.id,
|
||||||
cheat: cheat
|
cheat: cheat
|
||||||
},
|
},
|
||||||
@@ -2114,8 +2116,6 @@ const f0cklib = {
|
|||||||
select
|
select
|
||||||
items.id
|
items.id
|
||||||
from items
|
from items
|
||||||
left join tags_assign on tags_assign.item_id = items.id
|
|
||||||
left join tags on tags.id = tags_assign.tag_id
|
|
||||||
where
|
where
|
||||||
${db.unsafe(modequery)}
|
${db.unsafe(modequery)}
|
||||||
and items.active = true
|
and items.active = true
|
||||||
@@ -2126,7 +2126,6 @@ const f0cklib = {
|
|||||||
${mimeSQL}
|
${mimeSQL}
|
||||||
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
|
${(!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``}
|
||||||
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
|
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
|
||||||
group by items.id, tags.tag
|
|
||||||
order by random()
|
order by random()
|
||||||
limit 1
|
limit 1
|
||||||
`;
|
`;
|
||||||
@@ -2176,7 +2175,8 @@ const f0cklib = {
|
|||||||
// If audio is included, we avoid the strict tagId optimization to ensure audio is visible
|
// If audio is included, we avoid the strict tagId optimization to ensure audio is visible
|
||||||
const useTagIdOpt = tagId && !mimeParts.includes('audio');
|
const useTagIdOpt = tagId && !mimeParts.includes('audio');
|
||||||
const nsfpIds = cfg.nsfp || [];
|
const nsfpIds = cfg.nsfp || [];
|
||||||
const checkFilter = (!session || isAnonSession(session)) && nsfpIds.length > 0;
|
// Only apply nsfp filter for SFW/All modes — when user explicitly selected NSFW/NSFL, don't exclude those items
|
||||||
|
const checkFilter = (!session || isAnonSession(session)) && nsfpIds.length > 0 && (mode === 0 || mode === 3 || mode === undefined || mode === null);
|
||||||
|
|
||||||
// Use a single uniform query with ORDER BY random()
|
// Use a single uniform query with ORDER BY random()
|
||||||
// For 30k-100k items, this is performant enough and much more reliable than seeking.
|
// For 30k-100k items, this is performant enough and much more reliable than seeking.
|
||||||
@@ -2211,6 +2211,157 @@ const f0cklib = {
|
|||||||
itemid: item[0].id
|
itemid: item[0].id
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* getRandomPool — returns all candidate IDs/slugs for client-side random selection.
|
||||||
|
* Mirrors getRandom() filter logic exactly (mode, ratings, strict, exclusions, visibility, mime, etc.)
|
||||||
|
* Caps at 500 items; uses ORDER BY random() sampling for larger sets.
|
||||||
|
*/
|
||||||
|
getRandomPool: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, mode, ratings, fav, session, strict, exclude, user_id, is_admin, userHall: rawUserHall, userHallOwner: rawUserHallOwner } = {}) => {
|
||||||
|
if (fav && rawUser) {
|
||||||
|
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
|
||||||
|
if (isPrivate && !isAllowed) {
|
||||||
|
return { items: [], total: 0 };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
|
||||||
|
const hall = rawHall || null;
|
||||||
|
|
||||||
|
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
|
||||||
|
const isTitleSearch = _decodedTag.startsWith('title:');
|
||||||
|
const titleQuery = isTitleSearch ? _decodedTag.substring(6).trim() : null;
|
||||||
|
const tag = isTitleSearch ? null : lib.parseTag(rawTag ?? null);
|
||||||
|
|
||||||
|
const mime = (rawMime ?? "");
|
||||||
|
const userHallSlug = rawUserHall || null;
|
||||||
|
const userHallOwner = rawUserHallOwner || null;
|
||||||
|
|
||||||
|
let userHallId = null;
|
||||||
|
if (userHallSlug && userHallOwner) {
|
||||||
|
const uhRows = await db`
|
||||||
|
SELECT uh.id FROM user_halls uh
|
||||||
|
JOIN "user" u ON u.id = uh.user_id
|
||||||
|
WHERE u."user" ILIKE ${userHallOwner} AND uh.slug = ${userHallSlug}
|
||||||
|
LIMIT 1
|
||||||
|
`;
|
||||||
|
userHallId = uhRows[0]?.id || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { mimeParts, mimeSQL } = resolveMimeSQL(mime, session);
|
||||||
|
const excludedTags = session && exclude ? (exclude || []) : [];
|
||||||
|
|
||||||
|
const strictParams = ((strict || (tag && tag.includes(','))) && tag) ? tag.split(',').map(t => lib.slugify(t)).filter(t => t) : [];
|
||||||
|
const isStrict = strictParams.length > 0;
|
||||||
|
|
||||||
|
const ratingsArr = (Array.isArray(ratings) && ratings.length > 0) ? ratings : null;
|
||||||
|
const multiRatingSQL = ratingsArr ? lib.getMultiRatingMode(ratingsArr) : null;
|
||||||
|
const modequery = computeBaseMode(mode, ratings, session);
|
||||||
|
|
||||||
|
// Common filter fragments
|
||||||
|
const globalFilter = (!session || isAnonSession(session)) && getGlobalfilter(session) ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter(session))}))` : db``;
|
||||||
|
const excludeFilter = excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``;
|
||||||
|
|
||||||
|
let rows;
|
||||||
|
|
||||||
|
if (isTitleSearch && titleQuery) {
|
||||||
|
rows = await db`
|
||||||
|
SELECT items.id, items.slug FROM items
|
||||||
|
WHERE ${db.unsafe(modequery)}
|
||||||
|
AND items.active = true AND coalesce(items.visibility, 0) = 0
|
||||||
|
AND items.title ILIKE ${'%' + titleQuery + '%'} AND items.title IS NOT NULL
|
||||||
|
${mimeSQL} ${globalFilter} ${excludeFilter}
|
||||||
|
ORDER BY items.id
|
||||||
|
`;
|
||||||
|
} else if (fav && user) {
|
||||||
|
rows = await db`
|
||||||
|
SELECT DISTINCT items.id, items.slug FROM favorites
|
||||||
|
INNER JOIN items ON favorites.item_id = items.id
|
||||||
|
INNER JOIN "user" ON "user".id = favorites.user_id
|
||||||
|
WHERE ${db.unsafe(modequery)}
|
||||||
|
AND "user".user ILIKE ${user}
|
||||||
|
AND items.active = true AND coalesce(items.visibility, 0) = 0
|
||||||
|
${mimeSQL} ${globalFilter}
|
||||||
|
ORDER BY items.id
|
||||||
|
`;
|
||||||
|
} else if (user || tag) {
|
||||||
|
let tagFilter = db``;
|
||||||
|
if (tag) {
|
||||||
|
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
|
||||||
|
if (terms.length > 0) {
|
||||||
|
if (isStrict) {
|
||||||
|
tagFilter = db`and items.id in (
|
||||||
|
select ta.item_id
|
||||||
|
from tags_assign ta
|
||||||
|
join tags t on t.id = ta.tag_id
|
||||||
|
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
|
||||||
|
group by ta.item_id
|
||||||
|
having count(distinct t.normalized) = ${terms.length}
|
||||||
|
)`;
|
||||||
|
} else {
|
||||||
|
const conditions = terms.map(term => {
|
||||||
|
return db`and items.id in (select ta.item_id from tags_assign ta join tags t on t.id = ta.tag_id where t.normalized like '%' || slugify(${term}) || '%')`;
|
||||||
|
});
|
||||||
|
tagFilter = db`${conditions}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rows = await db`
|
||||||
|
SELECT items.id, items.slug FROM items
|
||||||
|
WHERE ${db.unsafe(modequery)}
|
||||||
|
AND items.active = true AND coalesce(items.visibility, 0) = 0
|
||||||
|
${tagFilter}
|
||||||
|
${user ? db`and items.username ilike ${user}` : db``}
|
||||||
|
${hall ? db`and items.id in (select item_id from halls_assign ha join halls h on h.id = ha.hall_id where h.slug = ${hall})` : db``}
|
||||||
|
${mimeSQL} ${globalFilter} ${excludeFilter}
|
||||||
|
ORDER BY items.id
|
||||||
|
`;
|
||||||
|
} else if (hall) {
|
||||||
|
rows = await db`
|
||||||
|
SELECT items.id, items.slug FROM items
|
||||||
|
JOIN halls_assign ha ON ha.item_id = items.id
|
||||||
|
JOIN halls h ON h.id = ha.hall_id
|
||||||
|
WHERE ${db.unsafe(modequery)}
|
||||||
|
AND h.slug = ${hall}
|
||||||
|
AND items.active = true AND coalesce(items.visibility, 0) = 0
|
||||||
|
${mimeSQL} ${globalFilter} ${excludeFilter}
|
||||||
|
ORDER BY items.id
|
||||||
|
`;
|
||||||
|
} else if (userHallId) {
|
||||||
|
rows = await db`
|
||||||
|
SELECT items.id, items.slug FROM items
|
||||||
|
JOIN user_halls_assign uha ON uha.item_id = items.id
|
||||||
|
WHERE ${db.unsafe(modequery)}
|
||||||
|
AND uha.hall_id = ${userHallId}
|
||||||
|
AND items.active = true AND coalesce(items.visibility, 0) = 0
|
||||||
|
${mimeSQL} ${excludeFilter}
|
||||||
|
ORDER BY items.id
|
||||||
|
`;
|
||||||
|
} else {
|
||||||
|
// Global pool — use tag optimization where possible
|
||||||
|
const globalModeQuery = modequery;
|
||||||
|
const tagId = session && !isAnonSession(session) && !multiRatingSQL && (mode === 0 || mode === 1 || mode === 4)
|
||||||
|
? (mode === 4 ? (cfg.nsfl_tag_id || 3) : (mode === 1 ? 2 : 1))
|
||||||
|
: null;
|
||||||
|
const useTagIdOpt = tagId && !mimeParts.includes('audio');
|
||||||
|
const nsfpIds = cfg.nsfp || [];
|
||||||
|
const checkFilter = (!session || isAnonSession(session)) && nsfpIds.length > 0 && (mode === 0 || mode === 3 || mode === undefined || mode === null);
|
||||||
|
|
||||||
|
rows = await db`
|
||||||
|
SELECT items.id, items.slug FROM items
|
||||||
|
${useTagIdOpt ? db`INNER JOIN tags_assign ta ON ta.item_id = items.id AND ta.tag_id = ${tagId}` : db``}
|
||||||
|
${checkFilter ? db`LEFT JOIN tags_assign filter_ta ON filter_ta.item_id = items.id AND filter_ta.tag_id IN ${db(nsfpIds)}` : db``}
|
||||||
|
WHERE items.active = true AND coalesce(items.visibility, 0) = 0
|
||||||
|
${mimeSQL}
|
||||||
|
${checkFilter ? db`AND filter_ta.tag_id IS NULL` : db``}
|
||||||
|
${excludeFilter}
|
||||||
|
${!useTagIdOpt ? db`AND ${db.unsafe(globalModeQuery)}` : db``}
|
||||||
|
ORDER BY items.id LIMIT 10000
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const items = rows.map(r => r.slug || String(r.id));
|
||||||
|
return { items, total: items.length, sampled: items.length >= 10000 };
|
||||||
|
},
|
||||||
getComments: async (itemId, sort = 'new', process = true) => {
|
getComments: async (itemId, sort = 'new', process = true) => {
|
||||||
const numericId = await resolveNumericItemId(itemId);
|
const numericId = await resolveNumericItemId(itemId);
|
||||||
if (!numericId) return [];
|
if (!numericId) return [];
|
||||||
|
|||||||
@@ -6,6 +6,234 @@ import { createI18n } from "../i18n.mjs";
|
|||||||
import { isAnonymizeSession } from "../settings.mjs";
|
import { isAnonymizeSession } from "../settings.mjs";
|
||||||
|
|
||||||
export default (router, tpl) => {
|
export default (router, tpl) => {
|
||||||
|
// ── Merged random + item load: single request instead of two ────────────
|
||||||
|
router.get(/^\/ajax\/item\/random/, async (req, res) => {
|
||||||
|
const tAjaxStart = Date.now();
|
||||||
|
let query = {};
|
||||||
|
if (typeof req.url === 'string') {
|
||||||
|
const parsedUrl = url.parse(req.url, true);
|
||||||
|
query = parsedUrl.query;
|
||||||
|
} else {
|
||||||
|
query = req.url.qs || {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const isGuest = !req.session || !req.session.user;
|
||||||
|
const reqMode = isGuest ? 0 : (query.mode !== undefined ? +query.mode : req.mode);
|
||||||
|
const ratingsRaw = req.cookies.ratings;
|
||||||
|
const ratingsArr = isGuest ? ['sfw'] : ((reqMode === 2 || reqMode === 3) ? null : (ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null));
|
||||||
|
|
||||||
|
const tag = query.tag || null;
|
||||||
|
const hall = query.hall || null;
|
||||||
|
const user = query.user || null;
|
||||||
|
const userHall = query.userHall || null;
|
||||||
|
const userHallOwner = query.userHallOwner || null;
|
||||||
|
const isFav = query.fav === 'true';
|
||||||
|
const isStrict = query.strict === '1';
|
||||||
|
const cookieMime = req.cookies?.mime !== undefined ? (decodeURIComponent(req.cookies.mime).trim() || null) : null;
|
||||||
|
const mime = (typeof query.mime !== 'undefined') ? (query.mime || null) : (cookieMime || null);
|
||||||
|
|
||||||
|
// Resolve random item ID
|
||||||
|
const randomData = await f0cklib.getRandom({
|
||||||
|
user, tag, hall, userHall, userHallOwner, mime,
|
||||||
|
fav: isFav,
|
||||||
|
mode: reqMode,
|
||||||
|
ratings: ratingsArr && ratingsArr.length > 0 ? ratingsArr : null,
|
||||||
|
strict: isStrict,
|
||||||
|
session: req.session,
|
||||||
|
exclude: req.session?.excluded_tags || [],
|
||||||
|
user_id: req.session?.id,
|
||||||
|
is_admin: req.session?.admin
|
||||||
|
});
|
||||||
|
const tRandom = Date.now();
|
||||||
|
if (!randomData || !randomData.itemid) {
|
||||||
|
return res.reply({
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ html: '', error: true, success: false, message: 'No items found' })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const itemid = String(randomData.itemid);
|
||||||
|
|
||||||
|
// Build context URL for the resolved item
|
||||||
|
let contextUrl = `/${itemid}`;
|
||||||
|
if (tag) contextUrl = `/tag/${encodeURIComponent(tag)}/${itemid}`;
|
||||||
|
if (hall) contextUrl = `/h/${encodeURIComponent(hall)}/${itemid}`;
|
||||||
|
if (userHall && userHallOwner) {
|
||||||
|
contextUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemid}`;
|
||||||
|
} else if (user) {
|
||||||
|
contextUrl = isFav
|
||||||
|
? `/user/${encodeURIComponent(user)}/favs/${itemid}`
|
||||||
|
: `/user/${encodeURIComponent(user)}/${itemid}`;
|
||||||
|
} else if (isFav) {
|
||||||
|
contextUrl = `/favs/${itemid}`;
|
||||||
|
}
|
||||||
|
if (mime) {
|
||||||
|
contextUrl = contextUrl.replace(new RegExp(`/${itemid}$`), `/${mime}/${itemid}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cfg.main.development) console.log(`[${new Date().toISOString()}] [AJAX-RANDOM] Resolved random item ${itemid} in ${Date.now() - tAjaxStart}ms`);
|
||||||
|
|
||||||
|
// Now run the full item load pipeline (same as /ajax/item/:id)
|
||||||
|
const bypassFilter = !!(query.bypass === '1');
|
||||||
|
const data = await f0cklib.getf0ck({
|
||||||
|
itemid: itemid,
|
||||||
|
mode: reqMode,
|
||||||
|
ratings: ratingsArr,
|
||||||
|
bypass_filter: bypassFilter,
|
||||||
|
session: req.session,
|
||||||
|
url: contextUrl,
|
||||||
|
user: user,
|
||||||
|
tag: tag,
|
||||||
|
hall: hall,
|
||||||
|
userHall: userHall,
|
||||||
|
userHallOwner: userHallOwner,
|
||||||
|
mime: mime,
|
||||||
|
fav: isFav,
|
||||||
|
ids: null,
|
||||||
|
random: true,
|
||||||
|
strict: isStrict || req.session?.strict_mode,
|
||||||
|
explicitStrict: isStrict,
|
||||||
|
exclude: req.session ? (req.session.excluded_tags || []) : [],
|
||||||
|
user_id: req.session?.id,
|
||||||
|
subf0ck: query.subf0ck || null
|
||||||
|
});
|
||||||
|
const tAjaxFetch = Date.now();
|
||||||
|
|
||||||
|
if (!data.success) {
|
||||||
|
const { t: tErr } = createI18n(req.session?.language || req.lang || 'en');
|
||||||
|
const modeLabels = { 0: 'SFW', 1: 'NSFW', 2: 'Untagged', 4: 'NSFL' };
|
||||||
|
const errorModeLabel = reqMode !== 3 ? (modeLabels[reqMode] || 'SFW') : null;
|
||||||
|
const errorHtml = tpl.render('error-partial', {
|
||||||
|
message: tErr('error.post_not_visible'),
|
||||||
|
tmp: null,
|
||||||
|
session: req.session ? { ...req.session } : false,
|
||||||
|
item_id: data.item?.id || itemid,
|
||||||
|
item_slug: data.item?.slug || null,
|
||||||
|
error_mode_label: errorModeLabel,
|
||||||
|
error_filter_hint: errorModeLabel ? tErr('error.filter_hint', { mode: `<strong>${errorModeLabel}</strong>` }) : null,
|
||||||
|
error_filter_hint_link: tErr('error.filter_hint_link'),
|
||||||
|
error_see_anyways: tErr('error.see_anyways')
|
||||||
|
}, req);
|
||||||
|
return res.reply({
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ html: errorHtml, pagination: '', error: true })
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// xD Score + comments — parallelize subscription + comments fetch
|
||||||
|
if (req.session || !cfg.main.hide_comments_from_public) {
|
||||||
|
if (req.session?.id) {
|
||||||
|
f0cklib.markNotificationsRead(req.session.id, itemid).catch(() => {});
|
||||||
|
}
|
||||||
|
const [sub, commentsForScore] = await Promise.all([
|
||||||
|
req.session ? f0cklib.getSubscriptionStatus(req.session.id, itemid) : false,
|
||||||
|
f0cklib.getComments(itemid, 'old', false)
|
||||||
|
]);
|
||||||
|
data.isSubscribed = sub;
|
||||||
|
const xdScore = f0cklib.computeXdScore(commentsForScore);
|
||||||
|
const xdMeta = f0cklib.xdScoreMeta(xdScore);
|
||||||
|
data.item.xd_score = xdScore;
|
||||||
|
data.item.xd_tier = xdMeta.tier;
|
||||||
|
data.item.xd_label = xdMeta.label;
|
||||||
|
data.commentsJSON = null;
|
||||||
|
data.comments = [];
|
||||||
|
} else {
|
||||||
|
data.isSubscribed = false;
|
||||||
|
data.commentsJSON = null;
|
||||||
|
data.comments = [];
|
||||||
|
data.item.xd_score = 0;
|
||||||
|
data.item.xd_tier = 0;
|
||||||
|
data.item.xd_label = '';
|
||||||
|
}
|
||||||
|
const tAjaxAux = Date.now();
|
||||||
|
|
||||||
|
// Session + template vars
|
||||||
|
data.session = req.session ? { ...req.session } : false;
|
||||||
|
data.url = { pathname: contextUrl };
|
||||||
|
data.fullscreen = req.cookies.fullscreen || 0;
|
||||||
|
data.hidePagination = true;
|
||||||
|
|
||||||
|
// Precompute hall display data
|
||||||
|
if (data.item && data.item.halls && data.item.halls.length) {
|
||||||
|
const currentHallSlug = data.tmp && data.tmp.hall
|
||||||
|
? (typeof data.tmp.hall === 'object' ? data.tmp.hall.slug : data.tmp.hall)
|
||||||
|
: null;
|
||||||
|
data.item.primaryHall = data.item.halls.find(h => h.slug === currentHallSlug) || data.item.halls[0];
|
||||||
|
data.item.otherHalls = data.item.halls.filter(h => h.slug !== data.item.primaryHall.slug);
|
||||||
|
} else if (data.item) {
|
||||||
|
data.item.primaryHall = null;
|
||||||
|
data.item.otherHalls = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Precomputed template booleans
|
||||||
|
if (data.item) {
|
||||||
|
const session = data.session;
|
||||||
|
const item = data.item;
|
||||||
|
if (isAnonymizeSession(req.session)) {
|
||||||
|
if (item.src) item.src = null;
|
||||||
|
item.username = 'anonymous';
|
||||||
|
item.author_banner_file = null;
|
||||||
|
item.author_banner_position = null;
|
||||||
|
item.author_banner_size = null;
|
||||||
|
item.author_avatar = null;
|
||||||
|
item.author_avatar_file = null;
|
||||||
|
item.author_color = null;
|
||||||
|
item.author_description = null;
|
||||||
|
item.author_display_name = null;
|
||||||
|
item.author_id = null;
|
||||||
|
if (data.uploader) {
|
||||||
|
data.uploader.name = 'anonymous';
|
||||||
|
data.uploader.id = null;
|
||||||
|
data.uploader.color = null;
|
||||||
|
}
|
||||||
|
if (Array.isArray(item.favorites)) {
|
||||||
|
item.favorites = item.favorites.map(f => {
|
||||||
|
const isSelf = session && session.id && f.user_id && Number(f.user_id) === Number(session.id);
|
||||||
|
if (isSelf) return f;
|
||||||
|
return { user_id: null, user: 'anonymous', login: 'anonymous', display_name: 'Anonymous', avatar: null, avatar_file: null, username_color: null, hide_fav_badge: f.hide_fav_badge, is_anon: true };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const isAnon = !!(session && (session.is_anon || (session.user && (session.user === 'anonymous' || session.user.startsWith('anon_')))));
|
||||||
|
data.is_mod_or_admin = !!(session && (session.admin || session.is_moderator));
|
||||||
|
data.can_manage_item = !isAnon && !!(session && (session.admin || session.is_moderator || (session.user && item.username && session.user.toLowerCase() === item.username.toLowerCase())));
|
||||||
|
data.can_extract_meta = !!(item.mime && item.mime.indexOf('flash') === -1 && !(item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime])));
|
||||||
|
data.user_has_favorited = lib.userHasFavorited(session, item.favorites);
|
||||||
|
data.halls_slugs = Array.isArray(item.halls) ? item.halls.map(h => h.slug).join(',') : '';
|
||||||
|
data.user_halls_slugs = Array.isArray(item.user_halls) ? item.user_halls.map(h => h.slug).join(',') : '';
|
||||||
|
data.item_rating_class = item.is_nsfl ? 'is-nsfl' : (item.is_nsfw ? 'is-nsfw' : (item.is_sfw ? 'is-sfw' : 'is-untagged'));
|
||||||
|
data.item_rating_label = item.is_nsfl ? 'NSFL' : (item.is_nsfw ? 'NSFW' : (item.is_sfw ? 'SFW' : '?'));
|
||||||
|
data.item_username_lower = (item.username || '').toLowerCase();
|
||||||
|
data.is_flash_item = !!(item.mime && (item.mime.indexOf('flash') !== -1 || item.mime.indexOf('shockwave') !== -1));
|
||||||
|
data.is_archive_item = !!(item.mime && item.mime.startsWith('application/') && cfg.mimes[item.mime] && !['swf', 'pdf'].includes(cfg.mimes[item.mime]));
|
||||||
|
data.current_hall_slug = (data.tmp && data.tmp.hall && typeof data.tmp.hall === 'object') ? data.tmp.hall.slug : (data.tmp && data.tmp.hall ? data.tmp.hall : '');
|
||||||
|
data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : '');
|
||||||
|
data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : '';
|
||||||
|
data.item_has_dimensions = !!(item.width && item.height);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render
|
||||||
|
const itemHtml = tpl.render('ajax-item', data, req);
|
||||||
|
const paginationHtml = tpl.render('snippets/pagination', data, req);
|
||||||
|
const tAjaxRender = Date.now();
|
||||||
|
|
||||||
|
// Detailed timing breakdown
|
||||||
|
console.log(`[AJAX-RANDOM] ${itemid} total=${tAjaxRender - tAjaxStart}ms | getRandom=${tRandom - tAjaxStart}ms | getf0ck=${tAjaxFetch - tRandom}ms | aux=${tAjaxAux - tAjaxFetch}ms | render=${tAjaxRender - tAjaxAux}ms`);
|
||||||
|
|
||||||
|
res.reply({
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({
|
||||||
|
html: itemHtml,
|
||||||
|
pagination: paginationHtml,
|
||||||
|
title: data.title,
|
||||||
|
id: itemid,
|
||||||
|
slug: data.item?.slug || null,
|
||||||
|
page: null,
|
||||||
|
is_random: true
|
||||||
|
})
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
router.get(/^\/ajax\/item\/(?<itemid>[a-zA-Z0-9_-]{11}|\d+)/, async (req, res) => {
|
router.get(/^\/ajax\/item\/(?<itemid>[a-zA-Z0-9_-]{11}|\d+)/, async (req, res) => {
|
||||||
const tAjaxStart = Date.now();
|
const tAjaxStart = Date.now();
|
||||||
let query = {};
|
let query = {};
|
||||||
|
|||||||
@@ -800,27 +800,13 @@ export default router => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Run item fetch + page lookup in parallel — saves one sequential DB round-trip
|
// Skip getItemPage for random — page number is meaningless and it's expensive
|
||||||
const [rows, itemPage] = await Promise.all([
|
const rows = await db`
|
||||||
db`
|
|
||||||
SELECT *
|
SELECT *
|
||||||
FROM "items"
|
FROM "items"
|
||||||
WHERE id = ${data.itemid} AND active = true
|
WHERE id = ${data.itemid} AND active = true
|
||||||
LIMIT 1
|
LIMIT 1
|
||||||
`,
|
`;
|
||||||
f0cklib.getItemPage({
|
|
||||||
targetItemId: data.itemid,
|
|
||||||
user, tag, hall, userHall, userHallOwner, mime,
|
|
||||||
fav: isFav,
|
|
||||||
mode,
|
|
||||||
ratings: ratingsArr && ratingsArr.length > 0 ? ratingsArr : null,
|
|
||||||
strict: isStrict,
|
|
||||||
session: req.session,
|
|
||||||
exclude: req.session?.excluded_tags || [],
|
|
||||||
user_id: req.session?.id,
|
|
||||||
is_admin: req.session?.admin
|
|
||||||
}).catch(() => 1)
|
|
||||||
]);
|
|
||||||
|
|
||||||
const item = rows[0];
|
const item = rows[0];
|
||||||
|
|
||||||
@@ -852,11 +838,49 @@ export default router => {
|
|||||||
dest: relativeDest,
|
dest: relativeDest,
|
||||||
url: directUrl,
|
url: directUrl,
|
||||||
direct_url: directUrl,
|
direct_url: directUrl,
|
||||||
page: itemPage
|
page: 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group.get('/random-pool', async (req, res) => {
|
||||||
|
const user = req.url.qs?.user || null;
|
||||||
|
const cookieMime = req.cookies?.mime !== undefined ? (decodeURIComponent(req.cookies.mime).trim() || null) : null;
|
||||||
|
const mime = (typeof req.url.qs?.mime !== 'undefined')
|
||||||
|
? (req.url.qs.mime || null)
|
||||||
|
: (cookieMime || null);
|
||||||
|
const tag = req.url.qs?.tag || null;
|
||||||
|
const hall = req.url.qs?.hall || null;
|
||||||
|
const userHall = req.url.qs?.userHall || null;
|
||||||
|
const userHallOwner = req.url.qs?.userHallOwner || null;
|
||||||
|
const isFav = req.url.qs?.fav === 'true';
|
||||||
|
const isStrict = req.url.qs?.strict === '1';
|
||||||
|
const mode = req.mode ?? 0;
|
||||||
|
const ratingsRaw = req.cookies.ratings;
|
||||||
|
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
|
||||||
|
|
||||||
|
const tStart = Date.now();
|
||||||
|
const pool = await f0cklib.getRandomPool({
|
||||||
|
user,
|
||||||
|
tag,
|
||||||
|
hall,
|
||||||
|
userHall,
|
||||||
|
userHallOwner,
|
||||||
|
mime,
|
||||||
|
fav: isFav,
|
||||||
|
mode,
|
||||||
|
ratings: ratingsArr && ratingsArr.length > 0 ? ratingsArr : null,
|
||||||
|
strict: isStrict,
|
||||||
|
session: req.session,
|
||||||
|
exclude: req.session?.excluded_tags || [],
|
||||||
|
user_id: req.session?.id,
|
||||||
|
is_admin: req.session?.admin
|
||||||
|
});
|
||||||
|
console.log(`[RANDOM-POOL] ${pool.total} items in ${Date.now() - tStart}ms (mode=${mode} tag=${tag} hall=${hall} user=${user})`);
|
||||||
|
|
||||||
|
return res.json(pool);
|
||||||
|
});
|
||||||
|
|
||||||
group.get(/\/recommendations(?:\/(?<type>videos|all))?$/, async (req, res) => {
|
group.get(/\/recommendations(?:\/(?<type>videos|all))?$/, async (req, res) => {
|
||||||
try {
|
try {
|
||||||
const limit = Math.min(+(req.url.qs?.limit || 20), 50);
|
const limit = Math.min(+(req.url.qs?.limit || 20), 50);
|
||||||
|
|||||||
@@ -67,7 +67,11 @@ export default (router, tpl) => {
|
|||||||
code: 404,
|
code: 404,
|
||||||
body: tpl.render('error', {
|
body: tpl.render('error', {
|
||||||
message: data.message,
|
message: data.message,
|
||||||
tmp: null
|
tmp: null,
|
||||||
|
session: req.session ? { ...req.session } : false,
|
||||||
|
error_filter_hint: null,
|
||||||
|
error_filter_hint_link: null,
|
||||||
|
error_see_anyways: null
|
||||||
}, req)
|
}, req)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
<span>{{ t('error.label') }}</span>
|
<span>{{ t('error.label') }}</span>
|
||||||
<code>{{ message }}</code>
|
<code>{{ message }}</code>
|
||||||
</div>
|
</div>
|
||||||
@if(error_filter_hint)
|
@if(typeof error_filter_hint !== 'undefined' && error_filter_hint)
|
||||||
<div class="_error_filter_hint">
|
<div class="_error_filter_hint">
|
||||||
<div class="_error_filter_hint_text">{{ error_filter_hint }}</div>
|
<div class="_error_filter_hint_text">{{ error_filter_hint }}</div>
|
||||||
<a href="#" data-action="open-filter-modal" class="_error_filter_hint_link">{{ error_filter_hint_link }}</a>
|
<a href="#" data-action="open-filter-modal" class="_error_filter_hint_link">{{ error_filter_hint_link }}</a>
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@
|
|||||||
<span>{{ t('error.label') }}</span>
|
<span>{{ t('error.label') }}</span>
|
||||||
<code>{{ message }}</code>
|
<code>{{ message }}</code>
|
||||||
</div>
|
</div>
|
||||||
@if(error_filter_hint)
|
@if(typeof error_filter_hint !== 'undefined' && error_filter_hint)
|
||||||
<div class="_error_filter_hint">
|
<div class="_error_filter_hint">
|
||||||
<div class="_error_filter_hint_text">{{ error_filter_hint }}</div>
|
<div class="_error_filter_hint_text">{{ error_filter_hint }}</div>
|
||||||
<a href="#" data-action="open-filter-modal" class="_error_filter_hint_link">{{ error_filter_hint_link }}</a>
|
<a href="#" data-action="open-filter-modal" class="_error_filter_hint_link">{{ error_filter_hint_link }}</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user