Testing: allow selecting multiple ratings for better filtering
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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 <a href="/mode/..."> 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');
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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
|
||||
`;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 || []
|
||||
|
||||
@@ -146,7 +146,14 @@ export default (router, tpl) => {
|
||||
mode = parseInt(req.url.qs.mode);
|
||||
}
|
||||
/* </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`
|
||||
SELECT c.*, i.mime, i.id as item_id
|
||||
@@ -843,7 +850,14 @@ export default (router, tpl) => {
|
||||
mode = parseInt(req.url.qs.mode);
|
||||
}
|
||||
/* </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 excludedTags = req.session ? (req.session.excluded_tags || []) : [];
|
||||
|
||||
|
||||
@@ -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 || []) : [],
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
@@ -9,16 +9,16 @@
|
||||
<!-- modes -->
|
||||
<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>
|
||||
<div class="mode-selector">
|
||||
<a href="/mode/0" class="mode-btn @if(mode==0) active @endif">SFW</a>
|
||||
<a href="/mode/1" class="mode-btn @if(mode==1) active @endif">NSFW</a>
|
||||
<div class="rating-selector" id="rating-selector">
|
||||
<button class="rating-toggle-btn" data-rating="sfw" id="rating-btn-sfw" title="SFW">SFW</button>
|
||||
<button class="rating-toggle-btn" data-rating="nsfw" id="rating-btn-nsfw" title="NSFW">NSFW</button>
|
||||
@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
|
||||
@if(session.admin || session.is_moderator)
|
||||
<a href="/mode/2" class="mode-btn @if(mode==2) active @endif">UNT</a>
|
||||
@if(session && session.admin || session && session.is_moderator)
|
||||
<button class="rating-toggle-btn" data-rating="untagged" id="rating-btn-untagged" title="Untagged">UNT</button>
|
||||
@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>
|
||||
<!-- mimes -->
|
||||
|
||||
Reference in New Issue
Block a user