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 @@
? ``
: '';
+ 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
+ ? ``
+ : '';
+
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)