Testing: allow selecting multiple ratings for better filtering

This commit is contained in:
2026-05-26 19:10:24 +02:00
parent 0f7eced14d
commit 7c2619e492
10 changed files with 384 additions and 49 deletions

View File

@@ -9254,6 +9254,73 @@ input:checked+.slider:before {
border-right: 1px solid rgba(255, 255, 255, 0.2); border-right: 1px solid rgba(255, 255, 255, 0.2);
} }
/* ---------- Multi-select rating toggles (filter modal) ---------- */
.rating-selector {
display: inline-flex;
border: 1px solid rgba(255, 255, 255, 0.2);
border-radius: 3px;
overflow: hidden;
vertical-align: middle;
grid-column: 1;
}
.rating-toggle-btn {
display: inline-block;
background: transparent;
color: rgba(255, 255, 255, 0.5);
border: none;
border-right: 1px solid rgba(255, 255, 255, 0.15);
padding: 2px 8px;
font-size: 11px;
cursor: pointer;
text-decoration: none;
transition: all 0.15s;
line-height: 18px;
font-family: inherit;
user-select: none;
}
.rating-toggle-btn:last-child {
border-right: none;
}
.rating-toggle-btn:hover {
color: #fff;
background: rgba(255, 255, 255, 0.08);
}
/* Per-rating active colours */
.rating-toggle-btn.active[data-rating="sfw"] {
background: #2e7d32;
color: #fff;
font-weight: bold;
}
.rating-toggle-btn.active[data-rating="nsfw"] {
background: #c62828;
color: #fff;
font-weight: bold;
}
.rating-toggle-btn.active[data-rating="nsfl"] {
background: #6a1b9a;
color: #fff;
font-weight: bold;
}
.rating-toggle-btn.active[data-rating="untagged"] {
background: #37474f;
color: #fff;
font-weight: bold;
}
.rating-toggle-btn.rating-toggle-all.active {
background: var(--accent);
color: var(--bg, #000);
font-weight: bold;
}
/* ---------- Random / Shuffle mode button ---------- */ /* ---------- Random / Shuffle mode button ---------- */
.shuffle-btn { .shuffle-btn {
background: none; background: none;

View File

@@ -358,6 +358,16 @@ window.cancelAnimFrame = (function () {
} }
} }
// Check multi-rating cookie
const ratingsRaw = document.cookie.split('; ').find(row => row.startsWith('ratings='));
const activeRatings = window.getRatingsCookie ? window.getRatingsCookie() : (() => {
if (!ratingsRaw) return [];
const val = ratingsRaw.split('=').slice(1).join('=');
const decoded = decodeURIComponent(val);
const parts = decoded.includes('|') ? decoded.split('|') : decoded.split(',');
return parts.filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r));
})();
let hasMimeFilter = false; let hasMimeFilter = false;
let mimeStr = ''; let mimeStr = '';
const cookieMime = document.cookie.split('; ').find(row => row.startsWith('mime=')); const cookieMime = document.cookie.split('; ').find(row => row.startsWith('mime='));
@@ -372,6 +382,28 @@ window.cancelAnimFrame = (function () {
let badgeText = ''; let badgeText = '';
let badgeClass = 'filter-badge'; let badgeClass = 'filter-badge';
if (activeRatings.length > 0) {
// If every available rating is selected, treat as ALL
const nsflEnabled = !!(window.f0ckSession?.enable_nsfl ?? true); // default true if unknown
const allRatings = nsflEnabled
? ['sfw', 'nsfw', 'nsfl', 'untagged']
: ['sfw', 'nsfw', 'untagged'];
const isAll = allRatings.every(r => activeRatings.includes(r));
if (isAll) {
badgeText = 'ALL';
badgeClass += ' filter-badge-all';
} else {
// Multi-rating mode: show abbreviated names
const abbr = { sfw: 'SFW', nsfw: 'NSFW', nsfl: 'NSFL', untagged: 'UNT' };
badgeText = activeRatings.map(r => abbr[r] || r.toUpperCase()).join('+');
// Use most prominent active rating for colour hint
if (activeRatings.includes('nsfw')) badgeClass += ' filter-badge-nsfw';
else if (activeRatings.includes('nsfl')) badgeClass += ' filter-badge-nsfl';
else if (activeRatings.includes('sfw')) badgeClass += ' filter-badge-sfw';
else badgeClass += ' filter-badge-unt';
}
} else {
switch (activeMode) { switch (activeMode) {
case 0: case 0:
badgeText = 'SFW'; badgeText = 'SFW';
@@ -397,6 +429,7 @@ window.cancelAnimFrame = (function () {
badgeText = 'SFW'; badgeText = 'SFW';
badgeClass += ' filter-badge-sfw'; badgeClass += ' filter-badge-sfw';
} }
}
const isRandom = document.cookie.includes('random_mode=1'); const isRandom = document.cookie.includes('random_mode=1');
let zomgHtml = ''; let zomgHtml = '';
@@ -445,12 +478,173 @@ window.cancelAnimFrame = (function () {
img.src = randomImg; img.src = randomImg;
}; };
// Initialize active mode from UI // Initialize active mode: prefer mode cookie, then session, then legacy mode-btn
const _modeCookieRaw = document.cookie.split('; ').find(r => r.startsWith('mode='));
if (_modeCookieRaw) {
window.activeMode = +_modeCookieRaw.split('=')[1];
} else if (window.f0ckSession && window.f0ckSession.mode !== undefined) {
window.activeMode = +window.f0ckSession.mode;
} else {
// Legacy fallback: read from old .mode-btn.active if present
const activeModeBtn = document.querySelector('.mode-btn.active'); const activeModeBtn = document.querySelector('.mode-btn.active');
if (activeModeBtn) { if (activeModeBtn && activeModeBtn.href) {
const modeMatch = activeModeBtn.href.match(/\/mode\/(\d)/); const modeMatch = activeModeBtn.href.match(/\/mode\/(\d)/);
if (modeMatch) window.activeMode = +modeMatch[1]; if (modeMatch) window.activeMode = +modeMatch[1];
} }
}
// ---- Multi-select Rating Toggles ----
// Reads/writes a `ratings` cookie (e.g. "sfw|untagged") and syncs with server via /mode/3 (ALL).
const getRatingsCookie = () => {
const raw = document.cookie.split('; ').find(r => r.startsWith('ratings='));
if (!raw) return [];
const val = raw.split('=').slice(1).join('='); // handle any = in value
// Support both | separator (new) and , (legacy/encoded)
const decoded = decodeURIComponent(val);
const parts = decoded.includes('|') ? decoded.split('|') : decoded.split(',');
return parts.filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r));
};
window.getRatingsCookie = getRatingsCookie;
const setRatingsCookie = (ratings) => {
const val = ratings.join('|'); // Use | separator — no encoding needed, no ambiguity
document.cookie = `ratings=${val}; Path=/; Max-Age=31536000; SameSite=Lax`;
};
const clearRatingsCookie = () => {
document.cookie = 'ratings=; Path=/; Max-Age=0';
};
const syncRatingButtonUI = () => {
const activeRatings = getRatingsCookie();
const selector = document.getElementById('rating-selector');
if (!selector) return;
selector.querySelectorAll('.rating-toggle-btn').forEach(btn => {
const r = btn.dataset.rating;
if (!r) return; // ALL button handled separately
btn.classList.toggle('active', activeRatings.includes(r));
});
// ALL button: active when ratings cookie is empty/absent (server mode is the authority)
const allBtn = document.getElementById('rating-btn-all');
if (allBtn) {
allBtn.classList.toggle('active', activeRatings.length === 0 && window.activeMode === 3);
}
};
// Wire up rating toggle buttons
document.addEventListener('click', (e) => {
const btn = e.target.closest('.rating-toggle-btn');
if (!btn) return;
e.preventDefault();
e.stopPropagation();
const isAllBtn = btn.classList.contains('rating-toggle-all');
const fromFilterModal = !!btn.closest('#excluded-tags-overlay');
if (isAllBtn) {
// ALL: clear ratings cookie, set mode=3 on server
clearRatingsCookie();
syncRatingButtonUI();
if (fromFilterModal) window._keepFilterModal = true;
window.activeMode = 3;
document.cookie = `mode=3; Path=/; Max-Age=31536000`;
document.dispatchEvent(new CustomEvent('f0ck:modeChanged', { detail: { mode: 3 } }));
fetch('/mode/3', { headers: { 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'include' })
.then(r => r.json())
.then(data => {
if (data.success) {
window.flashMessage('ALL MODE ACTIVATED');
gridCacheMap.clear();
const isGridView = document.querySelector('.posts, .tags-grid');
let reloadPromise = null;
if (isGridView) {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.delete('mode');
reloadPromise = loadPageAjax(currentUrl.toString(), true, { skipCache: true });
}
if (fromFilterModal) Promise.resolve(reloadPromise).finally(() => { window._keepFilterModal = false; });
}
})
.catch(() => { if (fromFilterModal) window._keepFilterModal = false; });
return;
}
const rating = btn.dataset.rating;
if (!rating) return;
// Toggle rating in cookie
const activeRatings = getRatingsCookie();
const idx = activeRatings.indexOf(rating);
if (idx === -1) {
activeRatings.push(rating);
} else {
activeRatings.splice(idx, 1);
}
if (activeRatings.length === 0) {
// Nothing selected: default back to SFW
clearRatingsCookie();
window.activeMode = 0;
document.cookie = `mode=0; Path=/; Max-Age=31536000`;
} else {
setRatingsCookie(activeRatings);
// Use mode=3 (ALL) on server when multi-select; single-select maps to native mode
const singleModeMap = { sfw: 0, nsfw: 1, nsfl: 4, untagged: 2 };
const serverMode = activeRatings.length === 1 ? (singleModeMap[activeRatings[0]] ?? 3) : 3;
window.activeMode = serverMode;
document.cookie = `mode=${serverMode}; Path=/; Max-Age=31536000`;
}
syncRatingButtonUI();
document.dispatchEvent(new CustomEvent('f0ck:modeChanged', { detail: { mode: window.activeMode } }));
if (fromFilterModal) window._keepFilterModal = true;
// Sync server mode and reload content
fetch(`/mode/${window.activeMode}`, { headers: { 'X-Requested-With': 'XMLHttpRequest' }, credentials: 'include' })
.then(r => r.json())
.then(data => {
if (data.success) {
const label = activeRatings.length > 0
? activeRatings.map(r => r.toUpperCase()).join('+') + ' ACTIVE'
: 'SFW MODE ACTIVATED';
window.flashMessage(label);
gridCacheMap.clear();
const isGridView = document.querySelector('.posts, .tags-grid');
const isItemView = document.getElementById('prev') || document.getElementById('next');
let reloadPromise = null;
if (isGridView) {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.delete('mode');
reloadPromise = loadPageAjax(currentUrl.toString(), true, { skipCache: true });
} else if (isItemView) {
updateNavForMode(window.activeMode);
}
if (fromFilterModal) Promise.resolve(reloadPromise).finally(() => { window._keepFilterModal = false; });
} else {
if (fromFilterModal) window._keepFilterModal = false;
}
})
.catch(() => { if (fromFilterModal) window._keepFilterModal = false; });
});
// Initialize rating toggle UI on page load
window.syncRatingButtonUI = syncRatingButtonUI;
// Migrate old URL-encoded ratings cookie to new pipe-separated format
(function migrateRatingsCookie() {
const raw = document.cookie.split('; ').find(r => r.startsWith('ratings='));
if (!raw) return;
const val = raw.split('=').slice(1).join('=');
if (val.includes('%')) {
// Cookie is URL-encoded — rewrite it with new format
const decoded = decodeURIComponent(val);
const cleaned = decoded.replace(/,/g, '|');
document.cookie = `ratings=${cleaned}; Path=/; Max-Age=31536000; SameSite=Lax`;
}
})();
syncRatingButtonUI();
// Cleanup strict param from URL bar on initial load if present (legacy or external link) // Cleanup strict param from URL bar on initial load if present (legacy or external link)
if (window.location.search.includes('strict=1')) { if (window.location.search.includes('strict=1')) {
@@ -3076,9 +3270,9 @@ window.cancelAnimFrame = (function () {
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
const target = e.target.nodeType === 3 ? e.target.parentElement : e.target; const target = e.target.nodeType === 3 ? e.target.parentElement : e.target;
// Check for mode selection // Check for mode selection (only applies to <a href="/mode/..."> links, not plain buttons)
const modeBtn = target.closest('.mode-btn'); const modeBtn = target.closest('.mode-btn');
if (modeBtn && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) { if (modeBtn && modeBtn.href && modeBtn.href.includes('/mode/') && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
e.preventDefault(); e.preventDefault();
// Update UI immediately for better UX // Update UI immediately for better UX
const parent = modeBtn.parentElement; const parent = modeBtn.parentElement;
@@ -4613,6 +4807,7 @@ window.cancelAnimFrame = (function () {
overlay.classList.add('visible'); overlay.classList.add('visible');
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
renderTags(); renderTags();
if (window.syncRatingButtonUI) window.syncRatingButtonUI();
if (window.innerWidth > 768) input.focus(); if (window.innerWidth > 768) input.focus();
} else { } else {
overlay.classList.remove('visible'); overlay.classList.remove('visible');

View File

@@ -81,6 +81,37 @@ export default new class {
} }
return tmp; return tmp;
}; };
/**
* Build a multi-rating SQL WHERE clause fragment from an array of rating strings.
* Supported values: 'sfw', 'nsfw', 'nsfl', 'untagged'
* Returns null if the ratings array is empty or contains all possible values (treat as ALL).
*/
getMultiRatingMode(ratings) {
if (!Array.isArray(ratings) || ratings.length === 0) return null;
const valid = ['sfw', 'nsfw', 'nsfl', 'untagged'];
const filtered = ratings.filter(r => valid.includes(r));
if (filtered.length === 0) return null;
// If all 4 are selected, treat as ALL
if (filtered.includes('sfw') && filtered.includes('nsfw') && filtered.includes('untagged') &&
(!cfg.enable_nsfl || filtered.includes('nsfl'))) return '1 = 1';
const parts = [];
if (filtered.includes('sfw')) {
parts.push('items.id in (select item_id from tags_assign where tag_id = 1)');
}
if (filtered.includes('nsfw')) {
parts.push('items.id in (select item_id from tags_assign where tag_id = 2)');
}
if (filtered.includes('nsfl') && cfg.enable_nsfl) {
parts.push(`items.id in (select item_id from tags_assign where tag_id = ${parseInt(cfg.nsfl_tag_id, 10) || 3})`);
}
if (filtered.includes('untagged')) {
parts.push('not exists (select 1 from tags_assign where item_id = items.id)');
}
if (parts.length === 0) return null;
return '(' + parts.join(' OR ') + ')';
};
createID() { createID() {
return crypto.randomBytes(16).toString("hex") + Date.now().toString(24); return crypto.randomBytes(16).toString("hex") + Date.now().toString(24);
}; };

View File

@@ -95,7 +95,7 @@ const xdScoreMeta = (score) => {
}; };
export default { export default {
getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, fav, session, limit, strict, newer, exclude, user_id, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore } = {}) => { getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore } = {}) => {
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null; const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
// --- title: prefix — search items.title instead of the tags table --- // --- title: prefix — search items.title instead of the tags table ---
@@ -149,7 +149,9 @@ export default {
const isStrict = strictParams.length > 0; const isStrict = strictParams.length > 0;
const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall: hallObj || hall, mime, page: actPage, mode: mode, view_mode: fav ? 'favs' : 'uploads', strict: strict, userHall: userHallObj || userHallSlug, userHallOwner }; const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall: hallObj || hall, mime, page: actPage, mode: mode, view_mode: fav ? 'favs' : 'uploads', strict: strict, userHall: userHallObj || userHallSlug, userHallOwner };
const baseMode = lib.getMode(mode ?? 0); // Multi-rating support: if `ratings` array provided, build an OR-based SQL fragment
const multiRatingSQL = (Array.isArray(ratings) && ratings.length > 0) ? lib.getMultiRatingMode(ratings) : null;
const baseMode = multiRatingSQL ?? lib.getMode(mode ?? 0);
const modequery = baseMode; const modequery = baseMode;
let tagFilter = db``; let tagFilter = db``;
@@ -340,7 +342,7 @@ export default {
view_mode: fav ? 'favs' : 'uploads' view_mode: fav ? 'favs' : 'uploads'
}; };
}, },
getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, session, strict, exclude, user_id, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang } = {}) => { getf0ck: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, itemid: rawItemid, mode, ratings, session, strict, exclude, user_id, fav, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, lang } = {}) => {
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null; const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
// --- title: prefix — search items.title instead of the tags table --- // --- title: prefix — search items.title instead of the tags table ---
@@ -387,7 +389,8 @@ export default {
const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall, mime, itemid, strict: strict, userHall: userHallObj || userHallSlug, userHallOwner }; const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall, mime, itemid, strict: strict, userHall: userHallObj || userHallSlug, userHallOwner };
const effMode = Number(mode ?? 0); const effMode = Number(mode ?? 0);
const modequery = lib.getMode(effMode); const multiRatingSQL = (Array.isArray(ratings) && ratings.length > 0) ? lib.getMultiRatingMode(ratings) : null;
const modequery = multiRatingSQL ?? lib.getMode(effMode);
if (itemid === null) { if (itemid === null) {
return { return {
@@ -739,7 +742,7 @@ export default {
tmp tmp
}; };
return data; return data;
}, getRandom: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, mode, fav, session, strict, exclude, userHall: rawUserHall, userHallOwner: rawUserHallOwner } = {}) => { }, getRandom: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, mode, ratings, fav, session, strict, exclude, userHall: rawUserHall, userHallOwner: rawUserHallOwner } = {}) => {
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null; const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
const hall = rawHall || null; const hall = rawHall || null;
@@ -779,7 +782,8 @@ export default {
const strictParams = ((strict || (tag && tag.includes(','))) && tag) ? tag.split(',').map(t => lib.slugify(t)).filter(t => t) : []; const strictParams = ((strict || (tag && tag.includes(','))) && tag) ? tag.split(',').map(t => lib.slugify(t)).filter(t => t) : [];
const isStrict = strictParams.length > 0; const isStrict = strictParams.length > 0;
const baseMode = lib.getMode(mode ?? 0); const multiRatingSQL = (Array.isArray(ratings) && ratings.length > 0) ? lib.getMultiRatingMode(ratings) : null;
const baseMode = multiRatingSQL ?? lib.getMode(mode ?? 0);
const modequery = baseMode; const modequery = baseMode;
let item; let item;
@@ -897,10 +901,13 @@ export default {
limit 1 limit 1
`; `;
} else { } else {
// Uniform random logic for global requests (no user/tag) // Uniform random logic for global requests (no user/tag/hall)
const baseMode = lib.getMode(mode ?? 0); // When multi-rating SQL is active, use it directly. Otherwise use the tag-join optimisation.
const modequery = baseMode; const globalModeQuery = multiRatingSQL ?? lib.getMode(mode ?? 0);
const tagId = (mode === 0 || mode === 1 || mode === 4) ? (mode === 4 ? (cfg.nsfl_tag_id || 3) : (mode === 1 ? 2 : 1)) : null; // tagId optimisation only applies for single native modes (not multi-rating)
const tagId = !multiRatingSQL && (mode === 0 || mode === 1 || mode === 4)
? (mode === 4 ? (cfg.nsfl_tag_id || 3) : (mode === 1 ? 2 : 1))
: null;
// 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 || [];
@@ -917,7 +924,7 @@ export default {
${mimeSQL} ${mimeSQL}
${checkFilter ? db`AND filter_ta.tag_id IS NULL` : db``} ${checkFilter ? db`AND filter_ta.tag_id IS NULL` : 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``}
${!useTagIdOpt ? db`AND ${db.unsafe(modequery)}` : db``} ${!useTagIdOpt ? db`AND ${db.unsafe(globalModeQuery)}` : db``}
ORDER BY random() ORDER BY random()
LIMIT 1 LIMIT 1
`; `;

View File

@@ -31,11 +31,14 @@ export default (router, tpl) => {
if (cfg.main.development) console.log(`[${new Date().toISOString()}] [AJAX] Starting item load for ${req.params.itemid}`); if (cfg.main.development) console.log(`[${new Date().toISOString()}] [AJAX] Starting item load for ${req.params.itemid}`);
const isRandom = query.random === '1' || req.cookies.random_mode === '1'; const isRandom = query.random === '1' || req.cookies.random_mode === '1';
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const itemid = req.params.itemid || req.url.pathname.match(/\/ajax\/item\/(\d+)/)?.[1]; const itemid = req.params.itemid || req.url.pathname.match(/\/ajax\/item\/(\d+)/)?.[1];
const data = await f0cklib.getf0ck({ const data = await f0cklib.getf0ck({
itemid: itemid, itemid: itemid,
mode: query.mode !== undefined ? +query.mode : req.mode, mode: query.mode !== undefined ? +query.mode : req.mode,
ratings: ratingsArr,
session: !!req.session, session: !!req.session,
url: contextUrl, url: contextUrl,
user: query.user, user: query.user,
@@ -191,6 +194,8 @@ export default (router, tpl) => {
const page = parseInt(query.page) || 1; const page = parseInt(query.page) || 1;
const isRandom = query.random === '1' || req.cookies.random_mode === '1'; const isRandom = query.random === '1' || req.cookies.random_mode === '1';
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const data = await f0cklib.getf0cks({ const data = await f0cklib.getf0cks({
page: page, page: page,
@@ -199,6 +204,7 @@ export default (router, tpl) => {
user: query.user || null, user: query.user || null,
mime: query.mime || (req.cookies.mime || null), mime: query.mime || (req.cookies.mime || null),
mode: query.mode !== undefined ? +query.mode : req.mode, mode: query.mode !== undefined ? +query.mode : req.mode,
ratings: ratingsArr,
session: !!req.session, session: !!req.session,
exclude: req.session ? (req.session.excluded_tags || []) : [], exclude: req.session ? (req.session.excluded_tags || []) : [],
user_id: req.session?.id, user_id: req.session?.id,

View File

@@ -497,6 +497,8 @@ export default router => {
const isFav = req.url.qs.fav === 'true'; const isFav = req.url.qs.fav === 'true';
const isStrict = req.url.qs.strict === '1'; const isStrict = req.url.qs.strict === '1';
const mode = req.session?.mode ?? 0; const mode = req.session?.mode ?? 0;
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const data = await f0cklib.getRandom({ const data = await f0cklib.getRandom({
user, user,
@@ -507,6 +509,7 @@ export default router => {
mime, mime,
fav: isFav, fav: isFav,
mode, mode,
ratings: ratingsArr && ratingsArr.length > 0 ? ratingsArr : null,
strict: isStrict, strict: isStrict,
session: !!req.session, session: !!req.session,
exclude: req.session?.excluded_tags || [] exclude: req.session?.excluded_tags || []

View File

@@ -146,7 +146,14 @@ export default (router, tpl) => {
mode = parseInt(req.url.qs.mode); mode = parseInt(req.url.qs.mode);
} }
/* </mode-override> */ /* </mode-override> */
const modequery = lib.getMode(mode).replace(/items\.id/g, 'i.id');
// Multi-rating cookie support (same logic as other routes)
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const multiRatingSQL = (ratingsArr && ratingsArr.length > 0) ? lib.getMultiRatingMode(ratingsArr) : null;
// Build mode SQL — replace items.id alias with i.id used in the activity query
const modequery = (multiRatingSQL ?? lib.getMode(mode)).replace(/items\.id/g, 'i.id');
const comments = await db` const comments = await db`
SELECT c.*, i.mime, i.id as item_id SELECT c.*, i.mime, i.id as item_id
@@ -843,7 +850,14 @@ export default (router, tpl) => {
mode = parseInt(req.url.qs.mode); mode = parseInt(req.url.qs.mode);
} }
/* </mode-override> */ /* </mode-override> */
const modequery = lib.getMode(mode).replace(/items\.id/g, 'i.id');
// Multi-rating cookie support (same logic as other routes)
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
const multiRatingSQL = (ratingsArr && ratingsArr.length > 0) ? lib.getMultiRatingMode(ratingsArr) : null;
// Build mode SQL — replace items.id alias with i.id used in the activity query
const modequery = (multiRatingSQL ?? lib.getMode(mode)).replace(/items\.id/g, 'i.id');
const globalfilter = cfg.nsfp.map(n => `tag_id = ${n}`).join(' or '); const globalfilter = cfg.nsfp.map(n => `tag_id = ${n}`).join(' or ');
const excludedTags = req.session ? (req.session.excluded_tags || []) : []; const excludedTags = req.session ? (req.session.excluded_tags || []) : [];

View File

@@ -62,9 +62,12 @@ export default (router, tpl) => {
}; };
try { try {
const isRandom = req.cookies.random_mode === '1'; const isRandom = req.cookies.random_mode === '1';
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
f0cks = await f0cklib.getf0cks({ f0cks = await f0cklib.getf0cks({
user: user, user: user,
mode: req.mode, mode: req.mode,
ratings: ratingsArr,
mime: mime, mime: mime,
fav: false, fav: false,
session: !!req.session, session: !!req.session,
@@ -83,9 +86,12 @@ export default (router, tpl) => {
if (!userData.is_ghost) { if (!userData.is_ghost) {
try { try {
const isRandom = req.cookies.random_mode === '1'; const isRandom = req.cookies.random_mode === '1';
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
favs = await f0cklib.getf0cks({ favs = await f0cklib.getf0cks({
user: user, user: user,
mode: req.mode, mode: req.mode,
ratings: ratingsArr,
mime: mime, mime: mime,
fav: true, fav: true,
session: !!req.session, session: !!req.session,
@@ -224,6 +230,7 @@ export default (router, tpl) => {
hall: req.params.hall, hall: req.params.hall,
fav: req.params.mode == 'favs', fav: req.params.mode == 'favs',
mode: req.mode, mode: req.mode,
ratings: (() => { const r = req.cookies.ratings; return r ? decodeURIComponent(r).split(/[|,]/).filter(x => ['sfw','nsfw','nsfl','untagged'].includes(x)) : null; })(),
session: !!req.session, session: !!req.session,
user_id: req.session?.id, user_id: req.session?.id,
exclude: req.session ? (req.session.excluded_tags || []) : [], exclude: req.session ? (req.session.excluded_tags || []) : [],

View File

@@ -39,6 +39,10 @@ export default (router, tpl) => {
} }
} }
const ratingsRaw = req.cookies.ratings;
const ratingsArr = ratingsRaw ? decodeURIComponent(ratingsRaw).split(/[|,]/).filter(r => ['sfw','nsfw','nsfl','untagged'].includes(r)) : null;
console.log('[RANDOM] ratings cookie:', ratingsRaw, '→ parsed:', ratingsArr);
const data = await f0cklib.getRandom({ const data = await f0cklib.getRandom({
user: opts.user, user: opts.user,
tag: opts.tag, tag: opts.tag,
@@ -47,6 +51,7 @@ export default (router, tpl) => {
page: opts.page, page: opts.page,
fav: opts.mode === 'favs', fav: opts.mode === 'favs',
mode: req.mode, mode: req.mode,
ratings: ratingsArr,
strict: opts.strict, strict: opts.strict,
session: !!req.session session: !!req.session
}); });

View File

@@ -9,16 +9,16 @@
<!-- modes --> <!-- modes -->
<div class="mode-filter"> <div class="mode-filter">
<button id="nav-shuffle-btn" class="shuffle-btn" title="Random Mode — shuffle all items"><span class="shuffle-icon">( - _ - )</span> {{ t('filter.random_mode') }}</button> <button id="nav-shuffle-btn" class="shuffle-btn" title="Random Mode — shuffle all items"><span class="shuffle-icon">( - _ - )</span> {{ t('filter.random_mode') }}</button>
<div class="mode-selector"> <div class="rating-selector" id="rating-selector">
<a href="/mode/0" class="mode-btn @if(mode==0) active @endif">SFW</a> <button class="rating-toggle-btn" data-rating="sfw" id="rating-btn-sfw" title="SFW">SFW</button>
<a href="/mode/1" class="mode-btn @if(mode==1) active @endif">NSFW</a> <button class="rating-toggle-btn" data-rating="nsfw" id="rating-btn-nsfw" title="NSFW">NSFW</button>
@if(enable_nsfl) @if(enable_nsfl)
<a href="/mode/4" class="mode-btn @if(mode==4) active @endif">NSFL</a> <button class="rating-toggle-btn" data-rating="nsfl" id="rating-btn-nsfl" title="NSFL">NSFL</button>
@endif @endif
@if(session.admin || session.is_moderator) @if(session && session.admin || session && session.is_moderator)
<a href="/mode/2" class="mode-btn @if(mode==2) active @endif">UNT</a> <button class="rating-toggle-btn" data-rating="untagged" id="rating-btn-untagged" title="Untagged">UNT</button>
@endif @endif
<a href="/mode/3" class="mode-btn @if(mode==3) active @endif">ALL</a> <button class="rating-toggle-btn rating-toggle-all" id="rating-btn-all" title="Show All Ratings">ALL</button>
</div> </div>
</div> </div>
<!-- mimes --> <!-- mimes -->