#9 - adding first stage of dynamic thumbnails on the main page

This commit is contained in:
2026-05-13 14:07:55 +02:00
parent d476a002d8
commit a1be7792a2
13 changed files with 114 additions and 27 deletions

View File

@@ -68,6 +68,7 @@
"bypass_duplicate_check": true, "bypass_duplicate_check": true,
"shitpost_mode": false, "shitpost_mode": false,
"protect_files": false, "protect_files": false,
"enable_dynamic_thumbs": false,
"allowed_comment_images": [ "allowed_comment_images": [
"i.imgur.com", "i.imgur.com",
"tenor.com", "tenor.com",

View File

@@ -15,6 +15,10 @@ const exec = cmd => new Promise((resolve, reject) => {
const _args = process.argv.slice(2); const _args = process.argv.slice(2);
const _itemid = +_args[0] || 0; const _itemid = +_args[0] || 0;
// Thumb size: 512px for high-quality dynamic thumbnails
const THUMB_SIZE = 512;
const THUMB_SPEC = `${THUMB_SIZE}x${THUMB_SIZE}`;
// Ensure temp and output directories exist // Ensure temp and output directories exist
if (!fs.existsSync('./tmp')) fs.mkdirSync('./tmp', { recursive: true }); if (!fs.existsSync('./tmp')) fs.mkdirSync('./tmp', { recursive: true });
if (!fs.existsSync('./public/t')) fs.mkdirSync('./public/t', { recursive: true }); if (!fs.existsSync('./public/t')) fs.mkdirSync('./public/t', { recursive: true });
@@ -36,7 +40,7 @@ for(let item of items) {
if(mime.startsWith('video/') || mime == 'image/gif') { if(mime.startsWith('video/') || mime == 'image/gif') {
const seeks = ['20%', '40%', '60%', '80%']; const seeks = ['20%', '40%', '60%', '80%'];
for (const seek of seeks) { for (const seek of seeks) {
await exec(`ffmpegthumbnailer -i./public/b/${filename} -s1024 -t ${seek} -o./tmp/${itemid}.png`); await exec(`ffmpegthumbnailer -i./public/b/${filename} -s${THUMB_SIZE} -t ${seek} -o./tmp/${itemid}.png`);
try { try {
const { stdout } = await exec(`magick "./tmp/${itemid}.png" -colorspace Gray -format "%[fx:mean]" info:`); const { stdout } = await exec(`magick "./tmp/${itemid}.png" -colorspace Gray -format "%[fx:mean]" info:`);
if (parseFloat(stdout.trim()) > 0.05) break; if (parseFloat(stdout.trim()) > 0.05) break;
@@ -72,12 +76,11 @@ for(let item of items) {
} }
} }
await exec(`magick "./tmp/${itemid}.png" -resize "128x128^" -gravity center -crop 128x128+0+0 +repage ./public/t/${itemid}.webp`); await exec(`magick "./tmp/${itemid}.png" -resize "${THUMB_SPEC}^" -gravity center -crop ${THUMB_SPEC}+0+0 +repage ./public/t/${itemid}.webp`);
await fs.promises.unlink(`./tmp/${itemid}.png`).catch(err => {}); await fs.promises.unlink(`./tmp/${itemid}.png`).catch(err => {});
await fs.promises.unlink(`./tmp/${itemid}.jpg`).catch(err => {}); await fs.promises.unlink(`./tmp/${itemid}.jpg`).catch(err => {});
} catch(err) { } catch(err) {
console.error(`Failed to generate thumbnail for ${itemid}:`, err.message); console.error(`Failed to generate thumbnail for ${itemid}:`, err.message);
// await exec(`magick ./mugge.png ./public/t/${itemid}.webp`);
} }
console.log(`current: ${itemid} (${count} / ${total})`); console.log(`current: ${itemid} (${count} / ${total})`);
count++; count++;

View File

@@ -9290,6 +9290,13 @@ div.posts>a.thumb.has-notif p::after {
overflow: hidden; overflow: hidden;
} }
/* ===== Dynamic Thumbnail Sizes ===== */
/* Size tier 2: 2×2 grid cells */
div.posts > a.thumb[data-size="2"] {
grid-column: span 2;
grid-row: span 2;
}
.thumb>.preview-video { .thumb>.preview-video {
position: absolute; position: absolute;
top: 0; top: 0;

View File

@@ -6518,6 +6518,7 @@ class NotificationSystem {
thumb.dataset.ext = data.mime.split('/')[1].replace('youtube', 'yt').toUpperCase(); thumb.dataset.ext = data.mime.split('/')[1].replace('youtube', 'yt').toUpperCase();
thumb.dataset.mode = mode; thumb.dataset.mode = mode;
thumb.dataset.bg = `/t/${data.id}.webp`; thumb.dataset.bg = `/t/${data.id}.webp`;
thumb.dataset.size = '1'; // New items start with no contributions → tier 1
thumb.style.backgroundImage = `url('/t/${data.id}.webp')`; thumb.style.backgroundImage = `url('/t/${data.id}.webp')`;
thumb.style.opacity = '0'; thumb.style.opacity = '0';
thumb.style.transform = 'scale(0.9)'; thumb.style.transform = 'scale(0.9)';

View File

@@ -2701,7 +2701,31 @@
const el = document.createElement('div'); const el = document.createElement('div');
el.className = 'scroll-tag-sugg-item'; el.dataset.idx = i; el.className = 'scroll-tag-sugg-item'; el.dataset.idx = i;
el.innerHTML = `<i class="fa-solid fa-tag" style="font-size:.65rem;color:rgba(255,255,255,.3)"></i>${esc(r.tag)}<span class="scroll-tag-sugg-count">${r.uses ?? ''}</span>`; el.innerHTML = `<i class="fa-solid fa-tag" style="font-size:.65rem;color:rgba(255,255,255,.3)"></i>${esc(r.tag)}<span class="scroll-tag-sugg-count">${r.uses ?? ''}</span>`;
el.addEventListener('mousedown', e => { e.preventDefault(); addTagInput.value = r.tag; closeSugg(); });
el.addEventListener('mouseup', (ev) => {
const sel = window.getSelection?.()?.toString().trim();
if (!sel || sel === r.tag) return;
el.addEventListener('click', (e) => e.stopImmediatePropagation(), { once: true, capture: true });
ev.stopPropagation();
window._showSelTagPopover?.(sel, el, (confirmed) => {
window.getSelection?.()?.removeAllRanges();
addTagInput.value = confirmed;
closeSugg();
submitTag();
});
});
el.addEventListener('click', e => {
const sel = window.getSelection?.()?.toString().trim();
if (sel && sel !== r.tag) {
e.preventDefault();
e.stopPropagation();
return;
}
addTagInput.value = r.tag;
closeSugg();
submitTag();
});
addTagSuggBox.appendChild(el); addTagSuggBox.appendChild(el);
}); });
addTagSuggBox.classList.add('show'); addTagSuggBox.classList.add('show');

View File

@@ -29,12 +29,15 @@ if (args.length === 0) {
process.exit(0); process.exit(0);
} }
const THUMB_SIZE = 512;
console.log(`[regen] Thumb size: ${THUMB_SIZE}px\n`);
const regen = async (item) => { const regen = async (item) => {
const { id, dest, mime, src } = item; const { id, dest, mime, src } = item;
console.log(`[${id}] Regenerating: ${dest} (${mime})`); console.log(`[${id}] Regenerating: ${dest} (${mime})`);
try { try {
await queue.genThumbnail(dest, mime, id, src || '', false); await queue.genThumbnail(dest, mime, id, src || '', false, THUMB_SIZE);
if (mime.startsWith('audio/') && queue._lastCoverExtracted) { if (mime.startsWith('audio/') && queue._lastCoverExtracted) {
await db`UPDATE items SET has_coverart = TRUE WHERE id = ${id}`; await db`UPDATE items SET has_coverart = TRUE WHERE id = ${id}`;

View File

@@ -246,12 +246,22 @@ export default new class queue {
`)[0].id; `)[0].id;
}; };
async genThumbnail(filename, mime, itemid, link, pending = false) { /**
* Returns the optimal thumbnail size in pixels for a given contribution score tier.
* Tier 1 = default 1x1 (512px), Tier 2 = 2x2 (512px — same source, larger display)
*/
getThumbSize(tier) {
return 512; // Always generate at 512px for crisp display at any size
}
async genThumbnail(filename, mime, itemid, link, pending = false, size = 512) {
const bDir = pending ? path.join(cfg.paths.pending, 'b') : cfg.paths.b; const bDir = pending ? path.join(cfg.paths.pending, 'b') : cfg.paths.b;
const tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t; const tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
const cDir = pending ? path.join(cfg.paths.pending, 'ca') : cfg.paths.ca; const cDir = pending ? path.join(cfg.paths.pending, 'ca') : cfg.paths.ca;
const tmpFile = path.join(os.tmpdir(), itemid + '.png'); const tmpFile = path.join(os.tmpdir(), itemid + '.png');
const tmpJpg = path.join(os.tmpdir(), itemid + '.jpg'); const tmpJpg = path.join(os.tmpdir(), itemid + '.jpg');
const thumbSize = (size && size > 128) ? size : 128;
const thumbSpec = `${thumbSize}x${thumbSize}`;
// Resolve real path if it's a symlink (important for reposts) // Resolve real path if it's a symlink (important for reposts)
let sourcePath = path.join(bDir, filename); let sourcePath = path.join(bDir, filename);
@@ -266,22 +276,37 @@ export default new class queue {
if (mime === 'video/youtube') { if (mime === 'video/youtube') {
const videoId = filename.startsWith('yt:') ? filename.split(':')[1] : null; const videoId = filename.startsWith('yt:') ? filename.split(':')[1] : null;
if (videoId) { if (videoId) {
const thumbUrl = `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`; // Try quality tiers from highest to lowest. maxresdefault may not exist for all videos
// (YouTube returns a 120×90 placeholder), so we check file size before accepting.
const thumbQualities = ['maxresdefault', 'sddefault', 'hqdefault'];
const proxyArgs = (cfg.main.socks && cfg.main.socks !== 'undefined' && cfg.main.socks !== '')
? ['--proxy', cfg.main.socks.includes('://') ? cfg.main.socks : `socks5h://${cfg.main.socks}`]
: [];
let fetched = false;
for (const quality of thumbQualities) {
const thumbUrl = `https://img.youtube.com/vi/${videoId}/${quality}.jpg`;
try { try {
const curlArgs = ['-s', '-L', thumbUrl, '-o', tmpFile]; await this.spawn('curl', ['-s', '-L', thumbUrl, '-o', tmpFile, ...proxyArgs]);
if (cfg.main.socks && cfg.main.socks !== 'undefined' && cfg.main.socks !== '') { const stat = await fs.promises.stat(tmpFile).catch(() => null);
curlArgs.push('--proxy', cfg.main.socks.includes('://') ? cfg.main.socks : `socks5h://${cfg.main.socks}`); // maxresdefault placeholder is ~1.3KB; real thumbnails are much larger
if (stat && stat.size > 5000) {
fetched = true;
break;
} }
await this.spawn('curl', curlArgs);
} catch (err) { } catch (err) {
console.error(`[QUEUE] YouTube thumbnail extraction failed for ${itemid}:`, err); // try next quality
}
}
if (!fetched) {
console.error(`[QUEUE] YouTube thumbnail extraction failed for ${itemid}: all quality levels failed or returned placeholder`);
} }
} }
} }
else if (mime.startsWith('video/') || mime == 'image/gif') { else if (mime.startsWith('video/') || mime == 'image/gif') {
const ffThumbSize = Math.max(thumbSize, 512);
const seeks = ['20%', '40%', '60%', '80%']; const seeks = ['20%', '40%', '60%', '80%'];
for (const seek of seeks) { for (const seek of seeks) {
await this.spawn('ffmpegthumbnailer', ['-i', sourcePath, '-s', '1024', '-t', seek, '-o', tmpFile]); await this.spawn('ffmpegthumbnailer', ['-i', sourcePath, '-s', String(ffThumbSize), '-t', seek, '-o', tmpFile]);
try { try {
const { stdout } = await this.spawn('magick', [tmpFile, '-colorspace', 'Gray', '-format', '%[fx:mean]', 'info:']); const { stdout } = await this.spawn('magick', [tmpFile, '-colorspace', 'Gray', '-format', '%[fx:mean]', 'info:']);
if (parseFloat(stdout.trim()) > 0.05) break; if (parseFloat(stdout.trim()) > 0.05) break;
@@ -338,7 +363,7 @@ export default new class queue {
const audioFallback = path.join(cfg.paths.s, 'img', 'audio.webp'); const audioFallback = path.join(cfg.paths.s, 'img', 'audio.webp');
await fs.promises.copyFile(audioFallback, tmpFile).catch(async () => { await fs.promises.copyFile(audioFallback, tmpFile).catch(async () => {
// If copy fails, fall back to generated placeholder // If copy fails, fall back to generated placeholder
await this.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', '-gravity', 'center', '-fill', '#666', '-pointsize', '40', '-annotate', '0', '♪', tmpFile]).catch(() => {}); await this.spawn('magick', ['-size', thumbSpec, 'xc:#1a1a1a', '-gravity', 'center', '-fill', '#666', '-pointsize', '40', '-annotate', '0', '♪', tmpFile]).catch(() => {});
}); });
} }
// Store extraction result for caller // Store extraction result for caller
@@ -361,7 +386,7 @@ export default new class queue {
} }
if (!usedCustom) { if (!usedCustom) {
await this.spawn('magick', [ await this.spawn('magick', [
'-size', '256x256', 'xc:#1a1a2e', '-size', thumbSpec, 'xc:#1a1a2e',
'-gravity', 'center', '-gravity', 'center',
'-fill', '#e040fb', '-fill', '#e040fb',
'-pointsize', '48', '-pointsize', '48',
@@ -386,7 +411,7 @@ export default new class queue {
await fs.promises.copyFile(pdfFallback, tmpFile).catch(async () => { await fs.promises.copyFile(pdfFallback, tmpFile).catch(async () => {
// If the asset is missing, generate a red PDF-style placeholder matching the user's reference // If the asset is missing, generate a red PDF-style placeholder matching the user's reference
await this.spawn('magick', [ await this.spawn('magick', [
'-size', '256x256', 'xc:#d32f2f', // Professional PDF Red '-size', thumbSpec, 'xc:#d32f2f', // Professional PDF Red
'-gravity', 'center', '-gravity', 'center',
'-fill', 'white', '-fill', 'white',
'-pointsize', '60', '-pointsize', '60',
@@ -397,12 +422,13 @@ export default new class queue {
} }
} }
await this.spawn('magick', [tmpFile, '-resize', '128x128^', '-gravity', 'center', '-crop', '128x128+0+0', '+repage', path.join(tDir, itemid + '.webp')]); await this.spawn('magick', [tmpFile, '-resize', `${thumbSpec}^`, '-gravity', 'center', '-crop', `${thumbSpec}+0+0`, '+repage', path.join(tDir, itemid + '.webp')]);
await fs.promises.unlink(tmpFile).catch(_ => { }); await fs.promises.unlink(tmpFile).catch(_ => { });
await fs.promises.unlink(tmpJpg).catch(_ => { }); await fs.promises.unlink(tmpJpg).catch(_ => { });
return true; return true;
}; };
async genBlurredThumbnail(itemid, pending = false) { async genBlurredThumbnail(itemid, pending = false) {
const tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t; const tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
const src = path.join(tDir, `${itemid}.webp`); const src = path.join(tDir, `${itemid}.webp`);

View File

@@ -224,7 +224,13 @@ export default {
${user_id ? db`EXISTS (SELECT 1 FROM notifications WHERE user_id = ${user_id} AND item_id = items.id AND is_read = false) as has_notification,` : db`false as has_notification,`} ${user_id ? db`EXISTS (SELECT 1 FROM notifications WHERE user_id = ${user_id} AND item_id = items.id AND is_read = false) as has_notification,` : db`false as has_notification,`}
(case when min(ta.tag_id) = 1 then 'SFW' when min(ta.tag_id) = 2 then 'NSFW' else 'NSFL' end) as tag, (case when min(ta.tag_id) = 1 then 'SFW' when min(ta.tag_id) = 2 then 'NSFW' else 'NSFL' end) as tag,
min(ta.tag_id) as tag_id, min(ta.tag_id) as tag_id,
max(uo.display_name) as display_name max(uo.display_name) as display_name,
${cfg.websrv.enable_dynamic_thumbs ? db`
(
(SELECT count(*) FROM favorites WHERE item_id = items.id) +
(SELECT count(*) FROM comments WHERE item_id = items.id AND is_deleted = false)
) as contribution
` : db`0 as contribution`}
from items from items
left join "user" author_u on author_u.user = items.username left join "user" author_u on author_u.user = items.username
left join user_options uo on uo.user_id = author_u.id left join user_options uo on uo.user_id = author_u.id
@@ -254,6 +260,19 @@ export default {
limit ${eps} limit ${eps}
`; `;
// Compute thumb_size tier from contribution score (only meaningful when dynamic thumbs enabled)
if (cfg.websrv.enable_dynamic_thumbs) {
for (const row of rows) {
const c = Number(row.contribution) || 0;
row.thumb_size = c >= 5 ? 2 : 1;
}
} else {
for (const row of rows) {
row.thumb_size = 1;
}
}
const cheat = []; const cheat = [];
// Increase range for better context // Increase range for better context
const range = 3; const range = 3;

View File

@@ -1065,6 +1065,7 @@ process.on('uncaughtException', err => {
show_koepfe: !!cfg.websrv.show_koepfe, show_koepfe: !!cfg.websrv.show_koepfe,
allow_language_change: cfg.websrv.allow_language_change !== false, allow_language_change: cfg.websrv.allow_language_change !== false,
enable_xd_score: !!cfg.websrv.enable_xd_score, enable_xd_score: !!cfg.websrv.enable_xd_score,
enable_dynamic_thumbs: !!cfg.websrv.enable_dynamic_thumbs,
enable_swf: !!cfg.websrv.enable_swf, enable_swf: !!cfg.websrv.enable_swf,
enable_danmaku: cfg.websrv.enable_danmaku !== false, enable_danmaku: cfg.websrv.enable_danmaku !== false,
enable_global_chat: !!cfg.websrv.enable_global_chat, enable_global_chat: !!cfg.websrv.enable_global_chat,

View File

@@ -125,9 +125,10 @@ export const handleRethumbUpload = async (req, res, itemId) => {
// Generate the thumbnail // Generate the thumbnail
const tDir = item.active ? cfg.paths.t : path.join(cfg.paths.pending, 't'); const tDir = item.active ? cfg.paths.t : path.join(cfg.paths.pending, 't');
const finalPath = path.join(tDir, `${item.id}.webp`); const finalPath = path.join(tDir, `${item.id}.webp`);
const thumbSpec = '512x512';
try { try {
await execFile('magick', [tmpPath, '-coalesce', '-resize', '128x128^', '-gravity', 'center', '-crop', '128x128+0+0', '+repage', finalPath]); await execFile('magick', [tmpPath, '-coalesce', '-resize', `${thumbSpec}^`, '-gravity', 'center', '-crop', `${thumbSpec}+0+0`, '+repage', finalPath]);
// Check if item contains NSFW or NSFL tag // Check if item contains NSFW or NSFL tag
const tags = await db` const tags = await db`

View File

@@ -335,6 +335,7 @@ export const handleUpload = async (req, res, self) => {
let thumbProcessed = false; let thumbProcessed = false;
// Custom Thumbnail for Flash // Custom Thumbnail for Flash
const dynThumbSize = 512;
if (actualMime === 'application/x-shockwave-flash' || actualMime === 'application/vnd.adobe.flash.movie') { if (actualMime === 'application/x-shockwave-flash' || actualMime === 'application/vnd.adobe.flash.movie') {
if (parts.thumbnail && parts.thumbnail.data && parts.thumbnail.data.length > 0) { if (parts.thumbnail && parts.thumbnail.data && parts.thumbnail.data.length > 0) {
try { try {
@@ -344,7 +345,7 @@ export const handleUpload = async (req, res, self) => {
const tDir = isPending ? path.join(cfg.paths.pending, 't') : cfg.paths.t; const tDir = isPending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
const thumbDest = path.join(tDir, `${itemid}.webp`); const thumbDest = path.join(tDir, `${itemid}.webp`);
await queue.spawn('magick', [thumbTmp, '-resize', '128x128^', '-gravity', 'center', '-crop', '128x128+0+0', '+repage', thumbDest]); await queue.spawn('magick', [thumbTmp, '-resize', `${dynThumbSize}x${dynThumbSize}^`, '-gravity', 'center', '-crop', `${dynThumbSize}x${dynThumbSize}+0+0`, '+repage', thumbDest]);
await fs.unlink(thumbTmp).catch(() => {}); await fs.unlink(thumbTmp).catch(() => {});
thumbProcessed = true; thumbProcessed = true;
console.log(`[UPLOAD] Custom thumbnail processed for Flash item ${itemid}`); console.log(`[UPLOAD] Custom thumbnail processed for Flash item ${itemid}`);
@@ -356,7 +357,7 @@ export const handleUpload = async (req, res, self) => {
try { try {
if (!thumbProcessed) { if (!thumbProcessed) {
await queue.genThumbnail(filename, actualMime, itemid, '', isPending); await queue.genThumbnail(filename, actualMime, itemid, '', isPending, dynThumbSize);
} }
if (actualMime.startsWith('audio/') && queue._lastCoverExtracted) { if (actualMime.startsWith('audio/') && queue._lastCoverExtracted) {
@@ -369,7 +370,7 @@ export const handleUpload = async (req, res, self) => {
const tPath = !isPending const tPath = !isPending
? path.join(cfg.paths.t, itemid + '.webp') ? path.join(cfg.paths.t, itemid + '.webp')
: path.join(cfg.paths.pending, 't', itemid + '.webp'); : path.join(cfg.paths.pending, 't', itemid + '.webp');
await queue.spawn('magick', ['-size', '128x128', 'xc:#1a1a1a', tPath]).catch(() => {}); await queue.spawn('magick', ['-size', `${dynThumbSize}x${dynThumbSize}`, 'xc:#1a1a1a', tPath]).catch(() => {});
} }
} }
@@ -510,7 +511,7 @@ export const handleUpload = async (req, res, self) => {
const thumbExists = await fs.access(thumbPath).then(() => true).catch(() => false); const thumbExists = await fs.access(thumbPath).then(() => true).catch(() => false);
if (!thumbExists) { if (!thumbExists) {
await queue.genThumbnail(filename, actualMime, itemid, '', isPending); await queue.genThumbnail(filename, actualMime, itemid, '', isPending, 512);
} }
if (actualMime.startsWith('audio/') && queue._lastCoverExtracted) { if (actualMime.startsWith('audio/') && queue._lastCoverExtracted) {

View File

@@ -3,7 +3,7 @@
@include(snippets/page-title) @include(snippets/page-title)
<div class="posts" data-current-page="{{ pagination.current }}" data-has-more="{{ pagination.next ? 'true' : 'false' }}"> <div class="posts" data-current-page="{{ pagination.current }}" data-has-more="{{ pagination.next ? 'true' : 'false' }}">
@each(items as item) @each(items as item)
<a href="{{ link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp"> <a href="{{ link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
<div class="thumb-indicators"> <div class="thumb-indicators">
@if(item.is_pinned) @if(item.is_pinned)
<i class="fa-solid fa-thumbtack pin-indicator anim"></i> <i class="fa-solid fa-thumbtack pin-indicator anim"></i>

View File

@@ -1,5 +1,5 @@
@each(items as item) @each(items as item)
<a href="{{ link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp"> <a href="{{ link.main }}{{ item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
<div class="thumb-indicators"> <div class="thumb-indicators">
@if(item.is_pinned) @if(item.is_pinned)
<i class="fa-solid fa-thumbtack pin-indicator anim"></i> <i class="fa-solid fa-thumbtack pin-indicator anim"></i>