From 03abac6bef804968e8a83506431551f0e109db5f Mon Sep 17 00:00:00 2001 From: Kibi Kelburton Date: Sat, 12 Sep 2026 06:57:10 +0200 Subject: [PATCH] bkmdfg --- public/s/css/f0ckm.css | 34 ++++++++++++-- public/s/js/f0ckm.js | 32 ++++++++++--- public/s/js/sidebar-activity.js | 17 ++++++- scripts/regen.mjs | 78 ++++++++++++++++++++++++++++--- src/inc/locales/de.json | 3 +- src/inc/locales/en.json | 3 +- src/inc/locales/nl.json | 3 +- src/inc/locales/zange.json | 3 +- src/inc/routeinc/f0cklib.mjs | 12 +++-- src/inc/routes/apiv2/index.mjs | 4 +- src/inc/routes/apiv2/settings.mjs | 19 ++++++-- 11 files changed, 178 insertions(+), 30 deletions(-) diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css index c041362..7a607cc 100644 --- a/public/s/css/f0ckm.css +++ b/public/s/css/f0ckm.css @@ -3196,13 +3196,11 @@ body.sidebar-right-hidden #sidebar-drag-zone { .sidebar-video-meta { display: flex; align-items: center; - gap: 5px; + flex-wrap: wrap; + gap: 3px 5px; font-size: 0.7em; color: #777; margin-top: 2px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } .sidebar-video-time { @@ -3232,6 +3230,34 @@ body.sidebar-right-hidden #sidebar-drag-zone { font-size: 0.9em; } +.sidebar-dev-score { + font-size: 0.85em; + font-weight: 700; + padding: 1px 5px; + border-radius: 3px; + display: inline-flex; + align-items: center; + gap: 3px; + background: rgba(255, 170, 0, 0.16); + color: #ffaa00; + border: 1px solid rgba(255, 170, 0, 0.4); + letter-spacing: 0.02em; + font-family: monospace, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas; + flex-shrink: 0; +} + +.sidebar-dev-score i { + font-size: 0.85em; + opacity: 0.85; +} + +html[theme='light'] .sidebar-dev-score, +html[theme='paper'] .sidebar-dev-score { + background: rgba(217, 119, 6, 0.14); + color: #b45309; + border-color: rgba(217, 119, 6, 0.35); +} + /* Light / Paper themes */ html[theme='light'] .sidebar-tabs, html[theme='paper'] .sidebar-tabs { diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js index 7a3e5e0..fa9e388 100644 --- a/public/s/js/f0ckm.js +++ b/public/s/js/f0ckm.js @@ -6668,14 +6668,23 @@ window.cancelAnimFrame = (function () { if (!tagname) return; suggestions.style.display = 'none'; try { + const csrf = window.f0ckSession?.csrf_token || document.querySelector('input[name="csrf_token"]')?.value || ''; const res = await fetch('/api/v2/settings/excluded_tags', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ tagname }) + headers: { + 'Content-Type': 'application/json', + 'X-CSRF-Token': csrf + }, + body: JSON.stringify({ tagname, csrf_token: csrf }) }); const data = await res.json(); - if (data.success) { renderTags(); input.value = ''; } - else window.flashMessage(data.msg || 'Error adding tag', 3000, 'error'); + if (data.success) { + renderTags(); + input.value = ''; + if (typeof gridCacheMap !== 'undefined') gridCacheMap.clear(); + } else { + window.flashMessage(data.msg || 'Error adding tag', 3000, 'error'); + } } catch (e) { console.error(e); } finally { @@ -6699,9 +6708,20 @@ window.cancelAnimFrame = (function () { e.preventDefault(); const tag = e.target.getAttribute('data-tag'); try { - const res = await fetch(`/api/v2/settings/excluded_tags/${encodeURIComponent(tag)}`, { method: 'DELETE' }); + const csrf = window.f0ckSession?.csrf_token || document.querySelector('input[name="csrf_token"]')?.value || ''; + const res = await fetch(`/api/v2/settings/excluded_tags/${encodeURIComponent(tag)}`, { + method: 'DELETE', + headers: { + 'X-CSRF-Token': csrf + } + }); const data = await res.json(); - if (data.success) renderTags(); + if (data.success) { + renderTags(); + if (typeof gridCacheMap !== 'undefined') gridCacheMap.clear(); + } else { + window.flashMessage(data.msg || 'Error removing tag', 3000, 'error'); + } } catch (e) { console.error(e); } } }); diff --git a/public/s/js/sidebar-activity.js b/public/s/js/sidebar-activity.js index 9708737..4a0a337 100644 --- a/public/s/js/sidebar-activity.js +++ b/public/s/js/sidebar-activity.js @@ -1199,6 +1199,19 @@ ? ` For You` : ''; + const isDev = !!(window.f0ckSession && window.f0ckSession.development); + const rawScore = (typeof video.score === 'number') + ? video.score + : (typeof video.rank_score === 'number' ? video.rank_score : (video.xd_score || 0)); + const formattedScore = (typeof rawScore === 'number') + ? (Number.isInteger(rawScore) ? rawScore : +(rawScore.toFixed(1))) + : rawScore; + + const devScoreAttr = isDev ? ` data-score="${escapeHtml(String(formattedScore))}"` : ''; + const devScoreBadge = isDev + ? ` score: ${formattedScore}` + : ''; + const isTagFeed = options.isTagFeed || false; const tagContext = options.tag || null; const targetHref = (isTagFeed && tagContext) @@ -1207,7 +1220,7 @@ const activeClass = options.isActive ? ' active-tag-item' : ''; return ` - diff --git a/scripts/regen.mjs b/scripts/regen.mjs index 21f2302..67f767d 100644 --- a/scripts/regen.mjs +++ b/scripts/regen.mjs @@ -19,6 +19,10 @@ import queue from "../src/inc/queue.mjs"; import cfg from "../src/inc/config.mjs"; import fs from "fs/promises"; import path from "path"; +import { fileURLToPath } from "url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const STATE_FILE = path.join(__dirname, '.regen_state.json'); const args = process.argv.slice(2); @@ -31,6 +35,8 @@ if (args.length === 0) { console.log(' node regen.mjs --pdf - Regenerate all PDF items'); console.log(' node regen.mjs --youtube - Regenerate all YouTube thumbnails'); console.log(' node regen.mjs --blur - Regenerate ONLY the blurred thumbnails for all items'); + console.log(' --from - Resume/start from a specific item ID (inclusive)'); + console.log(' --resume - Resume from last checkpoint saved in .regen_state.json'); process.exit(0); } @@ -45,6 +51,33 @@ const THUMB_SIZE = 512; const blurOnly = args.includes('--blur'); console.log(`[regen] Thumb size: ${THUMB_SIZE}px\n`); +let fromId = null; +const fromIdx = args.indexOf('--from'); +if (fromIdx !== -1 && args[fromIdx + 1]) { + fromId = parseInt(args[fromIdx + 1], 10); + if (isNaN(fromId)) { + console.error('Invalid ID provided for --from'); + process.exit(1); + } +} + +if (!fromId && args.includes('--resume')) { + try { + const raw = await fs.readFile(STATE_FILE, 'utf8'); + const state = JSON.parse(raw); + if (state.lastId) { + fromId = state.lastId; + console.log(`[regen] Resuming from checkpoint at item ID: ${fromId}\n`); + } + } catch (e) { + console.warn(`[regen] No previous state checkpoint found to resume from.\n`); + } +} + +if (fromId) { + console.log(`[regen] Starting from item ID >= ${fromId}\n`); +} + const regen = async (item) => { const { id, dest, mime, src } = item; @@ -89,46 +122,77 @@ const regen = async (item) => { // Shared NOT IN clause for Flash exclusion const flashExclude = db`mime NOT IN ${db(FLASH_MIMES)}`; +const fromClause = fromId ? db`AND id >= ${fromId}` : db``; + +const saveState = async (id) => { + try { + await fs.writeFile(STATE_FILE, JSON.stringify({ lastId: id, timestamp: new Date().toISOString() }, null, 2)); + } catch (_) {} +}; + +let currentItemId = null; +process.on('SIGINT', async () => { + if (currentItemId) { + await saveState(currentItemId); + console.log(`\n[regen] Interrupted! Saved state at item ID ${currentItemId}.`); + console.log(`[regen] Resume anytime with: node scripts/regen.mjs --resume (or --from ${currentItemId})\n`); + } + process.exit(130); +}); try { let items; if (args.includes('--all')) { - items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND ${flashExclude} ORDER BY id`; + items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND ${flashExclude} ${fromClause} ORDER BY id`; console.log(`Regenerating ALL ${items.length} non-Flash items...\n`); } else if (args.includes('--audio')) { - items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime ILIKE 'audio/%' ORDER BY id`; + items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime ILIKE 'audio/%' ${fromClause} ORDER BY id`; console.log(`Regenerating ${items.length} audio items...\n`); } else if (args.includes('--pdf')) { - items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime = 'application/pdf' ORDER BY id`; + items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime = 'application/pdf' ${fromClause} ORDER BY id`; console.log(`Regenerating ${items.length} PDF items...\n`); } else if (args.includes('--youtube')) { - items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime = 'video/youtube' ORDER BY id`; + items = await db`SELECT id, dest, mime, src FROM items WHERE active = true AND is_deleted = false AND mime = 'video/youtube' ${fromClause} ORDER BY id`; console.log(`Regenerating ${items.length} YouTube items...\n`); } else if (blurOnly) { items = await db` SELECT id, dest, mime, src FROM items - WHERE active = true AND is_deleted = false AND ${flashExclude} + WHERE active = true AND is_deleted = false AND ${flashExclude} ${fromClause} ORDER BY id `; console.log(`Regenerating ONLY blurred thumbnails for all ${items.length} non-Flash items...\n`); } else { - const ids = args.map(Number).filter(n => !isNaN(n) && n > 0); + const positionalArgs = []; + for (let i = 0; i < args.length; i++) { + if (args[i] === '--from') { + i++; + continue; + } + if (args[i].startsWith('--')) continue; + positionalArgs.push(args[i]); + } + const ids = positionalArgs.map(Number).filter(n => !isNaN(n) && n > 0); if (ids.length === 0) { console.error('No valid item IDs provided.'); process.exit(1); } - items = await db`SELECT id, dest, mime, src FROM items WHERE id IN ${db(ids)} ORDER BY id`; + items = await db`SELECT id, dest, mime, src FROM items WHERE id IN ${db(ids)} ${fromClause} ORDER BY id`; const found = items.map(i => i.id); const missing = ids.filter(id => !found.includes(id)); if (missing.length) console.warn(`Items not found: ${missing.join(', ')}\n`); } for (const item of items) { + currentItemId = item.id; await regen(item); + await saveState(item.id); } + // Clean up state file on normal completion + await fs.unlink(STATE_FILE).catch(() => {}); + console.log(`\nDone. ${items.length} items processed.`); process.exit(0); } catch (err) { diff --git a/src/inc/locales/de.json b/src/inc/locales/de.json index ca8bb60..18df633 100644 --- a/src/inc/locales/de.json +++ b/src/inc/locales/de.json @@ -277,7 +277,8 @@ "video": "Video", "audio": "Audio", "image": "Bild", - "flash": "Flash" + "flash": "Flash", + "grid_mode": "Thumbnailgröße" }, "shortcuts": { "title": "Tastaturkürzel", diff --git a/src/inc/locales/en.json b/src/inc/locales/en.json index 3cd2657..fa70c56 100644 --- a/src/inc/locales/en.json +++ b/src/inc/locales/en.json @@ -277,7 +277,8 @@ "video": "Video", "audio": "Audio", "image": "Image", - "flash": "Flash" + "flash": "Flash", + "grid_mode": "Thumbnail size" }, "shortcuts": { "title": "Keyboard Shortcuts", diff --git a/src/inc/locales/nl.json b/src/inc/locales/nl.json index 42a4801..31d9e9f 100644 --- a/src/inc/locales/nl.json +++ b/src/inc/locales/nl.json @@ -275,7 +275,8 @@ "video": "Video", "audio": "Audio", "image": "Afbeelding", - "flash": "Flash" + "flash": "Flash", + "grid_mode": "Thumbnailgrootte" }, "shortcuts": { "title": "Sneltoetsen", diff --git a/src/inc/locales/zange.json b/src/inc/locales/zange.json index 6a4c3c2..9950393 100644 --- a/src/inc/locales/zange.json +++ b/src/inc/locales/zange.json @@ -273,7 +273,8 @@ "video": "Video", "audio": "Tondatei", "image": "Bild", - "flash": "Blitz" + "flash": "Blitz", + "grid_mode": "Vorschaubildgröße" }, "shortcuts": { "title": "Tastaturkürzel", diff --git a/src/inc/routeinc/f0cklib.mjs b/src/inc/routeinc/f0cklib.mjs index 5f89e87..da53d92 100644 --- a/src/inc/routeinc/f0cklib.mjs +++ b/src/inc/routeinc/f0cklib.mjs @@ -2203,7 +2203,9 @@ const f0cklib = { xd_label: meta.label, width: r.width, height: r.height, - has_coverart: !!r.has_coverart + has_coverart: !!r.has_coverart, + score: 0, + rank_score: 0 }; }); }, @@ -2481,7 +2483,9 @@ const f0cklib = { width: r.width, height: r.height, has_coverart: !!r.has_coverart, - personalized: true + personalized: true, + score: r.rank_score != null ? Math.round(Number(r.rank_score) * 10) / 10 : 0, + rank_score: r.rank_score != null ? Math.round(Number(r.rank_score) * 10) / 10 : 0 }; }); } catch (err) { @@ -2682,7 +2686,9 @@ const f0cklib = { xd_label: meta.label, width: r.width, height: r.height, - has_coverart: !!r.has_coverart + has_coverart: !!r.has_coverart, + score: r.xd_score || 0, + rank_score: 0 }; }); diff --git a/src/inc/routes/apiv2/index.mjs b/src/inc/routes/apiv2/index.mjs index b3d2083..f7aba9d 100644 --- a/src/inc/routes/apiv2/index.mjs +++ b/src/inc/routes/apiv2/index.mjs @@ -761,7 +761,9 @@ export default router => { rating_class: item.rating_class, xd_score: item.xd_score, xd_tier: item.xd_tier, - personalized: !!item.personalized + personalized: !!item.personalized, + score: typeof item.score === 'number' ? item.score : (typeof item.rank_score === 'number' ? item.rank_score : (item.xd_score || 0)), + rank_score: item.rank_score ?? item.score ?? 0 })) }); } catch (err) { diff --git a/src/inc/routes/apiv2/settings.mjs b/src/inc/routes/apiv2/settings.mjs index c0eb0fe..fc39b43 100644 --- a/src/inc/routes/apiv2/settings.mjs +++ b/src/inc/routes/apiv2/settings.mjs @@ -83,7 +83,7 @@ export default router => { }); group.post(/\/excluded_tags/, lib.loggedin, async (req, res) => { - const tagname = req.post.tagname; + const tagname = req.post?.tagname || req.body?.tagname; if (!tagname) return res.json({ success: false, msg: 'No tag provided' }, 400); const tag = (await db`select id, tag, normalized from tags where normalized = slugify(${tagname})`)[0]; @@ -92,10 +92,17 @@ export default router => { await db` update user_options - set excluded_tags = array_append(excluded_tags, ${tag.id}) - where user_id = ${+req.session.id} and not (${tag.id} = any(excluded_tags)) + set excluded_tags = array_append(coalesce(excluded_tags, '{}'), ${tag.id}) + where user_id = ${+req.session.id} and not (${tag.id} = any(coalesce(excluded_tags, '{}'))) `; + if (req.session) { + if (!req.session.excluded_tags) req.session.excluded_tags = []; + if (!req.session.excluded_tags.includes(tag.id)) { + req.session.excluded_tags.push(tag.id); + } + } + // Return updated list const tags = await db` select t.id, t.tag, t.normalized @@ -114,10 +121,14 @@ export default router => { await db` update user_options - set excluded_tags = array_remove(excluded_tags, ${tag.id}) + set excluded_tags = array_remove(coalesce(excluded_tags, '{}'), ${tag.id}) where user_id = ${+req.session.id} `; + if (req.session && req.session.excluded_tags) { + req.session.excluded_tags = req.session.excluded_tags.filter(id => id !== tag.id); + } + const tags = await db` select t.id, t.tag, t.normalized from unnest((select excluded_tags from user_options where user_id = ${+req.session.id})) as et(id)