From 7c2619e49281a0a44ee4ca68f974931ac5aedc6c Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Tue, 26 May 2026 19:10:24 +0200 Subject: [PATCH] Testing: allow selecting multiple ratings for better filtering --- public/s/css/f0ckm.css | 67 +++++++ public/s/js/f0ckm.js | 253 +++++++++++++++++++++--- src/inc/lib.mjs | 31 +++ src/inc/routeinc/f0cklib.mjs | 29 +-- src/inc/routes/ajax.mjs | 6 + src/inc/routes/apiv2/index.mjs | 3 + src/inc/routes/comments.mjs | 18 +- src/inc/routes/index.mjs | 7 + src/inc/routes/random.mjs | 5 + views/snippets/excluded-tags-modal.html | 14 +- 10 files changed, 384 insertions(+), 49 deletions(-) diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index c9746dd..6a857e8 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -9254,6 +9254,73 @@ input:checked+.slider:before { 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 ---------- */ .shuffle-btn { background: none; diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index 3f1175c..729aee9 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -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 mimeStr = ''; const cookieMime = document.cookie.split('; ').find(row => row.startsWith('mime=')); @@ -372,30 +382,53 @@ window.cancelAnimFrame = (function () { let badgeText = ''; let badgeClass = 'filter-badge'; - switch (activeMode) { - case 0: - badgeText = 'SFW'; - badgeClass += ' filter-badge-sfw'; - break; - case 1: - badgeText = 'NSFW'; - badgeClass += ' filter-badge-nsfw'; - break; - case 4: - badgeText = 'NSFL'; - badgeClass += ' filter-badge-nsfl'; - break; - case 2: - badgeText = 'UNT'; - badgeClass += ' filter-badge-unt'; - break; - case 3: + 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'; - break; - default: - badgeText = 'SFW'; - badgeClass += ' filter-badge-sfw'; + } 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) { + case 0: + badgeText = 'SFW'; + badgeClass += ' filter-badge-sfw'; + break; + case 1: + badgeText = 'NSFW'; + badgeClass += ' filter-badge-nsfw'; + break; + case 4: + badgeText = 'NSFL'; + badgeClass += ' filter-badge-nsfl'; + break; + case 2: + badgeText = 'UNT'; + badgeClass += ' filter-badge-unt'; + break; + case 3: + badgeText = 'ALL'; + badgeClass += ' filter-badge-all'; + break; + default: + badgeText = 'SFW'; + badgeClass += ' filter-badge-sfw'; + } } const isRandom = document.cookie.includes('random_mode=1'); @@ -445,13 +478,174 @@ window.cancelAnimFrame = (function () { img.src = randomImg; }; - // Initialize active mode from UI - const activeModeBtn = document.querySelector('.mode-btn.active'); - if (activeModeBtn) { - const modeMatch = activeModeBtn.href.match(/\/mode\/(\d)/); - if (modeMatch) window.activeMode = +modeMatch[1]; + // 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'); + if (activeModeBtn && activeModeBtn.href) { + const modeMatch = activeModeBtn.href.match(/\/mode\/(\d)/); + 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) if (window.location.search.includes('strict=1')) { const cleanUrl = window.location.pathname + window.location.search.replace(/[?&]strict=1/, '').replace(/[?&]$/, '') + window.location.hash; @@ -3076,9 +3270,9 @@ window.cancelAnimFrame = (function () { document.addEventListener('click', (e) => { const target = e.target.nodeType === 3 ? e.target.parentElement : e.target; - // Check for mode selection + // Check for mode selection (only applies to links, not plain buttons) 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(); // Update UI immediately for better UX const parent = modeBtn.parentElement; @@ -4613,6 +4807,7 @@ window.cancelAnimFrame = (function () { overlay.classList.add('visible'); document.body.style.overflow = 'hidden'; renderTags(); + if (window.syncRatingButtonUI) window.syncRatingButtonUI(); if (window.innerWidth > 768) input.focus(); } else { overlay.classList.remove('visible'); diff --git a/src/inc/lib.mjs b/src/inc/lib.mjs index 3a93894..35ef24a 100644 --- a/src/inc/lib.mjs +++ b/src/inc/lib.mjs @@ -81,6 +81,37 @@ export default new class { } 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() { return crypto.randomBytes(16).toString("hex") + Date.now().toString(24); }; diff --git a/src/inc/routeinc/f0cklib.mjs b/src/inc/routeinc/f0cklib.mjs index 49c4712..4fdb828 100644 --- a/src/inc/routeinc/f0cklib.mjs +++ b/src/inc/routeinc/f0cklib.mjs @@ -95,7 +95,7 @@ const xdScoreMeta = (score) => { }; 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; // --- title: prefix — search items.title instead of the tags table --- @@ -149,7 +149,9 @@ export default { 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 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; let tagFilter = db``; @@ -340,7 +342,7 @@ export default { 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; // --- 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 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) { return { @@ -739,7 +742,7 @@ export default { tmp }; 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 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 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; let item; @@ -897,10 +901,13 @@ export default { limit 1 `; } else { - // Uniform random logic for global requests (no user/tag) - const baseMode = lib.getMode(mode ?? 0); - const modequery = baseMode; - const tagId = (mode === 0 || mode === 1 || mode === 4) ? (mode === 4 ? (cfg.nsfl_tag_id || 3) : (mode === 1 ? 2 : 1)) : null; + // Uniform random logic for global requests (no user/tag/hall) + // When multi-rating SQL is active, use it directly. Otherwise use the tag-join optimisation. + const globalModeQuery = multiRatingSQL ?? lib.getMode(mode ?? 0); + // 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 const useTagIdOpt = tagId && !mimeParts.includes('audio'); const nsfpIds = cfg.nsfp || []; @@ -917,7 +924,7 @@ export default { ${mimeSQL} ${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``} - ${!useTagIdOpt ? db`AND ${db.unsafe(modequery)}` : db``} + ${!useTagIdOpt ? db`AND ${db.unsafe(globalModeQuery)}` : db``} ORDER BY random() LIMIT 1 `; diff --git a/src/inc/routes/ajax.mjs b/src/inc/routes/ajax.mjs index aa50ab2..13a38ae 100644 --- a/src/inc/routes/ajax.mjs +++ b/src/inc/routes/ajax.mjs @@ -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}`); 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 data = await f0cklib.getf0ck({ itemid: itemid, mode: query.mode !== undefined ? +query.mode : req.mode, + ratings: ratingsArr, session: !!req.session, url: contextUrl, user: query.user, @@ -191,6 +194,8 @@ export default (router, tpl) => { const page = parseInt(query.page) || 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({ page: page, @@ -199,6 +204,7 @@ export default (router, tpl) => { user: query.user || null, mime: query.mime || (req.cookies.mime || null), mode: query.mode !== undefined ? +query.mode : req.mode, + ratings: ratingsArr, session: !!req.session, exclude: req.session ? (req.session.excluded_tags || []) : [], user_id: req.session?.id, diff --git a/src/inc/routes/apiv2/index.mjs b/src/inc/routes/apiv2/index.mjs index 1c0443e..1a9e4ac 100644 --- a/src/inc/routes/apiv2/index.mjs +++ b/src/inc/routes/apiv2/index.mjs @@ -497,6 +497,8 @@ export default router => { const isFav = req.url.qs.fav === 'true'; const isStrict = req.url.qs.strict === '1'; 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({ user, @@ -507,6 +509,7 @@ export default router => { mime, fav: isFav, mode, + ratings: ratingsArr && ratingsArr.length > 0 ? ratingsArr : null, strict: isStrict, session: !!req.session, exclude: req.session?.excluded_tags || [] diff --git a/src/inc/routes/comments.mjs b/src/inc/routes/comments.mjs index 04a1c59..5be1e16 100644 --- a/src/inc/routes/comments.mjs +++ b/src/inc/routes/comments.mjs @@ -146,7 +146,14 @@ export default (router, tpl) => { mode = parseInt(req.url.qs.mode); } /* */ - 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` SELECT c.*, i.mime, i.id as item_id @@ -843,7 +850,14 @@ export default (router, tpl) => { mode = parseInt(req.url.qs.mode); } /* */ - 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 excludedTags = req.session ? (req.session.excluded_tags || []) : []; diff --git a/src/inc/routes/index.mjs b/src/inc/routes/index.mjs index e195a44..d108443 100644 --- a/src/inc/routes/index.mjs +++ b/src/inc/routes/index.mjs @@ -62,9 +62,12 @@ export default (router, tpl) => { }; try { 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({ user: user, mode: req.mode, + ratings: ratingsArr, mime: mime, fav: false, session: !!req.session, @@ -83,9 +86,12 @@ export default (router, tpl) => { if (!userData.is_ghost) { try { 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({ user: user, mode: req.mode, + ratings: ratingsArr, mime: mime, fav: true, session: !!req.session, @@ -224,6 +230,7 @@ export default (router, tpl) => { hall: req.params.hall, fav: req.params.mode == 'favs', 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, user_id: req.session?.id, exclude: req.session ? (req.session.excluded_tags || []) : [], diff --git a/src/inc/routes/random.mjs b/src/inc/routes/random.mjs index 41c281f..45e0fae 100644 --- a/src/inc/routes/random.mjs +++ b/src/inc/routes/random.mjs @@ -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({ user: opts.user, tag: opts.tag, @@ -47,6 +51,7 @@ export default (router, tpl) => { page: opts.page, fav: opts.mode === 'favs', mode: req.mode, + ratings: ratingsArr, strict: opts.strict, session: !!req.session }); diff --git a/views/snippets/excluded-tags-modal.html b/views/snippets/excluded-tags-modal.html index 818ef8b..283ca1c 100644 --- a/views/snippets/excluded-tags-modal.html +++ b/views/snippets/excluded-tags-modal.html @@ -9,16 +9,16 @@
-
- SFW - NSFW +
+ + @if(enable_nsfl) - NSFL + @endif - @if(session.admin || session.is_moderator) - UNT + @if(session && session.admin || session && session.is_moderator) + @endif - ALL +