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);
|
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;
|
||||||
|
|||||||
@@ -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,30 +382,53 @@ window.cancelAnimFrame = (function () {
|
|||||||
let badgeText = '';
|
let badgeText = '';
|
||||||
let badgeClass = 'filter-badge';
|
let badgeClass = 'filter-badge';
|
||||||
|
|
||||||
switch (activeMode) {
|
if (activeRatings.length > 0) {
|
||||||
case 0:
|
// If every available rating is selected, treat as ALL
|
||||||
badgeText = 'SFW';
|
const nsflEnabled = !!(window.f0ckSession?.enable_nsfl ?? true); // default true if unknown
|
||||||
badgeClass += ' filter-badge-sfw';
|
const allRatings = nsflEnabled
|
||||||
break;
|
? ['sfw', 'nsfw', 'nsfl', 'untagged']
|
||||||
case 1:
|
: ['sfw', 'nsfw', 'untagged'];
|
||||||
badgeText = 'NSFW';
|
const isAll = allRatings.every(r => activeRatings.includes(r));
|
||||||
badgeClass += ' filter-badge-nsfw';
|
|
||||||
break;
|
if (isAll) {
|
||||||
case 4:
|
|
||||||
badgeText = 'NSFL';
|
|
||||||
badgeClass += ' filter-badge-nsfl';
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
badgeText = 'UNT';
|
|
||||||
badgeClass += ' filter-badge-unt';
|
|
||||||
break;
|
|
||||||
case 3:
|
|
||||||
badgeText = 'ALL';
|
badgeText = 'ALL';
|
||||||
badgeClass += ' filter-badge-all';
|
badgeClass += ' filter-badge-all';
|
||||||
break;
|
} else {
|
||||||
default:
|
// Multi-rating mode: show abbreviated names
|
||||||
badgeText = 'SFW';
|
const abbr = { sfw: 'SFW', nsfw: 'NSFW', nsfl: 'NSFL', untagged: 'UNT' };
|
||||||
badgeClass += ' filter-badge-sfw';
|
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');
|
const isRandom = document.cookie.includes('random_mode=1');
|
||||||
@@ -445,13 +478,174 @@ 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 activeModeBtn = document.querySelector('.mode-btn.active');
|
const _modeCookieRaw = document.cookie.split('; ').find(r => r.startsWith('mode='));
|
||||||
if (activeModeBtn) {
|
if (_modeCookieRaw) {
|
||||||
const modeMatch = activeModeBtn.href.match(/\/mode\/(\d)/);
|
window.activeMode = +_modeCookieRaw.split('=')[1];
|
||||||
if (modeMatch) window.activeMode = +modeMatch[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)
|
// 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')) {
|
||||||
const cleanUrl = window.location.pathname + window.location.search.replace(/[?&]strict=1/, '').replace(/[?&]$/, '') + window.location.hash;
|
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) => {
|
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');
|
||||||
|
|||||||
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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 || []
|
||||||
|
|||||||
@@ -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 || []) : [];
|
||||||
|
|
||||||
|
|||||||
@@ -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 || []) : [],
|
||||||
|
|||||||
@@ -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
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
Reference in New Issue
Block a user