From fecb0f0b0c73fdd7779a8bdcc7a65e2b52df644c Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Sat, 18 Jul 2026 23:33:25 +0200 Subject: [PATCH] tag depths --- public/s/js/f0ckm.js | 19 ++++++++++++++++++- src/inc/lib.mjs | 1 + src/inc/routeinc/f0cklib.mjs | 29 +++++++++++++++++++++++------ src/inc/routes/ajax.mjs | 3 ++- src/inc/routes/index.mjs | 3 ++- src/inc/routes/user_tags.mjs | 2 +- views/tag-cards.html | 2 +- 7 files changed, 48 insertions(+), 11 deletions(-) diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index aa5aac0..c91048c 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -2603,6 +2603,10 @@ window.cancelAnimFrame = (function () { if (isFav) ajaxUrl += `&fav=true`; if (mime) ajaxUrl += `&mime=${encodeURIComponent(mime)}`; + // Preserve tagger filter from URL query string + const taggerMatch = urlObj.searchParams.get('tagger'); + if (taggerMatch) ajaxUrl += `&tagger=${encodeURIComponent(taggerMatch)}`; + const isRandom = document.cookie.includes('random_mode=1') || url.includes('random=1') || window.location.search.includes('random=1'); if (isRandom) ajaxUrl += `&random=1`; const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true'); @@ -3327,10 +3331,15 @@ window.cancelAnimFrame = (function () { const itemid = numericSegments.pop(); // Extract context from Target URL first - let tag = null, user = null, isFavs = false, mime = null, hall = null, userHall = null, userHallOwner = null; + let tag = null, user = null, isFavs = false, mime = null, hall = null, userHall = null, userHallOwner = null, tagger = null; const tagMatch = url.match(/\/tag\/([^/?]+)/); if (tagMatch) tag = decodeURIComponent(tagMatch[1]); + // Tagger filter from query string + const taggerUrlObj = new URL(url, window.location.origin); + const taggerParam = taggerUrlObj.searchParams.get('tagger'); + if (taggerParam) tagger = decodeURIComponent(taggerParam); + // User hall: /user/:owner/hall/:slug/:id const userHallMatch = url.match(/\/user\/([^/]+)\/hall\/([^/]+)/); if (userHallMatch) { @@ -3368,6 +3377,10 @@ window.cancelAnimFrame = (function () { const wTagMatch = window.location.href.match(/\/tag\/([^/?]+)/); if (wTagMatch) tag = decodeURIComponent(wTagMatch[1]); } + if (!tagger) { + const wTaggerParam = new URL(window.location.href).searchParams.get('tagger'); + if (wTaggerParam) tagger = decodeURIComponent(wTaggerParam); + } if (!user && !userHall) { const wUserMatch = window.location.href.match(/\/user\/([^/?]+)/); if (wUserMatch && !window.location.href.match(/\/user\/[^/]+\/hall\//) ) { @@ -3403,6 +3416,7 @@ window.cancelAnimFrame = (function () { } else if (user) { params.append('user', user); } + if (tagger) params.append('tagger', tagger); if (isFavs) params.append('fav', 'true'); const isStrict = window.f0ckSession?.strict_mode || (localStorage.getItem('search_strict') === 'true'); @@ -3502,6 +3516,7 @@ window.cancelAnimFrame = (function () { else if (user) { _pushUrl = `/user/${encodeURIComponent(user)}/${itemid}`; if (isFavs) _pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemid}`; } else if (tag) _pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g,',').replace(/%20/g,' ')}/${itemid}`; else if (hall) _pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g,' ')}/${itemid}`; + if (tagger && tag) _pushUrl += `?tagger=${encodeURIComponent(tagger)}`; if (mime) _pushUrl = _pushUrl.replace(new RegExp(`/${itemid}$`), `/${mime}/${itemid}`); if (_hash) _pushUrl += _hash; @@ -3704,6 +3719,8 @@ window.cancelAnimFrame = (function () { } else if (tag) pushUrl = `/tag/${encodeURIComponent(tag).replace(/%2C/g, ',').replace(/%20/g, ' ')}/${itemid}`; else if (hall) pushUrl = `/h/${encodeURIComponent(hall).replace(/%20/g, ' ')}/${itemid}`; + // Append tagger filter so item nav stays in tagger context + if (tagger && tag) pushUrl += `?tagger=${encodeURIComponent(tagger)}`; if (mime) { // If it already has itemid at the end, insert mime before it diff --git a/src/inc/lib.mjs b/src/inc/lib.mjs index 35ef24a..45ded46 100644 --- a/src/inc/lib.mjs +++ b/src/inc/lib.mjs @@ -132,6 +132,7 @@ export default new class { // Build suffix with query params let suffix = env.strict ? '?strict=1' : ''; + if (env.tagger) suffix += (suffix ? '&' : '?') + `tagger=${encodeURIComponent(env.tagger)}`; // mainDisplay: decoded for human-readable display (e.g. div.location) // main: keeps percent-encoding for use in href attributes diff --git a/src/inc/routeinc/f0cklib.mjs b/src/inc/routeinc/f0cklib.mjs index 2eac94e..b3fd2a4 100644 --- a/src/inc/routeinc/f0cklib.mjs +++ b/src/inc/routeinc/f0cklib.mjs @@ -18,7 +18,7 @@ const flashMimes = Object.entries(cfg.mimes || {}).filter(([, ext]) => ext === ' const COUNT_CACHE_TTL_MS = 90_000; const countCache = new Map(); // key → { total, expiresAt } -function buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj }) { +function buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger }) { return JSON.stringify([ modequery, tag ?? '', @@ -30,7 +30,8 @@ function buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, ex excludedTags.slice().sort().join(','), newerThan ?? '', minXd, - userHallObj?.id ?? '' + userHallObj?.id ?? '', + tagger ?? '' ]); } @@ -136,7 +137,7 @@ const xdScoreMeta = (score) => { }; export default { - 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 } = {}) => { + 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, tagger: rawTagger } = {}) => { const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null; // --- title: prefix — search items.title instead of the tags table --- @@ -189,7 +190,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 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 tagger = rawTagger ? lib.escapeLike(rawTagger) : null; + 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, tagger }; // 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); @@ -200,6 +202,21 @@ export default { if (isTitleSearch && titleQuery) { // Title search: match items.title ILIKE '%query%' titleFilter = db`and items.title ILIKE ${'%' + titleQuery + '%'} and items.title IS NOT NULL`; + } else if (tagger && tag) { + // Tagger+tag filter: items where the specific user applied this specific tag + const terms = tag.split(',').map(t => t.trim()).filter(Boolean); + if (terms.length > 0) { + const conditions = terms.map(term => { + return db`and items.id in ( + select ta.item_id from tags_assign ta + join tags t on t.id = ta.tag_id + join "user" u on u.id = ta.user_id + where t.normalized like '%' || slugify(${term}) || '%' + and u.user ilike ${tagger} + )`; + }); + tagFilter = db`${conditions}`; + } } else if (tag) { const terms = tag.split(',').map(t => t.trim()).filter(Boolean); if (terms.length > 0) { @@ -233,7 +250,7 @@ export default { userHallFilter = db`and items.id in (select uha.item_id from user_halls_assign uha where uha.hall_id = ${userHallObj.id})`; } - const cacheKey = buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj }); + const cacheKey = buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger }); let total = getCachedCount(cacheKey); if (total === null) { @@ -372,7 +389,7 @@ export default { for (let i = Math.max(1, act_page - range); i <= Math.min(act_page + range, pages); i++) cheat.push(i); - const link = lib.genLink({ user, tag, hall: hallObj ? hallObj.slug : hall, mime, type: fav ? 'favs' : 'uploads', path: 'p/', strict: strict }); + const link = lib.genLink({ user, tag, hall: hallObj ? hallObj.slug : hall, mime, type: fav ? 'favs' : 'uploads', path: 'p/', strict: strict, tagger }); // Override link for title searches — pagination must use the /tag/title:... prefix if (isTitleSearch && titleQuery) { diff --git a/src/inc/routes/ajax.mjs b/src/inc/routes/ajax.mjs index c59a5f0..15c6e56 100644 --- a/src/inc/routes/ajax.mjs +++ b/src/inc/routes/ajax.mjs @@ -226,7 +226,8 @@ export default (router, tpl) => { strict: query.strict === '1' || query.strict === 'true' || req.session?.strict_mode, explicitStrict: query.strict === '1' || query.strict === 'true', newer: query.newer || null, - minXdScore: req.session?.min_xd_score || 0 + minXdScore: req.session?.min_xd_score || 0, + tagger: query.tagger || null }); if (!data.success) { diff --git a/src/inc/routes/index.mjs b/src/inc/routes/index.mjs index b8fa523..be95fff 100644 --- a/src/inc/routes/index.mjs +++ b/src/inc/routes/index.mjs @@ -244,7 +244,8 @@ export default (router, tpl) => { explicitStrict: !!(req.query?.strict || req.url.qs?.strict), random: req.cookies.random_mode === '1', minXdScore: req.params.itemid ? 0 : (req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0)), - lang: req.lang + lang: req.lang, + tagger: req.url.qs?.tagger || null }); console.log(`[DEBUG] Checking strict mode: query=${req.query?.strict}, session=${req.session?.strict_mode}, effective=${!!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode)}`); console.log(`[${new Date().toISOString()}] [ROUTE] Data fetch complete in ${Date.now() - tRouteStart}ms`); diff --git a/src/inc/routes/user_tags.mjs b/src/inc/routes/user_tags.mjs index ae5d1ec..65e7080 100644 --- a/src/inc/routes/user_tags.mjs +++ b/src/inc/routes/user_tags.mjs @@ -120,7 +120,7 @@ export default (router, tpl) => { return res.json({ success: true, - html: tpl.render('tag-cards', { toptags: tags, session: (req.session && req.session.user) ? { ...req.session } : false }, req), + html: tpl.render('tag-cards', { toptags: tags, user: { user: userName }, session: (req.session && req.session.user) ? { ...req.session } : false }, req), currentPage: page, pagination: paginationHtml, hasMore: tags.length === TAGS_PER_PAGE diff --git a/views/tag-cards.html b/views/tag-cards.html index 26bd316..4088c0e 100644 --- a/views/tag-cards.html +++ b/views/tag-cards.html @@ -1,5 +1,5 @@ @each(toptags as toptag) - +
{!! toptag.tag !!}