This commit is contained in:
2026-09-11 23:07:03 +02:00
parent 802a59d090
commit 4b615428de
14 changed files with 243 additions and 174 deletions
+17 -2
View File
@@ -2223,14 +2223,29 @@ class CommentSystem {
} }
const bannerEnabled = window.f0ckCommentBannerEnabled !== false && window.f0ckSession?.comment_banner_enabled !== false; const bannerEnabled = window.f0ckCommentBannerEnabled !== false && window.f0ckSession?.comment_banner_enabled !== false;
const bannerStyle = (bannerEnabled && comment.banner_file && comment.banner_file !== 'null') let bannerStyle = (bannerEnabled && comment.banner_file && comment.banner_file !== 'null')
? `style="--author-banner: url('/a/${comment.banner_file}'); --author-banner-position: ${comment.banner_position === 'center' ? 'center top' : (comment.banner_position || 'center top')}; --author-banner-size: ${(comment.banner_size && comment.banner_size !== 'cover') ? comment.banner_size : '100% auto'}; --author-banner-repeat: no-repeat;"` ? `style="--author-banner: url('/a/${comment.banner_file}'); --author-banner-position: ${comment.banner_position === 'center' ? 'center top' : (comment.banner_position || 'center top')}; --author-banner-size: ${(comment.banner_size && comment.banner_size !== 'cover') ? comment.banner_size : '100% auto'}; --author-banner-repeat: no-repeat;"`
: ''; : '';
const authorUserId = comment.user_id ?? (comment.username && window.f0ckSession && comment.username.toLowerCase() === (window.f0ckSession.user || '').toLowerCase() ? (window.f0ckSession.id || window.f0ckSession.user_id) : null); const authorUserId = comment.user_id ?? (comment.username && window.f0ckSession && comment.username.toLowerCase() === (window.f0ckSession.user || '').toLowerCase() ? (window.f0ckSession.id || window.f0ckSession.user_id) : null);
const authorUsernameColor = comment.username_color || (comment.username && window.f0ckSession && comment.username.toLowerCase() === (window.f0ckSession.user || '').toLowerCase() ? window.f0ckSession.username_color : null); const authorUsernameColor = comment.username_color || (comment.username && window.f0ckSession && comment.username.toLowerCase() === (window.f0ckSession.user || '').toLowerCase() ? window.f0ckSession.username_color : null);
return `<div class="${commentClass} ${isDeleted ? 'deleted' : ''} ${isPinned ? 'pinned' : ''}" id="c${comment.id}" ${bannerStyle}><div class="comment-avatar">${comment.username ? `<a href="/user/${comment.username}">` : ''}<img src="${comment.avatar_file ? `/a/${comment.avatar_file}` : (comment.avatar ? `/t/${comment.avatar}.webp` : '/a/default.png')}">${comment.username ? `</a>` : ''}</div><div class="comment-body"><div class="comment-header"><div class="comment-header-left">${pinnedBadge}${comment.username ? `<a href="/user/${comment.username}" class="comment-author" tooltip="ID: ${authorUserId ?? ''}" ${authorUsernameColor ? `style="color: ${authorUsernameColor}"` : ''}>${this.escapeHtml(comment.display_name || comment.username)}</a>` : '<span class="comment-author">System</span>'}${contextMarker}${backlinkHtml}</div><a href="#c${comment.id}" class="comment-time timeago" tooltip="${fullDate}" data-iso="${isoDate}" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}">${timeAgo}</a></div><div class="comment-content" data-raw="${this.escapeHtml(comment.content)}">${content}</div>${this.renderCommentAttachments(comment.files, comment.content)}${this.renderCommentPoll(comment.poll, comment.id, comment.username)}<div class="comment-footer"><div class="comment-footer-right"><div class="comment-actions">${!isDeleted && currentUserId ? `<button class="reply-btn" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}" title="Reply"><i class="fa-solid fa-reply"></i></button><button class="quote-btn" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}" title="Quote with Text"><i class="fa-solid fa-quote-left"></i></button><button class="report-comment-btn" data-id="${comment.id}" title="Report Comment" style="background:none;border:none;color:inherit;cursor:pointer;opacity:0.75;padding:0;"><i class="fa-solid fa-triangle-exclamation"></i></button>` : ''}${adminButtons}${userDeleteButton}</div></div></div></div><a href="#c${comment.id}" class="comment-permalink" title="Permalink" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}">#${comment.id}</a></div>${repliesHtml}`; const isAnonGuest = window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in;
if (isAnonGuest) bannerStyle = '';
const avatarHtml = isAnonGuest
? `<div class="comment-avatar"><img src="/a/default.png"></div>`
: `<div class="comment-avatar">${comment.username ? `<a href="/user/${comment.username}">` : ''}<img src="${comment.avatar_file ? `/a/${comment.avatar_file}` : (comment.avatar ? `/t/${comment.avatar}.webp` : '/a/default.png')}">${comment.username ? `</a>` : ''}</div>`;
const authorHtml = isAnonGuest
? `<span class="comment-author">anonymous</span>`
: (comment.username
? `<a href="/user/${comment.username}" class="comment-author" tooltip="ID: ${authorUserId ?? ''}" ${authorUsernameColor ? `style="color: ${authorUsernameColor}"` : ''}>${this.escapeHtml(comment.display_name || comment.username)}</a>`
: '<span class="comment-author">System</span>');
const anonDataAttrs = isAnonGuest ? '' : `data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}"`;
return `<div class="${commentClass} ${isDeleted ? 'deleted' : ''} ${isPinned ? 'pinned' : ''}" id="c${comment.id}" ${bannerStyle}>${avatarHtml}<div class="comment-body"><div class="comment-header"><div class="comment-header-left">${pinnedBadge}${authorHtml}${contextMarker}${backlinkHtml}</div><a href="#c${comment.id}" class="comment-time timeago" tooltip="${fullDate}" data-iso="${isoDate}" data-id="${comment.id}" ${anonDataAttrs}>${timeAgo}</a></div><div class="comment-content" data-raw="${this.escapeHtml(comment.content)}">${content}</div>${this.renderCommentAttachments(comment.files, comment.content)}${this.renderCommentPoll(comment.poll, comment.id, isAnonGuest ? null : comment.username)}<div class="comment-footer"><div class="comment-footer-right"><div class="comment-actions">${!isDeleted && currentUserId ? `<button class="reply-btn" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}" title="Reply"><i class="fa-solid fa-reply"></i></button><button class="quote-btn" data-id="${comment.id}" data-username="${comment.username}" data-display="${this.escapeHtml(comment.display_name || '')}" title="Quote with Text"><i class="fa-solid fa-quote-left"></i></button><button class="report-comment-btn" data-id="${comment.id}" title="Report Comment" style="background:none;border:none;color:inherit;cursor:pointer;opacity:0.75;padding:0;"><i class="fa-solid fa-triangle-exclamation"></i></button>` : ''}${adminButtons}${userDeleteButton}</div></div></div></div><a href="#c${comment.id}" class="comment-permalink" title="Permalink" data-id="${comment.id}" ${anonDataAttrs}>#${comment.id}</a></div>${repliesHtml}`;
} }
timeAgo(date) { timeAgo(date) {
+103 -138
View File
@@ -6526,7 +6526,7 @@ window.cancelAnimFrame = (function () {
} }
}; };
const initExcludedTagsModal = () => { const initExcludedTagsModal = () => {
if (window.f0ckSession && !window.f0ckSession.logged_in) return; const isLoggedIn = !!(window.f0ckSession && window.f0ckSession.logged_in);
const overlay = document.getElementById('excluded-tags-overlay'); const overlay = document.getElementById('excluded-tags-overlay');
if (!overlay) return; if (!overlay) return;
@@ -6544,9 +6544,9 @@ window.cancelAnimFrame = (function () {
overlay.offsetHeight; overlay.offsetHeight;
overlay.classList.add('visible'); overlay.classList.add('visible');
document.body.style.overflow = 'hidden'; document.body.style.overflow = 'hidden';
renderTags(); if (isLoggedIn) renderTags();
if (window.syncRatingButtonUI) window.syncRatingButtonUI(); if (window.syncRatingButtonUI) window.syncRatingButtonUI();
if (window.innerWidth > 768) input.focus(); if (isLoggedIn && window.innerWidth > 768) input.focus();
} else { } else {
overlay.classList.remove('visible'); overlay.classList.remove('visible');
document.body.style.overflow = ''; document.body.style.overflow = '';
@@ -6611,149 +6611,105 @@ window.cancelAnimFrame = (function () {
if (e.target === overlay) toggleModal(false); if (e.target === overlay) toggleModal(false);
}); });
const addTag = async () => { if (isLoggedIn) {
const tagname = input.value.trim(); const addTag = async () => {
if (!tagname) return; const tagname = input.value.trim();
suggestions.style.display = 'none'; if (!tagname) return;
try {
const res = await fetch('/api/v2/settings/excluded_tags', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({
tagname
})
});
const data = await res.json();
if (data.success) {
renderTags();
input.value = '';
} else {
window.flashMessage(data.msg || 'Error adding tag', 3000, 'error');
}
} catch (e) {
console.error(e);
} finally {
if (window.innerWidth > 768) input.focus();
}
};
input.addEventListener('keydown', e => {
if (e.key === 'Enter') {
e.preventDefault();
// If a suggestion is highlighted, use it
if (highlightIdx >= 0) {
const items = suggestions.querySelectorAll('.tag-suggestion-item');
if (items[highlightIdx]) {
input.value = items[highlightIdx].querySelector('.tag-suggestion-name').textContent;
}
}
suggestions.style.display = 'none'; suggestions.style.display = 'none';
addTag();
}
});
list.addEventListener('click', async e => {
if (e.target.classList.contains('remove-excluded-tag')) {
e.preventDefault();
const tag = e.target.getAttribute('data-tag');
try { try {
const res = await fetch(`/api/v2/settings/excluded_tags/${encodeURIComponent(tag)}`, { const res = await fetch('/api/v2/settings/excluded_tags', {
method: 'DELETE' method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ tagname })
}); });
const data = await res.json(); const data = await res.json();
if (data.success) renderTags(); if (data.success) { renderTags(); input.value = ''; }
else window.flashMessage(data.msg || 'Error adding tag', 3000, 'error');
} catch (e) { } catch (e) {
console.error(e); console.error(e);
} finally {
if (window.innerWidth > 768) input.focus();
} }
} };
}); input.addEventListener('keydown', e => {
if (e.key === 'Enter') {
// Autocomplete with dropdown suggestions
let debounceTimer = null;
let highlightIdx = -1;
const renderSuggestions = (items) => {
suggestions.innerHTML = '';
highlightIdx = -1;
if (!items.length) {
suggestions.style.display = 'none';
return;
}
items.forEach(s => {
const div = document.createElement('div');
div.className = 'tag-suggestion-item';
const name = document.createElement('span');
name.className = 'tag-suggestion-name';
name.textContent = s.tag;
const meta = document.createElement('span');
meta.className = 'tag-suggestion-meta';
meta.textContent = `${s.tagged}× · ${s.score.toFixed(2)}`;
div.appendChild(name);
div.appendChild(meta);
div.addEventListener('mousedown', (e) => {
e.preventDefault(); e.preventDefault();
input.value = s.tag; if (highlightIdx >= 0) {
const items = suggestions.querySelectorAll('.tag-suggestion-item');
if (items[highlightIdx]) input.value = items[highlightIdx].querySelector('.tag-suggestion-name').textContent;
}
suggestions.style.display = 'none'; suggestions.style.display = 'none';
addTag(); addTag();
});
suggestions.appendChild(div);
});
suggestions.style.display = 'block';
};
input.addEventListener('input', () => {
const q = input.value.trim();
highlightIdx = -1;
if (q.length < 1) {
suggestions.style.display = 'none';
return;
}
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
try {
const res = await fetch(`/api/v2/tags/suggest?q=${encodeURIComponent(q)}`);
const json = await res.json();
if (json.success && json.suggestions) {
renderSuggestions(json.suggestions.slice(0, 8));
} else {
suggestions.style.display = 'none';
}
} catch (e) {
suggestions.style.display = 'none';
} }
}, 300); });
});
input.addEventListener('keydown', e => { list.addEventListener('click', async e => {
const items = suggestions.querySelectorAll('.tag-suggestion-item'); if (e.target.classList.contains('remove-excluded-tag')) {
if (!items.length || suggestions.style.display === 'none') return; e.preventDefault();
const tag = e.target.getAttribute('data-tag');
try {
const res = await fetch(`/api/v2/settings/excluded_tags/${encodeURIComponent(tag)}`, { method: 'DELETE' });
const data = await res.json();
if (data.success) renderTags();
} catch (e) { console.error(e); }
}
});
if (e.key === 'ArrowDown') { // Autocomplete with dropdown suggestions
e.preventDefault(); let debounceTimer = null;
if (highlightIdx >= 0 && highlightIdx < items.length) items[highlightIdx].classList.remove('active'); let highlightIdx = -1;
highlightIdx = highlightIdx < items.length - 1 ? highlightIdx + 1 : 0;
items[highlightIdx].classList.add('active'); const renderSuggestions = (items) => {
items[highlightIdx].scrollIntoView({ block: 'nearest' }); suggestions.innerHTML = '';
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (highlightIdx >= 0 && highlightIdx < items.length) items[highlightIdx].classList.remove('active');
highlightIdx = highlightIdx > 0 ? highlightIdx - 1 : items.length - 1;
items[highlightIdx].classList.add('active');
items[highlightIdx].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'Escape') {
suggestions.style.display = 'none';
highlightIdx = -1; highlightIdx = -1;
} if (!items.length) { suggestions.style.display = 'none'; return; }
}); items.forEach(s => {
const div = document.createElement('div');
div.className = 'tag-suggestion-item';
const name = document.createElement('span'); name.className = 'tag-suggestion-name'; name.textContent = s.tag;
const meta = document.createElement('span'); meta.className = 'tag-suggestion-meta'; meta.textContent = `${s.tagged}× · ${s.score.toFixed(2)}`;
div.appendChild(name); div.appendChild(meta);
div.addEventListener('mousedown', (e) => { e.preventDefault(); input.value = s.tag; suggestions.style.display = 'none'; addTag(); });
suggestions.appendChild(div);
});
suggestions.style.display = 'block';
};
input.addEventListener('blur', () => { input.addEventListener('input', () => {
setTimeout(() => { suggestions.style.display = 'none'; }, 150); const q = input.value.trim();
}); highlightIdx = -1;
if (q.length < 1) { suggestions.style.display = 'none'; return; }
if (debounceTimer) clearTimeout(debounceTimer);
debounceTimer = setTimeout(async () => {
try {
const res = await fetch(`/api/v2/tags/suggest?q=${encodeURIComponent(q)}`);
const json = await res.json();
if (json.success && json.suggestions) renderSuggestions(json.suggestions.slice(0, 8));
else suggestions.style.display = 'none';
} catch (e) { suggestions.style.display = 'none'; }
}, 300);
});
input.addEventListener('keydown', e => {
const items = suggestions.querySelectorAll('.tag-suggestion-item');
if (!items.length || suggestions.style.display === 'none') return;
if (e.key === 'ArrowDown') {
e.preventDefault();
if (highlightIdx >= 0 && highlightIdx < items.length) items[highlightIdx].classList.remove('active');
highlightIdx = highlightIdx < items.length - 1 ? highlightIdx + 1 : 0;
items[highlightIdx].classList.add('active'); items[highlightIdx].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'ArrowUp') {
e.preventDefault();
if (highlightIdx >= 0 && highlightIdx < items.length) items[highlightIdx].classList.remove('active');
highlightIdx = highlightIdx > 0 ? highlightIdx - 1 : items.length - 1;
items[highlightIdx].classList.add('active'); items[highlightIdx].scrollIntoView({ block: 'nearest' });
} else if (e.key === 'Escape') {
suggestions.style.display = 'none'; highlightIdx = -1;
}
});
input.addEventListener('blur', () => { setTimeout(() => { suggestions.style.display = 'none'; }, 150); });
} // end isLoggedIn tag block
// ESC to close, "e" to open // ESC to close, "e" to open
document.addEventListener('keydown', (e) => { document.addEventListener('keydown', (e) => {
@@ -6922,30 +6878,39 @@ window.cancelAnimFrame = (function () {
const THUMB_SIZES = ['s', 'm', 'l', 'xl']; const THUMB_SIZES = ['s', 'm', 'l', 'xl'];
const gridSizeBtns = overlay.querySelectorAll('.nav-grid-size-btn'); const gridSizeBtns = overlay.querySelectorAll('.nav-grid-size-btn');
const applyNavThumbSize = (size) => { const applyNavThumbSize = (size, persist = true) => {
if (!THUMB_SIZES.includes(size)) size = 'm'; if (!THUMB_SIZES.includes(size)) size = window.f0ckDefaultThumbSize || 'm';
document.documentElement.dataset.thumbSize = size; document.documentElement.dataset.thumbSize = size;
localStorage.setItem(THUMB_SIZE_KEY, size); if (persist) localStorage.setItem(THUMB_SIZE_KEY, size);
gridSizeBtns.forEach(b => b.classList.toggle('active', b.dataset.thumbSize === size)); gridSizeBtns.forEach(b => b.classList.toggle('active', b.dataset.thumbSize === size));
}; };
// Sync active state each time the modal opens // Sync active state each time the modal opens
if (filterBtn) { if (filterBtn) {
filterBtn.addEventListener('click', () => { filterBtn.addEventListener('click', () => {
const cur = localStorage.getItem(THUMB_SIZE_KEY) || 'm'; const cur = localStorage.getItem(THUMB_SIZE_KEY) || window.f0ckDefaultThumbSize || 'm';
gridSizeBtns.forEach(b => b.classList.toggle('active', b.dataset.thumbSize === cur)); gridSizeBtns.forEach(b => b.classList.toggle('active', b.dataset.thumbSize === cur));
}); });
} }
gridSizeBtns.forEach(btn => { gridSizeBtns.forEach(btn => {
btn.addEventListener('click', () => applyNavThumbSize(btn.dataset.thumbSize)); btn.addEventListener('click', () => applyNavThumbSize(btn.dataset.thumbSize, true));
}); });
// Apply persisted size immediately on init (every page load) // Apply on init — do NOT persist so config default stays effective until user chooses
applyNavThumbSize(localStorage.getItem(THUMB_SIZE_KEY) || 'm'); applyNavThumbSize(localStorage.getItem(THUMB_SIZE_KEY) || window.f0ckDefaultThumbSize || 'm', false);
};
// ── Thumb size init (runs on every page, independently of the filter modal) ──
const initThumbSize = () => {
const THUMB_SIZE_KEY = 'scroller_thumb_size';
const THUMB_SIZES = ['s', 'm', 'l', 'xl'];
const size = localStorage.getItem(THUMB_SIZE_KEY) || window.f0ckDefaultThumbSize || 'm';
document.documentElement.dataset.thumbSize = THUMB_SIZES.includes(size) ? size : (window.f0ckDefaultThumbSize || 'm');
}; };
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
initThumbSize(); // always runs — applies data-thumb-size to <html>
initSearch(); initSearch();
initExcludedTagsModal(); initExcludedTagsModal();
if (window.updateFilterBadge) window.updateFilterBadge(); if (window.updateFilterBadge) window.updateFilterBadge();
+28 -17
View File
@@ -100,27 +100,27 @@
// ── Grid / Thumbnail size ───────────────────────────────────────────────── // ── Grid / Thumbnail size ─────────────────────────────────────────────────
const THUMB_SIZES = ['s', 'm', 'l', 'xl']; const THUMB_SIZES = ['s', 'm', 'l', 'xl'];
let thumbSize = localStorage.getItem(THUMB_SIZE_KEY) || 'm'; let thumbSize = localStorage.getItem(THUMB_SIZE_KEY) || window.f0ckDefaultThumbSize || 'm';
if (!THUMB_SIZES.includes(thumbSize)) thumbSize = 'm'; if (!THUMB_SIZES.includes(thumbSize)) thumbSize = window.f0ckDefaultThumbSize || 'm';
function applyThumbSize(size) { function applyThumbSize(size, persist = true) {
thumbSize = size; thumbSize = size;
document.documentElement.dataset.thumbSize = size; document.documentElement.dataset.thumbSize = size;
localStorage.setItem(THUMB_SIZE_KEY, size); if (persist) localStorage.setItem(THUMB_SIZE_KEY, size);
// Sync pill active state whenever called outside the click handler // Sync pill active state whenever called outside the click handler
document.querySelectorAll('#thumb-size-pills .filter-pill').forEach(p => document.querySelectorAll('#thumb-size-pills .filter-pill').forEach(p =>
p.classList.toggle('active', p.dataset.thumbSize === size) p.classList.toggle('active', p.dataset.thumbSize === size)
); );
} }
// Apply persisted size on load // Apply on init — do NOT persist so config default stays effective until user chooses
applyThumbSize(thumbSize); applyThumbSize(thumbSize, false);
// Wire pill click events // Wire pill click events
const thumbSizePillsEl = document.getElementById('thumb-size-pills'); const thumbSizePillsEl = document.getElementById('thumb-size-pills');
if (thumbSizePillsEl) { if (thumbSizePillsEl) {
thumbSizePillsEl.querySelectorAll('.filter-pill').forEach(pill => { thumbSizePillsEl.querySelectorAll('.filter-pill').forEach(pill => {
pill.addEventListener('click', () => { pill.addEventListener('click', () => {
applyThumbSize(pill.dataset.thumbSize); applyThumbSize(pill.dataset.thumbSize, true);
}); });
}); });
} }
@@ -1373,7 +1373,8 @@
const meta = document.createElement('div'); meta.className = 'scroll-meta'; const meta = document.createElement('div'); meta.className = 'scroll-meta';
// esc() the color value: a raw '"' in username_color would break out of the // esc() the color value: a raw '"' in username_color would break out of the
// style attribute and allow arbitrary HTML injection (XSS). // style attribute and allow arbitrary HTML injection (XSS).
const colorStyle = item.username_color ? `color:${esc(item.username_color)}` : ''; const isAnonGuest = window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in;
const colorStyle = (!isAnonGuest && item.username_color) ? `color:${esc(item.username_color)}` : '';
const ratingHtml = `<span class="scroll-rating ${esc(item.rating_class)}" data-item-id="${item.id}" data-rating="${esc(item.rating_class)}">${esc(item.rating_label)}</span>`; const ratingHtml = `<span class="scroll-rating ${esc(item.rating_class)}" data-item-id="${item.id}" data-rating="${esc(item.rating_class)}">${esc(item.rating_label)}</span>`;
const ocHtml = item.is_oc ? `<span class="scroll-oc"><i class="fa-solid fa-star" style="font-size:.6rem"></i> OC</span>` : ''; const ocHtml = item.is_oc ? `<span class="scroll-oc"><i class="fa-solid fa-star" style="font-size:.6rem"></i> OC</span>` : '';
const badgesHtml = `<div class="scroll-badges">${ratingHtml}${ocHtml}</div>`; const badgesHtml = `<div class="scroll-badges">${ratingHtml}${ocHtml}</div>`;
@@ -1383,10 +1384,15 @@
item.tags.split(', ').map(t => `<span class="scroll-tag-pill" data-tag="${esc(t.trim())}">${esc(t.trim())}</span>`).join('') item.tags.split(', ').map(t => `<span class="scroll-tag-pill" data-tag="${esc(t.trim())}">${esc(t.trim())}</span>`).join('')
}</div>` }</div>`
: ''; : '';
meta.innerHTML = ` const userMetaHtml = isAnonGuest
<div class="scroll-meta-inner"> ? `<div class="scroll-meta-top">
${badgesHtml} <img class="scroll-avatar" src="/a/default.png" alt="" loading="lazy">
<div class="scroll-meta-top"> <div>
<div class="scroll-username">anonymous</div>
<div class="scroll-timeago">${esc(item.stamp ? timeAgo(item.stamp * 1000) : (item.timeago || ''))}</div>
</div>
</div>`
: `<div class="scroll-meta-top">
<a href="/user/${esc(item.username)}" class="scroll-user-link"> <a href="/user/${esc(item.username)}" class="scroll-user-link">
<img class="scroll-avatar" src="${esc(item.avatar)}" alt="" loading="lazy" onerror="this.src='/a/default.png'"> <img class="scroll-avatar" src="${esc(item.avatar)}" alt="" loading="lazy" onerror="this.src='/a/default.png'">
</a> </a>
@@ -1396,7 +1402,11 @@
</a> </a>
<div class="scroll-timeago">${esc(item.stamp ? timeAgo(item.stamp * 1000) : (item.timeago || ''))}</div> <div class="scroll-timeago">${esc(item.stamp ? timeAgo(item.stamp * 1000) : (item.timeago || ''))}</div>
</div> </div>
</div> </div>`;
meta.innerHTML = `
<div class="scroll-meta-inner">
${badgesHtml}
${userMetaHtml}
${tagsHtml} ${tagsHtml}
${item.is_external && item.external_board && item.external_tid ${item.is_external && item.external_board && item.external_tid
? `<a class="scroll-id-link" href="https://boards.4chan.org/${item.external_board}/thread/${item.external_tid}#p${item.external_id}" target="_blank">/${item.external_board}/thread/${item.external_tid}</a>` ? `<a class="scroll-id-link" href="https://boards.4chan.org/${item.external_board}/thread/${item.external_tid}#p${item.external_id}" target="_blank">/${item.external_board}/thread/${item.external_tid}</a>`
@@ -2187,10 +2197,11 @@
function renderCommentEl(c, canReply) { function renderCommentEl(c, canReply) {
const el = document.createElement('div'); el.className = 'comment-item'; const el = document.createElement('div'); el.className = 'comment-item';
el.dataset.commentId = c.id || ''; el.dataset.commentId = c.id || '';
const av = c.avatar_file ? `/a/${c.avatar_file}` : (c.avatar ? `/t/${c.avatar}.webp` : '/a/default.png'); const isAnonGuest = window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in;
const nc = c.username_color ? `color:${esc(c.username_color)}` : ''; const av = isAnonGuest ? '/a/default.png' : (c.avatar_file ? `/a/${c.avatar_file}` : (c.avatar ? `/t/${c.avatar}.webp` : '/a/default.png'));
const nc = (!isAnonGuest && c.username_color) ? `color:${esc(c.username_color)}` : '';
const _i = window.f0ckI18n || {}; const _i = window.f0ckI18n || {};
const uname = c.display_name || c.username || 'anon'; const uname = isAnonGuest ? 'anonymous' : (c.display_name || c.username || 'anon');
el.innerHTML = ` el.innerHTML = `
<img class="comment-avatar" src="${esc(av)}" alt="" loading="lazy" onerror="this.src='/a/default.png'"> <img class="comment-avatar" src="${esc(av)}" alt="" loading="lazy" onerror="this.src='/a/default.png'">
<div class="comment-body"> <div class="comment-body">
@@ -2198,7 +2209,7 @@
<div class="comment-content" data-raw="${esc(c.content || '')}">${renderCommentContent(c.content || '')}</div> <div class="comment-content" data-raw="${esc(c.content || '')}">${renderCommentContent(c.content || '')}</div>
<div class="comment-meta"> <div class="comment-meta">
<span class="comment-time">${c.created_at ? timeAgo(c.created_at) : (_i.ta_just_now || _i.just_now || 'just now')}</span> <span class="comment-time">${c.created_at ? timeAgo(c.created_at) : (_i.ta_just_now || _i.just_now || 'just now')}</span>
${canReply ? ` ${(!isAnonGuest && canReply) ? `
<button class="comment-reply-btn" data-id="${c.id}" data-user="${esc(c.username || uname)}">${_i.reply || 'Reply'}</button> <button class="comment-reply-btn" data-id="${c.id}" data-user="${esc(c.username || uname)}">${_i.reply || 'Reply'}</button>
<button class="comment-quote-btn" data-id="${c.id}" data-user="${esc(c.username || uname)}">${_i.quote || 'Quote'}</button> <button class="comment-quote-btn" data-id="${c.id}" data-user="${esc(c.username || uname)}">${_i.quote || 'Quote'}</button>
` : ''} ` : ''}
+12 -5
View File
@@ -562,15 +562,22 @@
? `style="--author-banner: url('/a/${bannerFile}'); --author-banner-position: ${bannerPos === 'center' ? 'center top' : (bannerPos || 'center top')}; --author-banner-size: ${(bannerSz && bannerSz !== 'cover') ? bannerSz : '100% auto'}; --author-banner-repeat: no-repeat;"` ? `style="--author-banner: url('/a/${bannerFile}'); --author-banner-position: ${bannerPos === 'center' ? 'center top' : (bannerPos || 'center top')}; --author-banner-size: ${(bannerSz && bannerSz !== 'cover') ? bannerSz : '100% auto'}; --author-banner-repeat: no-repeat;"`
: ''; : '';
const isAnonGuest = window.f0ckSession?.guest_anonymize && !window.f0ckSession?.logged_in;
const effectiveBannerStyle = isAnonGuest ? '' : bannerStyle;
const authorAvatarHtml = isAnonGuest
? `<span class="sidebar-avatar-link"><img src="/a/default.png" class="sidebar-avatar" loading="eager" /></span>`
: `<a href="/user/${c.username.toLowerCase()}" class="sidebar-avatar-link"><img src="${avatarSrc}" class="sidebar-avatar" loading="eager" onload="this.classList.add('loaded')" onerror="this.classList.add('loaded');this.src='/a/default.png'" /></a>`;
const authorNameHtml = isAnonGuest
? `<span class="comment-author">anonymous</span>`
: `<a href="/user/${c.username.toLowerCase()}" class="comment-author" ${c.username_color ? `style="color: ${c.username_color}"` : ''}>${escapeHtml(c.display_name || c.username)}</a>`;
return ` return `
<div class="comment" id="sc${c.id}" ${bannerStyle}> <div class="comment" id="sc${c.id}" ${effectiveBannerStyle}>
<div class="comment-body"> <div class="comment-body">
<div class="comment-header"> <div class="comment-header">
<div class="comment-header-left"> <div class="comment-header-left">
<a href="/user/${c.username.toLowerCase()}" class="sidebar-avatar-link"> ${authorAvatarHtml}
<img src="${avatarSrc}" class="sidebar-avatar" loading="eager" onload="this.classList.add('loaded')" onerror="this.classList.add('loaded');this.src='/a/default.png'" /> ${authorNameHtml}
</a>
<a href="/user/${c.username.toLowerCase()}" class="comment-author" ${c.username_color ? `style="color: ${c.username_color}"` : ''}>${escapeHtml(c.display_name || c.username)}</a>
</div> </div>
<span class="comment-time timeago" tooltip="${fullDate}" style="font-size: 0.75em;"${tsAttr}>${timeStr}</span> <span class="comment-time timeago" tooltip="${fullDate}" style="font-size: 0.75em;"${tsAttr}>${timeStr}</span>
</div> </div>
+23 -6
View File
@@ -63,11 +63,22 @@ export default (router, tpl) => {
} }
// Transform for frontend if needed, or send as is // Transform for frontend if needed, or send as is
const anonymize = !req.session && cfg.main.guest_anonymize;
const outComments = anonymize
? comments.map(c => ({
...c,
username: 'anonymous',
display_name: null,
username_color: null,
avatar: null,
avatar_file: null
}))
: comments;
return res.reply({ return res.reply({
headers: { 'Content-Type': 'application/json; charset=utf-8' }, headers: { 'Content-Type': 'application/json; charset=utf-8' },
body: JSON.stringify({ body: JSON.stringify({
success: true, success: true,
comments, comments: outComments,
is_subscribed, is_subscribed,
is_locked, is_locked,
user_id: req.session ? req.session.id : null, user_id: req.session ? req.session.id : null,
@@ -122,6 +133,9 @@ export default (router, tpl) => {
// Browse User Comments // Browse User Comments
router.get(/\/user\/(?<user>[^\/]+)\/comments/, async (req, res) => { router.get(/\/user\/(?<user>[^\/]+)\/comments/, async (req, res) => {
if (cfg.main.guest_anonymize && !req.session) {
return res.redirect('/login');
}
const user = decodeURIComponent(req.params.user); const user = decodeURIComponent(req.params.user);
try { try {
@@ -1135,6 +1149,7 @@ export default (router, tpl) => {
} }
} }
const isAnonymized = !req.session && cfg.main.guest_anonymize;
const processedComments = comments.map(c => { const processedComments = comments.map(c => {
let ratingLabel = '?'; let ratingLabel = '?';
let ratingClass = 'untagged'; let ratingClass = 'untagged';
@@ -1144,9 +1159,6 @@ export default (router, tpl) => {
const commentContent = (c.content || '').trim(); const commentContent = (c.content || '').trim();
const commentFiles = filesMap.get(c.id) || []; const commentFiles = filesMap.get(c.id) || [];
// Compute overflow hint: true if content is likely taller than the 80px clamp.
// ~120 chars ≈ 2-3 wrapped lines in the sidebar; any newlines > 2 or file
// attachments (images/video) will also push height above the limit.
const isLong = commentContent.length > 120 const isLong = commentContent.length > 120
|| commentContent.split('\n').length > 2 || commentContent.split('\n').length > 2
|| commentFiles.length > 0 || commentFiles.length > 0
@@ -1154,17 +1166,22 @@ export default (router, tpl) => {
return { return {
...c, ...c,
username: isAnonymized ? 'anonymous' : c.username,
display_name: isAnonymized ? null : c.display_name,
username_color: isAnonymized ? null : c.username_color,
avatar: isAnonymized ? null : c.avatar,
avatar_file: isAnonymized ? null : c.avatar_file,
banner_file: isAnonymized ? null : c.banner_file,
content: commentContent, content: commentContent,
username_color: c.username_color,
item_rating_class: ratingClass, item_rating_class: ratingClass,
item_rating_label: ratingLabel, item_rating_label: ratingLabel,
files: commentFiles, files: commentFiles,
poll: pollMap.get(c.id) || null, poll: pollMap.get(c.id) || null,
is_long: isLong is_long: isLong
// created_at stays as the raw ISO timestamp so the frontend f0ckTimeAgo can localize it
}; };
}); });
if (req.url.qs?.json === 'true' || req.headers['x-requested-with'] === 'XMLHttpRequest') { if (req.url.qs?.json === 'true' || req.headers['x-requested-with'] === 'XMLHttpRequest') {
return res.reply({ return res.reply({
headers: { headers: {
+13
View File
@@ -12,6 +12,10 @@ const auth = async (req, res, next) => {
export default (router, tpl) => { export default (router, tpl) => {
router.get(/\/user\/(?<user>[^/]+)\/?$/, async (req, res) => { router.get(/\/user\/(?<user>[^/]+)\/?$/, async (req, res) => {
// When guest anonymization is active, user profiles must not be accessible without a session
if (cfg.main.guest_anonymize && !req.session) {
return res.redirect('/login');
}
const user = decodeURIComponent(req.params.user); const user = decodeURIComponent(req.params.user);
const mime = req.cookies.mime !== undefined ? req.cookies.mime : (req.query?.mime || req.url.qs?.mime || null); const mime = req.cookies.mime !== undefined ? req.cookies.mime : (req.query?.mime || req.url.qs?.mime || null);
@@ -223,6 +227,11 @@ export default (router, tpl) => {
return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) }); return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) });
} }
// When guest anonymization is active, user gallery pages must require authentication
if (cfg.main.guest_anonymize && !req.session && req.params.user && req.params.mode) {
return res.redirect('/login');
}
// Auto-persist strict mode from URL to session if it's there // Auto-persist strict mode from URL to session if it's there
if (req.session && (req.query?.strict !== undefined || req.url.qs?.strict !== undefined)) { if (req.session && (req.query?.strict !== undefined || req.url.qs?.strict !== undefined)) {
req.session.strict_mode = (req.query?.strict === '1' || req.url.qs?.strict === '1'); req.session.strict_mode = (req.query?.strict === '1' || req.url.qs?.strict === '1');
@@ -384,6 +393,10 @@ export default (router, tpl) => {
// Hall columns for display // Hall columns for display
data.halls_slugs = Array.isArray(item.halls) ? item.halls.map(h => h.slug).join(',') : ''; data.halls_slugs = Array.isArray(item.halls) ? item.halls.map(h => h.slug).join(',') : '';
data.user_halls_slugs = Array.isArray(item.user_halls) ? item.user_halls.map(h => h.slug).join(',') : ''; data.user_halls_slugs = Array.isArray(item.user_halls) ? item.user_halls.map(h => h.slug).join(',') : '';
// When guest anonymization is active, suppress source URL from the info modal
if (cfg.main.guest_anonymize && !req.session && item.src) {
item.src = null;
}
// Precomputed for template engine compatibility (avoids nested { } inside {{ }}) // Precomputed for template engine compatibility (avoids nested { } inside {{ }})
data.item_rating_class = item.is_nsfl ? 'is-nsfl' : (item.is_nsfw ? 'is-nsfw' : (item.is_sfw ? 'is-sfw' : 'is-untagged')); data.item_rating_class = item.is_nsfl ? 'is-nsfl' : (item.is_nsfw ? 'is-nsfw' : (item.is_sfw ? 'is-sfw' : 'is-untagged'));
data.item_rating_label = item.is_nsfl ? 'NSFL' : (item.is_nsfw ? 'NSFW' : (item.is_sfw ? 'SFW' : '?')); data.item_rating_label = item.is_nsfl ? 'NSFL' : (item.is_nsfw ? 'NSFW' : (item.is_sfw ? 'SFW' : '?'));
+13 -1
View File
@@ -377,9 +377,21 @@ export default (router, tpl) => {
const lastItem = items[items.length - 1]; const lastItem = items[items.length - 1];
const nextCursor = lastItem ? lastItem.id : null; const nextCursor = lastItem ? lastItem.id : null;
const isAnonymized = !req.session && cfg.main.guest_anonymize;
const outItems = isAnonymized
? items.map(item => ({
...item,
username: 'anonymous',
display_name: 'anonymous',
avatar: '/a/default.png',
username_color: null,
src_host: ''
}))
: items;
return res.reply({ return res.reply({
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store, no-cache' }, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store, no-cache' },
body: JSON.stringify({ success: true, items, nextCursor }) body: JSON.stringify({ success: true, items: outItems, nextCursor })
}); });
} catch (e) { } catch (e) {
console.error('[SCROLLER] Feed error:', e); console.error('[SCROLLER] Feed error:', e);
+13
View File
@@ -43,6 +43,9 @@ export default (router, tpl) => {
// List halls for a user // List halls for a user
router.get(/^\/user\/(?<owner>[^/]+)\/halls\/?$/, async (req, res) => { router.get(/^\/user\/(?<owner>[^/]+)\/halls\/?$/, async (req, res) => {
if (cfg.main.guest_anonymize && !req.session) {
return res.redirect('/login');
}
if (cfg.websrv.userhalls_enabled === false) return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) }); if (cfg.websrv.userhalls_enabled === false) return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) });
const ownerName = decodeURIComponent(req.params.owner); const ownerName = decodeURIComponent(req.params.owner);
const mode = req.mode ?? 0; const mode = req.mode ?? 0;
@@ -80,6 +83,9 @@ export default (router, tpl) => {
// Item grid for a user hall // Item grid for a user hall
router.get(/^\/user\/(?<owner>[^/]+)\/hall\/(?<slug>[^/]+)(?:\/p\/(?<page>\d+))?\/?$/, async (req, res) => { router.get(/^\/user\/(?<owner>[^/]+)\/hall\/(?<slug>[^/]+)(?:\/p\/(?<page>\d+))?\/?$/, async (req, res) => {
if (cfg.main.guest_anonymize && !req.session) {
return res.redirect('/login');
}
if (cfg.websrv.userhalls_enabled === false) return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) }); if (cfg.websrv.userhalls_enabled === false) return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) });
const ownerName = decodeURIComponent(req.params.owner); const ownerName = decodeURIComponent(req.params.owner);
const slug = decodeURIComponent(req.params.slug); const slug = decodeURIComponent(req.params.slug);
@@ -126,6 +132,9 @@ export default (router, tpl) => {
// Single item within a user hall // Single item within a user hall
router.get(/^\/user\/(?<owner>[^/]+)\/hall\/(?<slug>[^/]+)\/(?<itemid>\d+)\/?$/, async (req, res) => { router.get(/^\/user\/(?<owner>[^/]+)\/hall\/(?<slug>[^/]+)\/(?<itemid>\d+)\/?$/, async (req, res) => {
if (cfg.main.guest_anonymize && !req.session) {
return res.redirect('/login');
}
if (cfg.websrv.userhalls_enabled === false) return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) }); if (cfg.websrv.userhalls_enabled === false) return res.reply({ code: 404, body: tpl.render('error', { message: 'Not found', tmp: null }, req) });
const ownerName = decodeURIComponent(req.params.owner); const ownerName = decodeURIComponent(req.params.owner);
const slug = decodeURIComponent(req.params.slug); const slug = decodeURIComponent(req.params.slug);
@@ -176,6 +185,10 @@ export default (router, tpl) => {
data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : ''); data.current_user_hall_slug = (data.tmp && data.tmp.userHall && typeof data.tmp.userHall === 'object') ? data.tmp.userHall.slug : (data.tmp && data.tmp.userHall ? data.tmp.userHall : '');
data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : ''; data.current_user_hall_owner = (data.tmp && data.tmp.userHallOwner) ? data.tmp.userHallOwner : '';
data.item_has_dimensions = !!(item.width && item.height); data.item_has_dimensions = !!(item.width && item.height);
// When guest anonymization is active, suppress source URL from the info modal
if (cfg.main.guest_anonymize && !req.session && item.src) {
item.src = null;
}
} }
// Precompute hall display // Precompute hall display
+3
View File
@@ -158,6 +158,9 @@ export default (router, tpl) => {
// Main tags page // Main tags page
router.get(/^\/user\/(?<user>[^\/]+)\/tags$/, async (req, res) => { router.get(/^\/user\/(?<user>[^\/]+)\/tags$/, async (req, res) => {
if (cfg.main.guest_anonymize && !req.session) {
return res.redirect('/login');
}
const userParam = decodeURIComponent(req.params.user); const userParam = decodeURIComponent(req.params.user);
const u = await db` const u = await db`
+2
View File
@@ -1419,6 +1419,7 @@ process.on('uncaughtException', err => {
site: cfg.main.url.full, site: cfg.main.url.full,
domain: cfg.main.url.domain, domain: cfg.main.url.domain,
hide_comments_from_public: cfg.main.hide_comments_from_public, hide_comments_from_public: cfg.main.hide_comments_from_public,
guest_anonymize: !!cfg.main.guest_anonymize,
git_hash: typeof gitHash !== 'undefined' ? gitHash : 'unknown', git_hash: typeof gitHash !== 'undefined' ? gitHash : 'unknown',
get motd() { return getMotd(); }, get motd() { return getMotd(); },
get manual_approval() { return getManualApproval(); }, get manual_approval() { return getManualApproval(); },
@@ -1495,6 +1496,7 @@ process.on('uncaughtException', err => {
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_dynamic_thumbs: !!cfg.websrv.enable_dynamic_thumbs,
default_thumb_size: (() => { const v = (cfg.websrv.default_thumb_size || '').toLowerCase(); return ['s', 'm', 'l', 'xl'].includes(v) ? v : 'm'; })(),
comment_max_length: cfg.main.comment_max_length ?? null, comment_max_length: cfg.main.comment_max_length ?? null,
enable_swf: !!cfg.websrv.enable_swf, enable_swf: !!cfg.websrv.enable_swf,
enable_archive: !!cfg.websrv.enable_archive, enable_archive: !!cfg.websrv.enable_archive,
+7 -1
View File
@@ -100,7 +100,11 @@
<div class="user-infobox-info"> <div class="user-infobox-info">
<div class="user-infobox-header"> <div class="user-infobox-header">
<div class="user-infobox-username-container"> <div class="user-infobox-username-container">
@if(session || !guest_anonymize)
<a id="a_username" data-username="{{ item.username || '' }}" data-author-id="{{ item.author_id || '' }}" href="/user/{{ (item.username || '').toLowerCase() }}" tooltip="ID: {{ item.author_id }}" class="user-infobox-username">{!! item.author_display_name || item.username !!}</a> <a id="a_username" data-username="{{ item.username || '' }}" data-author-id="{{ item.author_id || '' }}" href="/user/{{ (item.username || '').toLowerCase() }}" tooltip="ID: {{ item.author_id }}" class="user-infobox-username">{!! item.author_display_name || item.username !!}</a>
@else
<span class="user-infobox-username">anonymous</span>
@endif
</div> </div>
<span class="user-infobox-timestamp"><a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span> <span class="user-infobox-timestamp"><a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="timestamp-link"><time class="timeago" tooltip="{{ item.timestamp.timefull }}">{{item.timestamp.timeago }}</time></a></span>
</div> </div>
@@ -126,7 +130,7 @@
<span class="badge badge-dark"> <span class="badge badge-dark">
<a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="id-link" data-item-id="{{ item.id }}" @if(user_alternative_infobox)style="display:none"@endif>{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</a> <a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="id-link" data-item-id="{{ item.id }}" @if(user_alternative_infobox)style="display:none"@endif>{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</a>
@if(!user_alternative_infobox) — [<a id="a_username" data-username="{{ item.username || '' }}" @if(session) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ (item.username || '').toLowerCase() }}" @if(session && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>] @endif @if(!user_alternative_infobox) — [@if(session || !guest_anonymize)<a id="a_username" data-username="{{ item.username || '' }}" @if(session) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ (item.username || '').toLowerCase() }}" @if(session && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>@else<span>anonymous</span>@endif] @endif
@if(!user_alternative_infobox) @if(!user_alternative_infobox)
<span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span> <span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
@endif @endif
@@ -206,6 +210,8 @@
@each(item.favorites as fav) @each(item.favorites as fav)
@if(fav.hide_fav_badge && (!session || session.user !== fav.user)) @if(fav.hide_fav_badge && (!session || session.user !== fav.user))
<a class="ghost-fav" tooltip="?" flow="up" style="cursor: default;"><img src="/s/img/ghost_fav.svg" style="height: 32px; width: 32px;" loading="lazy" /></a> <a class="ghost-fav" tooltip="?" flow="up" style="cursor: default;"><img src="/s/img/ghost_fav.svg" style="height: 32px; width: 32px;" loading="lazy" /></a>
@elseif(!session && guest_anonymize)
<a class="ghost-fav" tooltip="anonymous" flow="up" style="cursor: default;"><img src="/a/default.png" style="height: 32px; width: 32px;" loading="lazy" /></a>
@else @else
<a href="/user/{{ fav.user.toLowerCase() }}" tooltip="{!! fav.display_name || fav.user !!}" flow="up"><img src="@if(fav.avatar_file)/a/{{ fav.avatar_file }}@elseif(fav.avatar)/t/{{ fav.avatar }}.webp@else/a/default.png@endif" style="height: 32px; width: 32px@if(fav.username_color); border-color: {{ fav.username_color }}@endif" loading="lazy" /></a> <a href="/user/{{ fav.user.toLowerCase() }}" tooltip="{!! fav.display_name || fav.user !!}" flow="up"><img src="@if(fav.avatar_file)/a/{{ fav.avatar_file }}@elseif(fav.avatar)/t/{{ fav.avatar }}.webp@else/a/default.png@endif" style="height: 32px; width: 32px@if(fav.username_color); border-color: {{ fav.username_color }}@endif" loading="lazy" /></a>
@endif @endif
+3 -1
View File
@@ -131,7 +131,7 @@
</div> </div>
<div class="blahlol"> <div class="blahlol">
<span class="badge badge-dark"> <span class="badge badge-dark">
<a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="id-link" data-item-id="{{ item.id }}">{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</a> — [<a id="a_username" data-username="{{ item.username || '' }}" @if(session) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ item_username_lower }}" @if(session && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>] <span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span> <a href="/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="id-link" data-item-id="{{ item.id }}">{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</a> — [@if(session || !guest_anonymize)<a id="a_username" data-username="{{ item.username || '' }}" @if(session) data-author-id="{{ item.author_id || '' }}" @endif href="/user/{{ item_username_lower }}" @if(session && item.author_id) tooltip="ID: {{ item.author_id }}" @endif @if(item.author_color) style="color: {{ item.author_color }}" @endif>{!! item.author_display_name || item.username || 'unknown' !!}</a>@else<span>anonymous</span>@endif] <span id="oc-badge-container">@if(item.is_oc) — <span class="oc-badge" tooltip="Original Content">OC</span>@endif</span>
</span> </span>
<span id="hall-badge-container">@if(halls_enabled && item.primaryHall)<span class="badge hall-badge-wrap"> <span id="hall-badge-container">@if(halls_enabled && item.primaryHall)<span class="badge hall-badge-wrap">
<a href="/h/{{ item.primaryHall.slug }}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> {{ item.primaryHall.name }}</a>@if(item.otherHalls && item.otherHalls.length)<span class="hall-overflow-pill">+{{ item.otherHalls.length }}<span class="hall-overflow-tooltip">@each(item.otherHalls as oh)<a href="/h/{{ oh.slug }}">{{ oh.name }}</a>@endeach</span></span>@endif <a href="/h/{{ item.primaryHall.slug }}" class="hall-badge-primary"><i class="fa-solid fa-layer-group"></i> {{ item.primaryHall.name }}</a>@if(item.otherHalls && item.otherHalls.length)<span class="hall-overflow-pill">+{{ item.otherHalls.length }}<span class="hall-overflow-tooltip">@each(item.otherHalls as oh)<a href="/h/{{ oh.slug }}">{{ oh.name }}</a>@endeach</span></span>@endif
@@ -174,6 +174,8 @@
@each(item.favorites as fav) @each(item.favorites as fav)
@if(fav.hide_fav_badge && (!session || session.user !== fav.user)) @if(fav.hide_fav_badge && (!session || session.user !== fav.user))
<a class="ghost-fav" tooltip="?" flow="up" style="cursor: default;"><img src="/s/img/ghost_fav.svg" style="height: 32px; width: 32px;" loading="lazy" /></a> <a class="ghost-fav" tooltip="?" flow="up" style="cursor: default;"><img src="/s/img/ghost_fav.svg" style="height: 32px; width: 32px;" loading="lazy" /></a>
@elseif(!session && guest_anonymize)
<a class="ghost-fav" tooltip="anonymous" flow="up" style="cursor: default;"><img src="/a/default.png" style="height: 32px; width: 32px;" loading="lazy" /></a>
@else @else
<a href="/user/{{ fav.user.toLowerCase() }}" tooltip="{!! fav.display_name || fav.user !!}" flow="up"><img src="@if(fav.avatar_file)/a/{{ fav.avatar_file }}@elseif(fav.avatar)/t/{{ fav.avatar }}.webp@else/a/default.png@endif" style="height: 32px; width: 32px@if(fav.username_color); border-color: {{ fav.username_color }}@endif" loading="lazy" /></a> <a href="/user/{{ fav.user.toLowerCase() }}" tooltip="{!! fav.display_name || fav.user !!}" flow="up"><img src="@if(fav.avatar_file)/a/{{ fav.avatar_file }}@elseif(fav.avatar)/t/{{ fav.avatar }}.webp@else/a/default.png@endif" style="height: 32px; width: 32px@if(fav.username_color); border-color: {{ fav.username_color }}@endif" loading="lazy" /></a>
@endif @endif
+4 -1
View File
@@ -139,8 +139,9 @@
@endif @endif
@if(private_society && !session) @if(private_society && !session)
<script> <script>
window.f0ckSession = { logged_in: false, onara: @if(onara) true @else false @endif, enable_item_slugs: @if(enable_item_slugs) true @else false @endif, hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }}, development: @if(development) true @else false @endif, allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif, hide_sidebar_default: @if(hide_sidebar_default) true @else false @endif, public_untagged: @if(public_untagged) true @else false @endif, public_nsfw: @if(public_nsfw) true @else false @endif }; window.f0ckSession = { logged_in: false, onara: @if(onara) true @else false @endif, enable_item_slugs: @if(enable_item_slugs) true @else false @endif, hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif, guest_anonymize: @if(guest_anonymize) true @else false @endif, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }}, development: @if(development) true @else false @endif, allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif, hide_sidebar_default: @if(hide_sidebar_default) true @else false @endif, public_untagged: @if(public_untagged) true @else false @endif, public_nsfw: @if(public_nsfw) true @else false @endif };
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {}; window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
window.f0ckDefaultThumbSize = "{{ default_thumb_size }}";
(() => { (() => {
const loginModal = document.getElementById('login-modal'); const loginModal = document.getElementById('login-modal');
const registerModal = document.getElementById('register-modal'); const registerModal = document.getElementById('register-modal');
@@ -384,6 +385,7 @@
strict_mode: @if(session && session.strict_mode) true @else false @endif, strict_mode: @if(session && session.strict_mode) true @else false @endif,
logged_in: @if(session) true @else false @endif, logged_in: @if(session) true @else false @endif,
hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif, hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif,
guest_anonymize: @if(guest_anonymize) true @else false @endif,
use_new_layout: @if(session)@if(session.use_new_layout) true @else false @endif@else @if(default_layout === 'legacy')false @else true @endif@endif, use_new_layout: @if(session)@if(session.use_new_layout) true @else false @endif@else @if(default_layout === 'legacy')false @else true @endif@endif,
csrf_token: "{{ csrf_token }}", csrf_token: "{{ csrf_token }}",
default_theme: "{{ default_theme }}", default_theme: "{{ default_theme }}",
@@ -435,6 +437,7 @@
mode: {{ mode !== undefined ? mode : 0 }} mode: {{ mode !== undefined ? mode : 0 }}
}; };
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {}; window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
window.f0ckDefaultThumbSize = "{{ default_thumb_size }}";
window.f0ckI18n = { window.f0ckI18n = {
write_comment: "{{ t('comments.write_comment') }}", write_comment: "{{ t('comments.write_comment') }}",
post: "{{ t('comments.post') }}", post: "{{ t('comments.post') }}",
+2 -2
View File
@@ -1,5 +1,5 @@
<!doctype html> <!doctype html>
<html lang="{{ lang || 'en' }}" theme="@if(typeof theme !== 'undefined'){{ theme }}@endif" res="@if(typeof fullscreen !== 'undefined'){{ fullscreen == 1 ? 'fullscreen' : '' }}@endif"> <html lang="{{ lang || 'en' }}" theme="@if(typeof theme !== 'undefined'){{ theme }}@endif" res="@if(typeof fullscreen !== 'undefined'){{ fullscreen == 1 ? 'fullscreen' : '' }}@endif" data-thumb-size="{{ default_thumb_size || 'm' }}">
<head> <head>
@if(typeof page_meta !== 'undefined' && page_meta.title)<title>{{ domain }} - {{ page_meta.title }}</title>@elseif(typeof item !== 'undefined')<title>{{ domain }} - {{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</title>@else<title>{{ domain }}</title>@endif @if(typeof page_meta !== 'undefined' && page_meta.title)<title>{{ domain }} - {{ page_meta.title }}</title>@elseif(typeof item !== 'undefined')<title>{{ domain }} - {{ (enable_item_slugs && item.slug) ? item.slug : item.id }}</title>@else<title>{{ domain }}</title>@endif
@@ -8,7 +8,7 @@
<meta name="mobile-web-app-capable" content="yes"> <meta name="mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent"> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="{{ domain }}"> <meta name="apple-mobile-web-app-title" content="{{ domain }}">
<script>(function(){var h=document.documentElement;if(localStorage.getItem('hideItemRatings')==='true')h.classList.add('hide-item-ratings-active');if(localStorage.getItem('blurNsfw')==='true')h.classList.add('blur-nsfw-active');if(localStorage.getItem('blurNsfl')==='true')h.classList.add('blur-nsfl-active');if(localStorage.getItem('blurSfw')==='true')h.classList.add('blur-sfw-active');if(localStorage.getItem('blurUntagged')==='true')h.classList.add('blur-untagged-active');if(localStorage.getItem('blurDetail')!=='false')h.classList.add('blur-detail-active');if(localStorage.getItem('imageExpandOnClick')!=='false')h.classList.add('image-expand-active');})();</script> <script>(function(){var h=document.documentElement;if(localStorage.getItem('hideItemRatings')==='true')h.classList.add('hide-item-ratings-active');if(localStorage.getItem('blurNsfw')==='true')h.classList.add('blur-nsfw-active');if(localStorage.getItem('blurNsfl')==='true')h.classList.add('blur-nsfl-active');if(localStorage.getItem('blurSfw')==='true')h.classList.add('blur-sfw-active');if(localStorage.getItem('blurUntagged')==='true')h.classList.add('blur-untagged-active');if(localStorage.getItem('blurDetail')!=='false')h.classList.add('blur-detail-active');if(localStorage.getItem('imageExpandOnClick')!=='false')h.classList.add('image-expand-active');var ts=localStorage.getItem('scroller_thumb_size');if(ts&&['s','m','l','xl'].includes(ts))h.dataset.thumbSize=ts;})();</script>
<style> <style>
html { background-color: #000; color: #fff; } html { background-color: #000; color: #fff; }
@if(session && session.font) @if(session && session.font)