This commit is contained in:
2026-09-21 00:02:48 +02:00
parent 4e857fd5ef
commit 990f463534
17 changed files with 690 additions and 168 deletions
+47
View File
@@ -7331,6 +7331,53 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
text-decoration: underline; text-decoration: underline;
} }
.chan-rehost-action-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 4px 16px;
min-width: 100px;
box-sizing: border-box;
white-space: nowrap;
flex-shrink: 0;
border-radius: 6px;
font-size: 0.8rem;
font-weight: 600;
font-family: inherit;
background: rgba(74, 222, 128, 0.15);
color: #4ade80;
border: 1px solid rgba(74, 222, 128, 0.3);
cursor: pointer;
text-decoration: none;
transition: all 0.15s ease;
}
.chan-rehost-action-btn:hover {
background: rgba(74, 222, 128, 0.28);
border-color: rgba(74, 222, 128, 0.55);
color: #4ade80;
text-decoration: none;
}
.chan-rehost-action-btn:active {
transform: scale(0.98);
}
.chan-rehost-action-btn.rehosted {
background: rgba(74, 222, 128, 0.2);
border-color: rgba(74, 222, 128, 0.4);
color: #4ade80;
min-width: auto;
padding: 4px 12px;
}
.chan-rehost-action-btn.loading {
opacity: 0.75;
cursor: wait;
pointer-events: none;
}
.item_title, .item_title,
.item-title, .item-title,
.item-itle { .item-itle {
+37 -13
View File
@@ -293,17 +293,34 @@
return; return;
} }
const favoBtns = document.querySelectorAll("#a_favo"); const favoBtns = document.querySelectorAll("#a_favo");
if (!favoBtns.length) return;
const firstFavoBtn = favoBtns[0]; const firstFavoBtn = favoBtns[0];
const chanRehostBtn = document.querySelector('#chan-item-rehost-btn'); const chanRehostBtn = document.querySelector('#chan-item-rehost-btn');
const isChan = firstFavoBtn?.dataset?.isChan === 'true' || !!chanRehostBtn; const isChan = firstFavoBtn?.dataset?.isChan === 'true' || !!chanRehostBtn;
const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url; const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url;
let localId = firstFavoBtn?.dataset?.localId; let localId = firstFavoBtn?.dataset?.localId;
const wasAlreadyFav = favoBtns[0].classList.contains('fa-solid') && !favoBtns[0].classList.contains('fa-spinner');
const setFavoUi = (isFav) => {
favoBtns.forEach(btn => {
btn.classList.remove('fa-spinner', 'fa-spin');
btn.classList.add('iconset', 'fa-heart');
btn.classList.toggle('fa-solid', isFav);
btn.classList.toggle('fa-regular', !isFav);
});
};
const setFavoLoading = () => {
favoBtns.forEach(btn => {
btn.classList.remove('fa-heart');
btn.classList.add('iconset', 'fa-solid', 'fa-spinner', 'fa-spin');
});
};
// If viewing un-rehosted 4chan post, auto-rehost first // If viewing un-rehosted 4chan post, auto-rehost first
if (isChan && !localId && chanUrl) { if (isChan && !localId && chanUrl) {
favoBtns.forEach(btn => { setFavoLoading();
btn.className = 'iconset fa-solid fa-spinner fa-spin';
});
try { try {
const csrfToken = window.f0ckSession?.csrf_token || ''; const csrfToken = window.f0ckSession?.csrf_token || '';
const rehostResp = await fetch('/api/v2/scroller/rehost', { const rehostResp = await fetch('/api/v2/scroller/rehost', {
@@ -331,6 +348,15 @@
const infoEl = document.querySelector("#a_info"); const infoEl = document.querySelector("#a_info");
if (infoEl) infoEl.dataset.itemId = localId; if (infoEl) infoEl.dataset.itemId = localId;
const timeEl = document.querySelector('time.timeago');
if (timeEl) {
const nowIso = new Date().toISOString();
timeEl.dataset.iso = nowIso;
const fullDate = typeof window.f0ckFormatDateFull === 'function' ? window.f0ckFormatDateFull(nowIso) : new Date().toLocaleString();
timeEl.setAttribute('tooltip', fullDate);
timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now'));
}
if (chanRehostBtn) { if (chanRehostBtn) {
chanRehostBtn.classList.add('rehosted'); chanRehostBtn.classList.add('rehosted');
chanRehostBtn.outerHTML = ` chanRehostBtn.outerHTML = `
@@ -345,9 +371,7 @@
} }
} catch (err) { } catch (err) {
console.error('[CHAN-FAV-ADMIN] Auto-rehost error:', err); console.error('[CHAN-FAV-ADMIN] Auto-rehost error:', err);
favoBtns.forEach(btn => { setFavoUi(wasAlreadyFav);
btn.className = 'iconset fa-regular fa-heart';
});
if (typeof window.flashMessage === 'function') { if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to favorite post', 3000, 'error'); window.flashMessage('Failed to favorite post', 3000, 'error');
} }
@@ -356,11 +380,12 @@
} }
const ctx = getContext(); const ctx = getContext();
if (!ctx) return; if (!ctx) {
setFavoUi(wasAlreadyFav);
return;
}
const postid = localId ? Number(localId) : ctx.postid; const postid = localId ? Number(localId) : ctx.postid;
const wasAlreadyFav = favoBtns.length > 0 && favoBtns[0].classList.contains('fa-solid');
try { try {
const res = await post('/api/v2/togglefav', { const res = await post('/api/v2/togglefav', {
postid: postid, postid: postid,
@@ -373,10 +398,7 @@
// Authoritative server state takes priority; fallback to inverting previous UI state // Authoritative server state takes priority; fallback to inverting previous UI state
const isNowFav = (res.favorited !== undefined) ? !!res.favorited : !wasAlreadyFav; const isNowFav = (res.favorited !== undefined) ? !!res.favorited : !wasAlreadyFav;
favoBtns.forEach(favoBtn => { setFavoUi(isNowFav);
favoBtn.classList.toggle('fa-solid', isNowFav);
favoBtn.classList.toggle('fa-regular', !isNowFav);
});
const favcontainer = document.querySelector('#favs'); const favcontainer = document.querySelector('#favs');
if (favcontainer) { if (favcontainer) {
@@ -446,12 +468,14 @@
window.flashMessage((window.f0ckI18n && (isNowFav ? window.f0ckI18n.fav_added : window.f0ckI18n.fav_removed)) || (isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES')); window.flashMessage((window.f0ckI18n && (isNowFav ? window.f0ckI18n.fav_added : window.f0ckI18n.fav_removed)) || (isNowFav ? 'ADDED TO FAVORITES' : 'REMOVED FROM FAVORITES'));
if (navigator.vibrate) navigator.vibrate(50); if (navigator.vibrate) navigator.vibrate(50);
} else { } else {
setFavoUi(wasAlreadyFav);
const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.'; const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.';
if (typeof window.flashMessage === 'function') { if (typeof window.flashMessage === 'function') {
window.flashMessage(errMsg, 3000, 'error'); window.flashMessage(errMsg, 3000, 'error');
} }
} }
} catch (err) { } catch (err) {
setFavoUi(wasAlreadyFav);
if (typeof window.flashMessage === 'function') { if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to update favorite.', 3000, 'error'); window.flashMessage('Failed to update favorite.', 3000, 'error');
} }
+423 -62
View File
@@ -3476,7 +3476,15 @@ window.cancelAnimFrame = (function () {
} }
const locEl = document.querySelector('.location'); const locEl = document.querySelector('.location');
if (locEl && !locEl.textContent.includes('#')) { if (locEl && !locEl.textContent.includes('#')) {
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + initialHash; const lastLink = locEl.querySelector('a:last-of-type');
if (lastLink) {
lastLink.textContent = lastLink.textContent.replace(/#.*$/, '') + '#' + initialHash;
if (lastLink.href) {
lastLink.href = lastLink.href.replace(/#.*$/, '') + '#' + initialHash;
}
} else {
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + initialHash;
}
} }
const foundIdx = albumData.findIndex(item => const foundIdx = albumData.findIndex(item =>
String(item.slug || '') === initialHash || String(item.slug || '') === initialHash ||
@@ -3600,7 +3608,15 @@ window.cancelAnimFrame = (function () {
} }
const locEl = document.querySelector('.location'); const locEl = document.querySelector('.location');
if (locEl && subKey) { if (locEl && subKey) {
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + subKey; const lastLink = locEl.querySelector('a:last-of-type');
if (lastLink) {
lastLink.textContent = lastLink.textContent.replace(/#.*$/, '') + '#' + subKey;
if (lastLink.href) {
lastLink.href = lastLink.href.replace(/#.*$/, '') + '#' + subKey;
}
} else {
locEl.textContent = locEl.textContent.replace(/#.*$/, '') + '#' + subKey;
}
} }
// Pause any playing audio or video // Pause any playing audio or video
@@ -11023,6 +11039,50 @@ window.cancelAnimFrame = (function () {
if (_navEl && window.ResizeObserver) { if (_navEl && window.ResizeObserver) {
new ResizeObserver(window.syncNavbarHeight).observe(_navEl); new ResizeObserver(window.syncNavbarHeight).observe(_navEl);
} }
const _existingNavbarLogo = document.getElementById('navbar-logo');
if (_existingNavbarLogo && !_existingNavbarLogo.complete) {
_existingNavbarLogo.addEventListener('load', window.syncNavbarHeight);
}
window.updateBrandImage = (url) => {
const logos = document.querySelectorAll('#navbar-logo');
if (url) {
window.f0ckBrandImages = [url];
if (logos.length > 0) {
logos.forEach(el => {
el.src = url;
el.style.display = '';
el.onload = () => { if (window.syncNavbarHeight) window.syncNavbarHeight(); };
});
} else {
document.querySelectorAll('a.navbar-brand').forEach(brandLink => {
brandLink.querySelectorAll('.f0ck').forEach(s => s.style.display = 'none');
const img = document.createElement('img');
img.id = 'navbar-logo';
img.src = url;
img.alt = window.f0ckDomain || document.title;
img.style.cssText = 'max-height:40px;vertical-align:middle;max-width:180px;width:auto;';
img.onload = () => { if (window.syncNavbarHeight) window.syncNavbarHeight(); };
brandLink.prepend(img);
});
}
} else {
window.f0ckBrandImages = [];
logos.forEach(el => el.remove());
document.querySelectorAll('a.navbar-brand').forEach(brandLink => {
const f0ckSpan = brandLink.querySelector('.f0ck');
if (f0ckSpan) {
f0ckSpan.style.display = '';
} else {
const span = document.createElement('span');
span.className = 'f0ck';
span.textContent = window.f0ckCustomBrandText || window.f0ckDomain || '';
brandLink.appendChild(span);
}
});
}
if (window.syncNavbarHeight) window.syncNavbarHeight();
};
// Sidebar state init — apply persisted hidden/visible state on load // Sidebar state init — apply persisted hidden/visible state on load
window.initSidebarRightToggle = () => { window.initSidebarRightToggle = () => {
@@ -11152,7 +11212,10 @@ window.cancelAnimFrame = (function () {
const itemIdMatch = url.match(/\/([a-zA-Z0-9_-]{11}|\d+)(?:\?.*|#.*)?$/); const itemIdMatch = url.match(/\/([a-zA-Z0-9_-]{11}|\d+)(?:\?.*|#.*)?$/);
if (itemIdMatch) { if (itemIdMatch) {
const itemId = itemIdMatch[1]; const itemId = itemIdMatch[1];
document.querySelectorAll(`a.thumb.has-notif[href$="/${itemId}"], a.thumb.has-notif[data-item-id="${itemId}"]`).forEach(el => { if (window.NotificationSystemInstance) {
window.NotificationSystemInstance.markItemRead(itemId);
}
document.querySelectorAll(`a.thumb.has-notif[href$="/${itemId}"], a.thumb.has-notif[data-item-id="${itemId}"], a.lazy-thumb.has-notif[data-item-id="${itemId}"]`).forEach(el => {
el.classList.remove('has-notif'); el.classList.remove('has-notif');
}); });
} }
@@ -11265,11 +11328,12 @@ window.cancelAnimFrame = (function () {
// Restore scroll position // Restore scroll position
requestAnimationFrame(() => { requestAnimationFrame(() => {
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
const ic = document.querySelector('.index-container'); const ic = document.querySelector('.index-container');
if (ic && cache.scroll !== undefined) { if (ic && cache.scroll !== undefined) {
ic.scrollTo(0, cache.scroll); ic.scrollTo(0, cache.scroll);
} else {
window.scrollTo(0, cache.scroll || 0);
} }
}); });
@@ -11283,8 +11347,9 @@ window.cancelAnimFrame = (function () {
window.updateVisitIndicators(); window.updateVisitIndicators();
window.initLazyLoading(); window.initLazyLoading();
if (window.initSidebarRightToggle) window.initSidebarRightToggle(); if (window.initSidebarRightToggle) window.initSidebarRightToggle();
if (window.syncNavbarHeight) window.syncNavbarHeight(); if (window.NotificationSystemInstance) {
window.NotificationSystemInstance?.pollDebounced?.(); window.NotificationSystemInstance.poll();
}
window._onaraCurrentGridUrl = urlObj.pathname + urlObj.search; window._onaraCurrentGridUrl = urlObj.pathname + urlObj.search;
isNavigating = false; isNavigating = false;
return; // SKIP FETCH return; // SKIP FETCH
@@ -11342,6 +11407,11 @@ window.cancelAnimFrame = (function () {
indexWrapper.className = 'index-layout-wrapper'; indexWrapper.className = 'index-layout-wrapper';
indexWrapper.innerHTML = '<div class="index-container"><div class="posts grid-transition"></div></div>'; indexWrapper.innerHTML = '<div class="index-container"><div class="posts grid-transition"></div></div>';
// Reset window scroll to 0 before appending indexWrapper so :has(.index-container) doesn't lock a scrolled offset
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
main.innerHTML = ''; main.innerHTML = '';
main.appendChild(indexWrapper); main.appendChild(indexWrapper);
@@ -11764,7 +11834,10 @@ window.cancelAnimFrame = (function () {
// SCROLL TO TOP ON EVERY NEW NAVIGATION // SCROLL TO TOP ON EVERY NEW NAVIGATION
if (!options.skipScroll) { if (!options.skipScroll) {
window.scrollTo(0, 0); window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
} }
if (window.syncNavbarHeight) window.syncNavbarHeight();
// Re-init activity feed if applicable // Re-init activity feed if applicable
if (document.getElementById('activity-container')) { if (document.getElementById('activity-container')) {
@@ -11891,15 +11964,20 @@ window.cancelAnimFrame = (function () {
if (options.skipPush && history.state && history.state.scroll !== undefined) { if (options.skipPush && history.state && history.state.scroll !== undefined) {
// Back/Forward Navigation: Restore saved scroll position // Back/Forward Navigation: Restore saved scroll position
requestAnimationFrame(() => { requestAnimationFrame(() => {
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
const ic = document.querySelector('.index-container'); const ic = document.querySelector('.index-container');
if (ic) ic.scrollTo(0, history.state.scroll); if (ic) ic.scrollTo(0, history.state.scroll);
else window.scrollTo(0, history.state.scroll); else window.scrollTo(0, history.state.scroll);
}); });
} else if (replace) { } else if (replace) {
// New Navigation: Reset to top // New Navigation: Reset to top
window.scrollTo(0, 0);
document.documentElement.scrollTop = 0;
document.body.scrollTop = 0;
const ic = document.querySelector('.index-container'); const ic = document.querySelector('.index-container');
if (ic) ic.scrollTo(0, 0); if (ic) ic.scrollTo(0, 0);
else window.scrollTo(0, 0);
} }
// Trigger fade-in // Trigger fade-in
@@ -11981,6 +12059,7 @@ window.cancelAnimFrame = (function () {
// Notify extensions and re-init systems (e.g. CommentSystem) // Notify extensions and re-init systems (e.g. CommentSystem)
document.dispatchEvent(new Event('f0ck:contentLoaded')); document.dispatchEvent(new Event('f0ck:contentLoaded'));
if (window.initSidebarRightToggle) window.initSidebarRightToggle(); if (window.initSidebarRightToggle) window.initSidebarRightToggle();
if (window.syncNavbarHeight) window.syncNavbarHeight();
// Sync has-notif highlights after grid loads — handles PWA where visibilitychange doesn't fire // Sync has-notif highlights after grid loads — handles PWA where visibilitychange doesn't fire
window.NotificationSystemInstance?.pollDebounced?.(); window.NotificationSystemInstance?.pollDebounced?.();
window._onaraCurrentGridUrl = urlObj.pathname + urlObj.search; window._onaraCurrentGridUrl = urlObj.pathname + urlObj.search;
@@ -12237,6 +12316,14 @@ window.cancelAnimFrame = (function () {
return; return;
} }
const itemid = keySegments.pop(); const itemid = keySegments.pop();
if (itemid) {
if (window.NotificationSystemInstance) {
window.NotificationSystemInstance.markItemRead(itemid);
}
document.querySelectorAll(`a.thumb.has-notif[data-item-id="${itemid}"], a.thumb.has-notif[href$="/${itemid}"], a.lazy-thumb.has-notif[data-item-id="${itemid}"]`).forEach(el => {
el.classList.remove('has-notif');
});
}
// Extract context from Target URL first // Extract context from Target URL first
let tag = null, user = null, isFavs = false, mime = null, hall = null, userHall = null, userHallOwner = null, tagger = null; let tag = null, user = null, isFavs = false, mime = null, hall = null, userHall = null, userHallOwner = null, tagger = null;
@@ -12427,6 +12514,11 @@ window.cancelAnimFrame = (function () {
const _ic = main.querySelector('.index-container'); const _ic = main.querySelector('.index-container');
const _ntc = _iw || _ic; const _ntc = _iw || _ic;
if (_ntc && _isIndexPage) { if (_ntc && _isIndexPage) {
if (itemid) {
_ntc.querySelectorAll(`a.thumb.has-notif[data-item-id="${itemid}"], a.thumb.has-notif[href$="/${itemid}"], a.lazy-thumb.has-notif[data-item-id="${itemid}"]`).forEach(el => {
el.classList.remove('has-notif');
});
}
const _ck = window.location.pathname + window.location.search; const _ck = window.location.pathname + window.location.search;
const _pw = document.querySelector('.pagination-wrapper'); const _pw = document.querySelector('.pagination-wrapper');
gridCacheMap.set(_ck, { node: _ntc, scroll: currentScroll, pagination: _pw ? _pw.innerHTML : '' }); gridCacheMap.set(_ck, { node: _ntc, scroll: currentScroll, pagination: _pw ? _pw.innerHTML : '' });
@@ -12707,6 +12799,11 @@ window.cancelAnimFrame = (function () {
const nodeToCache = indexWrapper || indexContainer; const nodeToCache = indexWrapper || indexContainer;
if (nodeToCache && isIndexPage) { if (nodeToCache && isIndexPage) {
if (itemid) {
nodeToCache.querySelectorAll(`a.thumb.has-notif[data-item-id="${itemid}"], a.thumb.has-notif[href$="/${itemid}"], a.lazy-thumb.has-notif[data-item-id="${itemid}"]`).forEach(el => {
el.classList.remove('has-notif');
});
}
const cacheKey = window.location.pathname + window.location.search; const cacheKey = window.location.pathname + window.location.search;
const paginationWrapper = document.querySelector('.pagination-wrapper'); const paginationWrapper = document.querySelector('.pagination-wrapper');
gridCacheMap.set(cacheKey, { gridCacheMap.set(cacheKey, {
@@ -13021,6 +13118,13 @@ window.cancelAnimFrame = (function () {
const thumbnail = target.closest('.posts > a:not(.notif-item)'); const thumbnail = target.closest('.posts > a:not(.notif-item)');
if (thumbnail && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) { if (thumbnail && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
e.preventDefault(); e.preventDefault();
if (thumbnail.classList.contains('has-notif')) {
thumbnail.classList.remove('has-notif');
}
const clickedId = thumbnail.dataset.itemId || thumbnail.getAttribute('data-item-id') || thumbnail.getAttribute('href')?.match(/\/([a-zA-Z0-9_-]{11}|\d+)(?:[?#]|$)/)?.[1];
if (clickedId && window.NotificationSystemInstance) {
window.NotificationSystemInstance.markItemRead(clickedId);
}
if (isOnaraActive()) { if (isOnaraActive()) {
const wasOpen = document.body.classList.contains('onara-modal-open'); const wasOpen = document.body.classList.contains('onara-modal-open');
document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active')); document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active'));
@@ -13459,10 +13563,13 @@ window.cancelAnimFrame = (function () {
const ratingGroup = modal.querySelector('#chan-rehost-rating-group'); const ratingGroup = modal.querySelector('#chan-rehost-rating-group');
let selectedRating = 'untagged'; let selectedRating = 'untagged';
let userSelectedRating = false;
let userEditedTags = false;
ratingGroup.addEventListener('click', (e) => { ratingGroup.addEventListener('click', (e) => {
const pill = e.target.closest('.chan-rehost-pill'); const pill = e.target.closest('.chan-rehost-pill');
if (!pill) return; if (!pill) return;
userSelectedRating = true;
ratingGroup.querySelectorAll('.chan-rehost-pill').forEach(b => b.classList.remove('active')); ratingGroup.querySelectorAll('.chan-rehost-pill').forEach(b => b.classList.remove('active'));
pill.classList.add('active'); pill.classList.add('active');
selectedRating = pill.dataset.rating; selectedRating = pill.dataset.rating;
@@ -13471,19 +13578,23 @@ window.cancelAnimFrame = (function () {
const tagsInput = modal.querySelector('#chan-rehost-tags-input'); const tagsInput = modal.querySelector('#chan-rehost-tags-input');
const commentInput = modal.querySelector('#chan-rehost-comment-input'); const commentInput = modal.querySelector('#chan-rehost-comment-input');
// Pre-fill current rating and tags if available tagsInput.addEventListener('input', () => {
userEditedTags = true;
});
// Pre-fill current rating and tags if available (don't overwrite if user already interacted)
fetch(`/api/v2/scroller/rehost/details/${itemId}`) fetch(`/api/v2/scroller/rehost/details/${itemId}`)
.then(r => r.json()) .then(r => r.json())
.then(data => { .then(data => {
if (data && data.success) { if (data && data.success) {
if (data.rating) { if (data.rating && data.rating !== 'untagged' && !userSelectedRating) {
selectedRating = data.rating; selectedRating = data.rating;
ratingGroup.querySelectorAll('.chan-rehost-pill').forEach(b => { ratingGroup.querySelectorAll('.chan-rehost-pill').forEach(b => {
b.classList.toggle('active', b.dataset.rating === data.rating); b.classList.toggle('active', b.dataset.rating === data.rating);
}); });
} }
if (data.tags && data.tags.length) { if (data.tags && data.tags.length && !userEditedTags && !tagsInput.value) {
tagsInput.value = data.tags.join(', '); tagsInput.value = data.tags.map(t => (typeof t === 'string' ? t : t.tag)).join(', ');
} }
} }
}).catch(() => {}); }).catch(() => {});
@@ -13507,6 +13618,122 @@ window.cancelAnimFrame = (function () {
if (e.target === modal) closeModal(); if (e.target === modal) closeModal();
}; };
const updateDomAfterSave = (resData) => {
try {
const finalRating = resData.rating || selectedRating || 'untagged';
const finalTags = Array.isArray(resData.tags) ? resData.tags : [];
// 1. Update #tags container
const tagsContainer = document.querySelector('#tags');
if (tagsContainer) {
tagsContainer.dataset.itemId = itemId;
tagsContainer.dataset.canManage = 'true';
const ratingBadges = {
sfw: { cls: 'badge-success is-sfw', label: 'SFW' },
nsfw: { cls: 'badge-warning is-nsfw', label: 'NSFW' },
nsfl: { cls: 'badge-danger is-nsfl', label: 'NSFL' },
untagged: { cls: 'badge-untagged is-untagged', label: '?' }
};
const rMeta = ratingBadges[finalRating] || ratingBadges.untagged;
let ratingEl = tagsContainer.querySelector('.rating-tag');
if (!ratingEl) {
ratingEl = document.createElement('span');
tagsContainer.prepend(ratingEl);
}
ratingEl.className = `badge ${rMeta.cls} rating-tag can-cycle`;
ratingEl.dataset.itemId = itemId;
ratingEl.dataset.rating = finalRating;
ratingEl.innerHTML = `<span class="rating-label">${rMeta.label}</span>`;
// Remove previous tag badges and re-render new ones
tagsContainer.querySelectorAll('.tag-badge').forEach(el => el.remove());
finalTags.forEach(t => {
const tagName = typeof t === 'string' ? t : (t.tag || '');
const tagNorm = typeof t === 'string' ? tagName.toLowerCase().replace(/[^a-z0-9_-]/g, '') : (t.normalized || tagName);
const tagBadge = (typeof t === 'object' && t.badge) ? t.badge : 'badge-light';
const tagId = (typeof t === 'object' && t.id) ? t.id : '';
const span = document.createElement('span');
span.className = `badge ${tagBadge} tag-badge`;
if (tagId) span.dataset.tagId = tagId;
span.dataset.tag = tagName;
span.dataset.tagNormalized = tagNorm;
const a = document.createElement('a');
a.href = `/tag/${encodeURIComponent(tagNorm)}`;
a.className = 'tag-name';
a.textContent = tagName;
span.appendChild(a);
tagsContainer.appendChild(span);
});
}
// 2. Update .item-layout-container
const layoutContainer = document.querySelector('.item-layout-container');
if (layoutContainer) {
layoutContainer.dataset.itemId = itemId;
}
// 3. Update .media-object mode
const mediaObj = document.querySelector('.media-object');
if (mediaObj) {
mediaObj.dataset.mode = finalRating;
}
// 4. Update .location breadcrumb
const locEl = document.querySelector('.location');
if (locEl && !locEl.querySelector('.is-rehosted')) {
const sep = document.createElement('span');
sep.className = 'location-sep';
sep.style.cssText = 'opacity: 0.6; margin: 0 4px;';
sep.textContent = '→';
const a = document.createElement('a');
a.href = `/${itemId}`;
a.className = 'location-link is-rehosted';
a.title = `View rehosted post #${itemId} on f0ckm`;
a.style.cssText = 'color: #4ade80; font-weight: 700;';
a.innerHTML = `<i class="fa-solid fa-check"></i> #${itemId}`;
locEl.appendChild(sep);
locEl.appendChild(a);
}
// 5. Update #a_favo
const favBtn = document.querySelector('#a_favo');
if (favBtn) {
favBtn.dataset.itemId = itemId;
favBtn.dataset.localId = itemId;
}
// 6. Update #chan-item-rehost-btn if present
const rehostBtn = document.querySelector('#chan-item-rehost-btn');
if (rehostBtn) {
rehostBtn.outerHTML = `
<span class="chan-rehost-btn-group" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/${itemId}" class="chan-rehost-action-btn rehosted" title="View Post #${itemId}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;">
<i class="fa-solid fa-check"></i> #${itemId}
</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${itemId}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;">
<i class="fa-solid fa-pen"></i>
</button>
</span>
`;
}
// 7. Invalidate caches
if (typeof window.invalidateItemCache === 'function') {
window.invalidateItemCache(itemId);
window.invalidateItemCache(window.location.pathname);
}
} catch (err) {
console.error('[REHOST-MODAL] DOM update error:', err);
}
};
const saveDetails = async (andView = false) => { const saveDetails = async (andView = false) => {
const saveBtn = modal.querySelector('.chan-rehost-modal-save'); const saveBtn = modal.querySelector('.chan-rehost-modal-save');
const saveViewBtn = modal.querySelector('.chan-rehost-modal-save-view'); const saveViewBtn = modal.querySelector('.chan-rehost-modal-save-view');
@@ -13535,12 +13762,13 @@ window.cancelAnimFrame = (function () {
window.showToast('Details saved for #' + itemId + '!'); window.showToast('Details saved for #' + itemId + '!');
} }
closeModal(); closeModal();
updateDomAfterSave(resData);
if (typeof onSave === 'function') { if (typeof onSave === 'function') {
onSave(resData); onSave(resData);
} }
if (andView) { if (andView) {
if (typeof window.loadItemAjax === 'function') { if (typeof window.loadItemAjax === 'function') {
window.loadItemAjax('/' + itemId); window.loadItemAjax('/' + itemId, true, { skipCache: true });
} else { } else {
window.location.href = '/' + itemId; window.location.href = '/' + itemId;
} }
@@ -13602,9 +13830,19 @@ window.cancelAnimFrame = (function () {
if (data.success && data.item_id) { if (data.success && data.item_id) {
btn.classList.remove('loading'); btn.classList.remove('loading');
btn.classList.add('rehosted'); btn.classList.add('rehosted');
const timeEl = document.querySelector('time.timeago');
if (timeEl) {
const nowIso = new Date().toISOString();
timeEl.dataset.iso = nowIso;
const fullDate = typeof window.f0ckFormatDateFull === 'function' ? window.f0ckFormatDateFull(nowIso) : new Date().toLocaleString();
timeEl.setAttribute('tooltip', fullDate);
timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now'));
}
btn.outerHTML = ` btn.outerHTML = `
<span class="chan-rehost-btn-group" style="display:inline-flex;gap:4px;align-items:center;"> <span class="chan-rehost-btn-group" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/${data.item_id}" class="chan-rehost-action-btn rehosted" title="View Post #${data.item_id}" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"> <a href="/${data.item_id}" class="chan-rehost-action-btn rehosted" title="View Post #${data.item_id}" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;">
<i class="fa-solid fa-check"></i> #${data.item_id} <i class="fa-solid fa-check"></i> #${data.item_id}
</a> </a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="${data.item_id}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"> <button type="button" class="chan-rehost-edit-btn" data-item-id="${data.item_id}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;">
@@ -14146,6 +14384,10 @@ window.cancelAnimFrame = (function () {
loadPageAjax(url, true, { skipPush: true }); loadPageAjax(url, true, { skipPush: true });
} else { } else {
document.title = window._onaraReturnTitle || window.f0ckDomain || 'f0ck'; document.title = window._onaraReturnTitle || window.f0ckDomain || 'f0ck';
if (window.updateVisitIndicators) window.updateVisitIndicators();
if (window.NotificationSystemInstance) {
window.NotificationSystemInstance.poll();
}
} }
} }
return; return;
@@ -17723,31 +17965,9 @@ class NotificationSystem {
document.dispatchEvent(new CustomEvent('f0ck:global_chat_presence', { detail: data.data })); document.dispatchEvent(new CustomEvent('f0ck:global_chat_presence', { detail: data.data }));
} else if (data.type === 'brand_image') { } else if (data.type === 'brand_image') {
window.f0ckDebug(`[SSE] Brand image update received:`, data.data?.url); window.f0ckDebug(`[SSE] Brand image update received:`, data.data?.url);
if (data.data?.url) { if (window.updateBrandImage) {
const src = data.data.url; window.updateBrandImage(data.data?.url || null);
// Update the randomizeLogo pool so clicking the brand always uses the current image
window.f0ckBrandImages = [src];
const logos = document.querySelectorAll('#navbar-logo');
if (logos.length > 0) {
logos.forEach(el => { el.src = src; el.style.display = ''; });
} else {
// Logo element doesn't exist yet (no image was set before) — create it
document.querySelectorAll('a.navbar-brand').forEach(brandLink => {
const textNode = Array.from(brandLink.childNodes).find(n => n.nodeType === Node.TEXT_NODE);
const img = document.createElement('img');
img.id = 'navbar-logo';
img.src = src;
img.alt = document.title;
img.style.cssText = 'max-height:40px;vertical-align:middle;max-width:180px;width:auto;';
if (textNode) brandLink.insertBefore(img, textNode);
else brandLink.prepend(img);
});
}
} else {
window.f0ckBrandImages = [];
document.querySelectorAll('#navbar-logo').forEach(el => el.remove());
} }
} }
} catch (err) { } catch (err) {
console.error('SSE data parse error', err); console.error('SSE data parse error', err);
@@ -18053,7 +18273,12 @@ class NotificationSystem {
}; };
document.addEventListener('f0ck:contentLoaded', reapplyOwnBanner); document.addEventListener('f0ck:contentLoaded', reapplyOwnBanner);
window.addEventListener('pageshow', (e) => { if (e.persisted) reapplyOwnBanner(); }); window.addEventListener('pageshow', (e) => {
if (e.persisted) {
reapplyOwnBanner();
this.poll();
}
});
// Poll fresh display_name from DB — used on tab focus to catch missed SSE events // Poll fresh display_name from DB — used on tab focus to catch missed SSE events
this.syncDisplayName = async () => { this.syncDisplayName = async () => {
@@ -18218,6 +18443,12 @@ class NotificationSystem {
// Handle "Mark as Read" // Handle "Mark as Read"
const notifTargetItemId = link.dataset.itemId || link.getAttribute('data-item-id') || link.getAttribute('href')?.match(/\/([a-zA-Z0-9_-]{11}|\d+)(?:[?#]|$)/)?.[1];
const notifTargetItemSlug = link.dataset.itemSlug || link.getAttribute('data-item-slug');
if (notifTargetItemId || notifTargetItemSlug) {
this.markItemRead(notifTargetItemId, notifTargetItemSlug);
}
if (link.dataset.id && link.classList.contains('unread')) { if (link.dataset.id && link.classList.contains('unread')) {
window.f0ckDebug(`[NotificationSystem] Marking ${link.dataset.id} as read...`); window.f0ckDebug(`[NotificationSystem] Marking ${link.dataset.id} as read...`);
// Fire and forget (keepalive ensures it survives navigation) // Fire and forget (keepalive ensures it survives navigation)
@@ -18226,8 +18457,8 @@ class NotificationSystem {
keepalive: true keepalive: true
}).catch(err => console.error("Failed to mark read", err)); }).catch(err => console.error("Failed to mark read", err));
// Decrement badge count // Decrement badge count if not already decremented by markItemRead
if (this.countBadge) { if (!notifTargetItemId && !notifTargetItemSlug && this.countBadge) {
let count = parseInt(this.countBadge.textContent) || 0; let count = parseInt(this.countBadge.textContent) || 0;
if (count > 0) { if (count > 0) {
count--; count--;
@@ -18521,21 +18752,32 @@ class NotificationSystem {
const unreadItemIds = new Set(); const unreadItemIds = new Set();
notifications.forEach(n => { notifications.forEach(n => {
if (!n.is_read && n.item_id) { if (!n.is_read && n.item_id) {
unreadItemIds.add(String(n.item_id)); const idStr = String(n.item_id);
if (currentPath === `/${n.item_id}` || currentPath === `/${n.item_id}/`) return; unreadItemIds.add(idStr);
document.querySelectorAll(`a.thumb[href$="/${n.item_id}"], a.lazy-thumb[href$="/${n.item_id}"]`).forEach(el => { if (n.item_slug) unreadItemIds.add(String(n.item_slug));
if (n.slug) unreadItemIds.add(String(n.slug));
if (currentPath === `/${n.item_id}` || currentPath === `/${n.item_id}/` || (n.item_slug && currentPath === `/${n.item_slug}`)) return;
document.querySelectorAll(`a.thumb[data-item-id="${idStr}"], a.lazy-thumb[data-item-id="${idStr}"], a.thumb[href$="/${idStr}"], a.lazy-thumb[href$="/${idStr}"]`).forEach(el => {
el.classList.add('has-notif'); el.classList.add('has-notif');
}); });
} }
}); });
// Remove .has-notif from any thumb whose item is no longer in the unread set. // Remove .has-notif from any thumb whose item is no longer in the unread set.
document.querySelectorAll('a.thumb.has-notif, a.lazy-thumb.has-notif').forEach(el => { const removeStaleHasNotif = (root = document) => {
const match = el.getAttribute('href')?.match(/\/(\d+)$/); root.querySelectorAll('a.thumb.has-notif, a.lazy-thumb.has-notif').forEach(el => {
if (match && !unreadItemIds.has(match[1])) { const id = el.dataset.itemId || el.getAttribute('data-item-id') || el.getAttribute('href')?.match(/\/([a-zA-Z0-9_-]{11}|\d+)(?:[?#]|$)/)?.[1];
el.classList.remove('has-notif'); if (id && !unreadItemIds.has(String(id))) {
} el.classList.remove('has-notif');
}); }
});
};
removeStaleHasNotif(document);
if (typeof gridCacheMap !== 'undefined' && gridCacheMap) {
gridCacheMap.forEach(cacheEntry => {
if (cacheEntry?.node) removeStaleHasNotif(cacheEntry.node);
});
}
// Render the active tab // Render the active tab
this._renderActiveTab(); this._renderActiveTab();
@@ -18592,7 +18834,7 @@ class NotificationSystem {
const userLabel = isDeleted ? (i18n.notif_moderation || 'Moderation') : (i18n.notif_system || 'System'); const userLabel = isDeleted ? (i18n.notif_moderation || 'Moderation') : (i18n.notif_system || 'System');
const itemLink = `/${itemKey}`; const itemLink = `/${itemKey}`;
return ` return `
<a href="${itemLink}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}"> <a href="${itemLink}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/deleted/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div> <div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/deleted/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div>
<div class="notif-content"> <div class="notif-content">
<div class="notif-user"><strong>${userLabel}</strong></div> <div class="notif-user"><strong>${userLabel}</strong></div>
@@ -18644,7 +18886,7 @@ class NotificationSystem {
let thumbSrc, thumbOnerror; let thumbSrc, thumbOnerror;
if (n.type === 'warning') { if (n.type === 'warning') {
return ` return `
<div class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}"> <div class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div> <div class="notif-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div class="notif-content"> <div class="notif-content">
<div class="notif-user"><strong ${n.username_color ? `style="color: ${n.username_color}"` : ''}>${user}</strong></div> <div class="notif-user"><strong ${n.username_color ? `style="color: ${n.username_color}"` : ''}>${user}</strong></div>
@@ -18662,7 +18904,7 @@ class NotificationSystem {
} }
const thumb = n.item_id ? `<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="${thumbSrc}" alt="thumb" onerror="${thumbOnerror}"></div>` : ''; const thumb = n.item_id ? `<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="${thumbSrc}" alt="thumb" onerror="${thumbOnerror}"></div>` : '';
return ` return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}"> <a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
${thumb} ${thumb}
<div class="notif-content"> <div class="notif-content">
<div class="notif-user"><strong ${n.username_color ? `style="color: ${n.username_color}"` : ''}>${user}</strong></div> <div class="notif-user"><strong ${n.username_color ? `style="color: ${n.username_color}"` : ''}>${user}</strong></div>
@@ -18678,7 +18920,7 @@ class NotificationSystem {
if (n.type === 'approve') { if (n.type === 'approve') {
const link = `/${itemKey}`; const link = `/${itemKey}`;
return ` return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}"> <a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/${n.item_id}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';};}"></div> <div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/${n.item_id}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';};}"></div>
<div class="notif-content"> <div class="notif-content">
<div> <div>
@@ -18693,7 +18935,7 @@ class NotificationSystem {
if (n.type === 'upload_success') { if (n.type === 'upload_success') {
const link = `/${itemKey}`; const link = `/${itemKey}`;
return ` return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}"> <a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none';"></div> <div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none';"></div>
<div class="notif-content"> <div class="notif-content">
<div> <div>
@@ -18712,7 +18954,7 @@ class NotificationSystem {
const urlDisplay = url ? `<div style="font-size: 0.8em; opacity: 0.7; margin-top: 4px; word-break: break-all; max-height: 3.2em; overflow: hidden;">${url}</div>` : ''; const urlDisplay = url ? `<div style="font-size: 0.8em; opacity: 0.7; margin-top: 4px; word-break: break-all; max-height: 3.2em; overflow: hidden;">${url}</div>` : '';
const link = n.item_id ? `/${itemKey}` : '#'; const link = n.item_id ? `/${itemKey}` : '#';
return ` return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'}" data-id="${n.id}"> <a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'}" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-content"> <div class="notif-content">
<div> <div>
<strong>${(window.f0ckI18n && window.f0ckI18n.notif_upload_error) || 'Your background upload failed.'}</strong> <strong>${(window.f0ckI18n && window.f0ckI18n.notif_upload_error) || 'Your background upload failed.'}</strong>
@@ -18730,7 +18972,7 @@ class NotificationSystem {
const label = isDeleted ? ((window.f0ckI18n && window.f0ckI18n.notif_upload_deleted) || 'A moderator deleted your upload') : ((window.f0ckI18n && window.f0ckI18n.notif_upload_denied) || 'Your upload was denied'); const label = isDeleted ? ((window.f0ckI18n && window.f0ckI18n.notif_upload_deleted) || 'A moderator deleted your upload') : ((window.f0ckI18n && window.f0ckI18n.notif_upload_denied) || 'Your upload was denied');
const link = `/notifications#notif-${n.id}`; const link = `/notifications#notif-${n.id}`;
return ` return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-notif-nav="true"> <a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}" data-notif-nav="true">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/deleted/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div> <div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/deleted/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div>
<div class="notif-content"> <div class="notif-content">
<div> <div>
@@ -18746,7 +18988,7 @@ class NotificationSystem {
if (n.type === 'admin_pending') { if (n.type === 'admin_pending') {
const link = '/mod/approve'; const link = '/mod/approve';
return ` return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}"> <a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/pending/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div> <div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/mod/pending/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';}"></div>
<div class="notif-content"> <div class="notif-content">
<div> <div>
@@ -18761,7 +19003,7 @@ class NotificationSystem {
if (n.type === 'report') { if (n.type === 'report') {
const link = '/mod/reports'; const link = '/mod/reports';
return ` return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}"> <a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/${n.item_id}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';};}"></div> <div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/${n.item_id}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/${n.item_id}.webp';this.onerror=function(){this.style.display='none';};}"></div>
<div class="notif-content"> <div class="notif-content">
<div> <div>
@@ -18776,7 +19018,7 @@ class NotificationSystem {
if (n.type === 'warning') { if (n.type === 'warning') {
const link = `/notifications?tab=system#notif-${n.id}`; const link = `/notifications?tab=system#notif-${n.id}`;
return ` return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-notif-nav="true"> <a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}" data-notif-nav="true">
<div class="notif-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div> <div class="notif-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div class="notif-content"> <div class="notif-content">
<div> <div>
@@ -18800,7 +19042,7 @@ class NotificationSystem {
const thumb = n.item_id ? `<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none'"></div>` : ''; const thumb = n.item_id ? `<div class="notif-thumb"${n.item_mode ? ` data-mode="${n.item_mode}"` : ''}><img src="/t/${n.item_id}.webp" alt="thumb" onerror="this.style.display='none'"></div>` : '';
return ` return `
<a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}"> <a href="${link}" class="notif-item ${n.is_read ? '' : 'unread'} notif-with-thumb" data-id="${n.id}" data-item-id="${n.item_id || ''}" data-item-slug="${n.item_slug || n.slug || ''}">
${thumb} ${thumb}
<div class="notif-content"> <div class="notif-content">
<div> <div>
@@ -18812,6 +19054,113 @@ class NotificationSystem {
`; `;
} }
markItemRead(itemId, slug = null) {
if (!itemId && !slug) return;
const strId = itemId ? String(itemId) : null;
const strSlug = slug ? String(slug) : null;
const matchesItem = (n) => {
if (!n) return false;
if (strId && String(n.item_id) === strId) return true;
if (strSlug && (String(n.item_slug) === strSlug || String(n.slug) === strSlug)) return true;
return false;
};
[this._cachedUser, this._cachedSystem].forEach(list => {
if (!Array.isArray(list)) return;
list.forEach(n => {
if (matchesItem(n) && !n.is_read) {
n.is_read = true;
}
});
});
// Remove matching .notif-item elements from dropdown
if (this.list) {
const items = this.list.querySelectorAll('.notif-item');
items.forEach(el => {
const notifItemId = el.dataset.itemId;
const notifItemSlug = el.dataset.itemSlug;
const notifHref = el.getAttribute('href') || '';
const isMatch = (strId && notifItemId === strId) ||
(strSlug && notifItemSlug === strSlug) ||
(strId && (notifHref === `/${strId}` || notifHref.startsWith(`/${strId}#`) || notifHref.startsWith(`/${strId}?`))) ||
(strSlug && (notifHref === `/${strSlug}` || notifHref.startsWith(`/${strSlug}#`) || notifHref.startsWith(`/${strSlug}?`)));
if (isMatch) {
el.remove();
}
});
if (this.list.children.length === 0) {
this.list.innerHTML = `<div class="notif-empty">${window.f0ckI18n?.no_notifications || 'No new notifications'}</div>`;
}
}
// Mark as read in full history page if open
document.querySelectorAll('.notifications-list-full .notif-item.unread').forEach(el => {
const notifItemId = el.dataset.itemId;
const notifItemSlug = el.dataset.itemSlug;
const notifHref = el.getAttribute('href') || '';
const isMatch = (strId && notifItemId === strId) ||
(strSlug && notifItemSlug === strSlug) ||
(strId && (notifHref === `/${strId}` || notifHref.startsWith(`/${strId}#`) || notifHref.startsWith(`/${strId}?`))) ||
(strSlug && (notifHref === `/${strSlug}` || notifHref.startsWith(`/${strSlug}#`) || notifHref.startsWith(`/${strSlug}?`)));
if (isMatch) {
el.classList.remove('unread');
}
});
// Recalculate unread counts
const userUnread = (this._cachedUser || []).filter(n => !n.is_read).length;
const systemUnread = (this._cachedSystem || []).filter(n => !n.is_read).length;
const totalUnread = userUnread + systemUnread;
if (this.countBadge) {
this.countBadge.textContent = totalUnread;
this.countBadge.style.display = totalUnread > 0 ? 'block' : 'none';
}
const userBadge = document.getElementById('notif-tab-badge-user');
const systemBadge = document.getElementById('notif-tab-badge-system');
if (userBadge) {
userBadge.textContent = userUnread;
userBadge.style.display = userUnread > 0 ? '' : 'none';
}
if (systemBadge) {
systemBadge.textContent = systemUnread;
systemBadge.style.display = systemUnread > 0 ? '' : 'none';
}
if (typeof window._scrollerNotifHook === 'function') {
window._scrollerNotifHook(totalUnread);
}
// Remove .has-notif from matching thumbnails in DOM
const removeHasNotif = (root = document) => {
root.querySelectorAll('a.thumb.has-notif, a.lazy-thumb.has-notif').forEach(el => {
const id = el.dataset.itemId || el.getAttribute('data-item-id') || el.getAttribute('href')?.match(/\/([a-zA-Z0-9_-]{11}|\d+)(?:[?#]|$)/)?.[1];
if (id && (id === strId || id === strSlug)) {
el.classList.remove('has-notif');
}
});
};
removeHasNotif(document);
// Also strip from any detached cached grids in gridCacheMap
if (typeof gridCacheMap !== 'undefined' && gridCacheMap) {
gridCacheMap.forEach(cacheEntry => {
if (cacheEntry && cacheEntry.node) {
removeHasNotif(cacheEntry.node);
}
});
}
// Tell server in background (fire-and-forget with keepalive)
const serverId = strId || strSlug;
if (serverId) {
fetch(`/api/notifications/item/${encodeURIComponent(serverId)}/read`, {
method: 'POST',
keepalive: true
}).catch(() => {});
}
}
async markAllRead() { async markAllRead() {
try { try {
@@ -18829,7 +19178,7 @@ class NotificationSystem {
} }
markAllReadUI() { markAllReadUI() {
this.countBadge.style.display = 'none'; if (this.countBadge) this.countBadge.style.display = 'none';
// Clear per-tab badges // Clear per-tab badges
const userBadge = document.getElementById('notif-tab-badge-user'); const userBadge = document.getElementById('notif-tab-badge-user');
const systemBadge = document.getElementById('notif-tab-badge-system'); const systemBadge = document.getElementById('notif-tab-badge-system');
@@ -18843,6 +19192,16 @@ class NotificationSystem {
} }
// Also update History page items if present // Also update History page items if present
document.querySelectorAll('.notifications-list-full .notif-item.unread').forEach(el => el.classList.remove('unread')); document.querySelectorAll('.notifications-list-full .notif-item.unread').forEach(el => el.classList.remove('unread'));
// Remove .has-notif highlights
document.querySelectorAll('a.thumb.has-notif, a.lazy-thumb.has-notif').forEach(el => el.classList.remove('has-notif'));
if (typeof gridCacheMap !== 'undefined' && gridCacheMap) {
gridCacheMap.forEach(cacheEntry => {
if (cacheEntry?.node) cacheEntry.node.querySelectorAll('a.thumb.has-notif, a.lazy-thumb.has-notif').forEach(el => el.classList.remove('has-notif'));
});
}
if (typeof window._scrollerNotifHook === 'function') {
window._scrollerNotifHook(0);
}
} }
handleTagsUpdate(data) { handleTagsUpdate(data) {
@@ -19053,6 +19412,8 @@ class NotificationSystem {
return false; return false;
}); });
favoBtns.forEach(favoBtn => { favoBtns.forEach(favoBtn => {
favoBtn.classList.remove('fa-spinner', 'fa-spin');
favoBtn.classList.add('iconset', 'fa-heart');
favoBtn.classList.toggle('fa-solid', isNowFav); favoBtn.classList.toggle('fa-solid', isNowFav);
favoBtn.classList.toggle('fa-regular', !isNowFav); favoBtn.classList.toggle('fa-regular', !isNowFav);
}); });
+37 -13
View File
@@ -257,17 +257,34 @@
return; return;
} }
const favoBtns = document.querySelectorAll("#a_favo"); const favoBtns = document.querySelectorAll("#a_favo");
if (!favoBtns.length) return;
const firstFavoBtn = favoBtns[0]; const firstFavoBtn = favoBtns[0];
const chanRehostBtn = document.querySelector('#chan-item-rehost-btn'); const chanRehostBtn = document.querySelector('#chan-item-rehost-btn');
const isChan = firstFavoBtn?.dataset?.isChan === 'true' || !!chanRehostBtn; const isChan = firstFavoBtn?.dataset?.isChan === 'true' || !!chanRehostBtn;
const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url; const chanUrl = firstFavoBtn?.dataset?.chanUrl || chanRehostBtn?.dataset?.url;
let localId = firstFavoBtn?.dataset?.localId; let localId = firstFavoBtn?.dataset?.localId;
const wasAlreadyFav = favoBtns[0].classList.contains('fa-solid') && !favoBtns[0].classList.contains('fa-spinner');
const setFavoUi = (isFav) => {
favoBtns.forEach(btn => {
btn.classList.remove('fa-spinner', 'fa-spin');
btn.classList.add('iconset', 'fa-heart');
btn.classList.toggle('fa-solid', isFav);
btn.classList.toggle('fa-regular', !isFav);
});
};
const setFavoLoading = () => {
favoBtns.forEach(btn => {
btn.classList.remove('fa-heart');
btn.classList.add('iconset', 'fa-solid', 'fa-spinner', 'fa-spin');
});
};
// If viewing un-rehosted 4chan post, auto-rehost first // If viewing un-rehosted 4chan post, auto-rehost first
if (isChan && !localId && chanUrl) { if (isChan && !localId && chanUrl) {
favoBtns.forEach(btn => { setFavoLoading();
btn.className = 'iconset fa-solid fa-spinner fa-spin';
});
try { try {
const csrfToken = window.f0ckSession?.csrf_token || ''; const csrfToken = window.f0ckSession?.csrf_token || '';
const rehostResp = await fetch('/api/v2/scroller/rehost', { const rehostResp = await fetch('/api/v2/scroller/rehost', {
@@ -295,6 +312,15 @@
const infoEl = document.querySelector("#a_info"); const infoEl = document.querySelector("#a_info");
if (infoEl) infoEl.dataset.itemId = localId; if (infoEl) infoEl.dataset.itemId = localId;
const timeEl = document.querySelector('time.timeago');
if (timeEl) {
const nowIso = new Date().toISOString();
timeEl.dataset.iso = nowIso;
const fullDate = typeof window.f0ckFormatDateFull === 'function' ? window.f0ckFormatDateFull(nowIso) : new Date().toLocaleString();
timeEl.setAttribute('tooltip', fullDate);
timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now'));
}
if (chanRehostBtn) { if (chanRehostBtn) {
chanRehostBtn.classList.add('rehosted'); chanRehostBtn.classList.add('rehosted');
chanRehostBtn.outerHTML = ` chanRehostBtn.outerHTML = `
@@ -309,9 +335,7 @@
} }
} catch (err) { } catch (err) {
console.error('[CHAN-FAV] Auto-rehost error:', err); console.error('[CHAN-FAV] Auto-rehost error:', err);
favoBtns.forEach(btn => { setFavoUi(wasAlreadyFav);
btn.className = 'iconset fa-regular fa-heart';
});
if (typeof window.flashMessage === 'function') { if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to favorite post', 3000, 'error'); window.flashMessage('Failed to favorite post', 3000, 'error');
} }
@@ -320,11 +344,12 @@
} }
const ctx = getContext(); const ctx = getContext();
if (!ctx) return; if (!ctx) {
setFavoUi(wasAlreadyFav);
return;
}
const postid = localId ? Number(localId) : ctx.postid; const postid = localId ? Number(localId) : ctx.postid;
const wasAlreadyFav = favoBtns.length > 0 && favoBtns[0].classList.contains('fa-solid');
try { try {
const res = await post('/api/v2/togglefav', { const res = await post('/api/v2/togglefav', {
postid: postid, postid: postid,
@@ -337,10 +362,7 @@
// Authoritative server state takes priority; fallback to inverting previous UI state // Authoritative server state takes priority; fallback to inverting previous UI state
const isNowFav = (res.favorited !== undefined) ? !!res.favorited : !wasAlreadyFav; const isNowFav = (res.favorited !== undefined) ? !!res.favorited : !wasAlreadyFav;
favoBtns.forEach(favoBtn => { setFavoUi(isNowFav);
favoBtn.classList.toggle('fa-solid', isNowFav);
favoBtn.classList.toggle('fa-regular', !isNowFav);
});
// span#favs // span#favs
const favcontainer = document.querySelector('#favs'); const favcontainer = document.querySelector('#favs');
@@ -412,12 +434,14 @@
if (navigator.vibrate) navigator.vibrate(50); if (navigator.vibrate) navigator.vibrate(50);
} }
else { else {
setFavoUi(wasAlreadyFav);
const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.'; const errMsg = (res && (res.msg || res.error)) || 'Anonymous favoriting is disabled.';
if (typeof window.flashMessage === 'function') { if (typeof window.flashMessage === 'function') {
window.flashMessage(errMsg, 3000, 'error'); window.flashMessage(errMsg, 3000, 'error');
} }
} }
} catch (err) { } catch (err) {
setFavoUi(wasAlreadyFav);
if (typeof window.flashMessage === 'function') { if (typeof window.flashMessage === 'function') {
window.flashMessage('Failed to update favorite.', 3000, 'error'); window.flashMessage('Failed to update favorite.', 3000, 'error');
} }
+1 -1
View File
@@ -1476,7 +1476,7 @@ const f0cklib = {
]); ]);
console.log(`[GETF0CK_OPT] All queries finished in ${Date.now() - runTimings}ms`); console.log(`[GETF0CK_OPT] All queries finished in ${Date.now() - runTimings}ms`);
const link = lib.genLink({ user, tag, hall: (hall && typeof hall === 'object') ? hall.slug : hall, mime, type: fav ? 'favs' : 'uploads', path: '', strict: false }); const link = lib.genLink({ user, tag, hall: (hall && typeof hall === 'object') ? hall.slug : hall, mime, type: fav ? 'favs' : 'uploads', path: '', strict: strict });
// Override link for title searches — pagination must use the /tag/title:... prefix // Override link for title searches — pagination must use the /tag/title:... prefix
if (isTitleSearch && titleQuery) { if (isTitleSearch && titleQuery) {
link.main = `/tag/title:${encodeURIComponent(titleQuery)}/`; link.main = `/tag/title:${encodeURIComponent(titleQuery)}/`;
+26 -11
View File
@@ -170,13 +170,19 @@ export default (router, tpl) => {
cdnUrls.push(`http://i.4cdn.org/${board}/${p.tim}${ext}`); cdnUrls.push(`http://i.4cdn.org/${board}/${p.tim}${ext}`);
cdnUrls.push(`/api/v2/scroller/external/4chan/${board}/media/${p.tim}${ext}`); cdnUrls.push(`/api/v2/scroller/external/4chan/${board}/media/${p.tim}${ext}`);
}); });
const rehostMap = {};
if (cdnUrls.length > 0) { if (cdnUrls.length > 0) {
try { try {
const rows = await db`SELECT id, src FROM items WHERE src = ANY(${cdnUrls})`; const rows = await db`SELECT id, src, stamp FROM items WHERE src = ANY(${cdnUrls})`;
rows.forEach(r => { rows.forEach(r => {
const info = { id: r.id, stamp: r.stamp };
rehosts[r.src] = r.id; rehosts[r.src] = r.id;
rehostMap[r.src] = info;
const m = r.src.match(/(\d{13,20})/); const m = r.src.match(/(\d{13,20})/);
if (m) rehosts[m[1]] = r.id; if (m) {
rehosts[m[1]] = r.id;
rehostMap[m[1]] = info;
}
}); });
} catch (e) { } catch (e) {
console.error('[CHAN] Rehost check error:', e.message); console.error('[CHAN] Rehost check error:', e.message);
@@ -199,7 +205,10 @@ export default (router, tpl) => {
const isVideo = ['.webm', '.mp4'].includes(ext); const isVideo = ['.webm', '.mp4'].includes(ext);
const isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(ext); const isImage = ['.jpg', '.jpeg', '.png', '.gif', '.webp'].includes(ext);
const externalMediaUrl = p.tim ? `https://i.4cdn.org/${board}/${p.tim}${ext}` : null; const externalMediaUrl = p.tim ? `https://i.4cdn.org/${board}/${p.tim}${ext}` : null;
const localId = (p.tim && rehosts[p.tim]) || (externalMediaUrl && rehosts[externalMediaUrl]) || null; const rehostInfo = (p.tim && rehostMap[p.tim]) || (externalMediaUrl && rehostMap[externalMediaUrl]) || null;
const localId = rehostInfo ? rehostInfo.id : null;
const effectiveTime = (localId && rehostInfo?.stamp) ? Number(rehostInfo.stamp) : p.time;
const isoStr = new Date(effectiveTime * 1000).toISOString();
return { return {
no: p.no, no: p.no,
@@ -208,9 +217,10 @@ export default (router, tpl) => {
com_formatted: formatComment(p.com || ''), com_formatted: formatComment(p.com || ''),
name: p.name || 'Anonymous', name: p.name || 'Anonymous',
trip: p.trip || '', trip: p.trip || '',
time: p.time, time: effectiveTime,
timeago: lib.timeAgo(p.time * 1000, req.lang || 'en'), timeago: lib.timeAgo(effectiveTime * 1000, req.lang || 'en'),
date_str: p.now || new Date(p.time * 1000).toLocaleString(), date_str: (localId && rehostInfo?.stamp) ? new Date(effectiveTime * 1000).toLocaleString() : (p.now || new Date(effectiveTime * 1000).toLocaleString()),
iso_str: isoStr,
has_media: !!(p.tim && p.ext), has_media: !!(p.tim && p.ext),
tim: p.tim, tim: p.tim,
ext: ext, ext: ext,
@@ -240,6 +250,7 @@ export default (router, tpl) => {
first_media_no: firstMediaPost ? firstMediaPost.no : null, first_media_no: firstMediaPost ? firstMediaPost.no : null,
rehosts_count: Object.keys(rehosts).length, rehosts_count: Object.keys(rehosts).length,
session: req.session ? { ...req.session } : false, session: req.session ? { ...req.session } : false,
csrf_token: req.session?.csrf_token || '',
domain: cfg.main.url.domain, domain: cfg.main.url.domain,
tmp: null, tmp: null,
page_meta: { page_meta: {
@@ -306,7 +317,7 @@ export default (router, tpl) => {
const rehosts = {}; const rehosts = {};
if (cdnUrls.length > 0) { if (cdnUrls.length > 0) {
try { try {
const rows = await db`SELECT id, src FROM items WHERE src = ANY(${cdnUrls})`; const rows = await db`SELECT id, src, stamp FROM items WHERE src = ANY(${cdnUrls})`;
rows.forEach(r => { rows.forEach(r => {
rehosts[r.src] = r.id; rehosts[r.src] = r.id;
const m = r.src.match(/(\d{13,20})/); const m = r.src.match(/(\d{13,20})/);
@@ -333,13 +344,15 @@ export default (router, tpl) => {
let localDest = null; let localDest = null;
let localThumb = null; let localThumb = null;
let localTitle = null; let localTitle = null;
let localStamp = null;
let itemFavorites = []; let itemFavorites = [];
let userHasFavorited = false; let userHasFavorited = false;
if (localId) { if (localId) {
try { try {
const localItem = await db`SELECT id, username, dest, title, mime, width, height FROM items WHERE id = ${localId} LIMIT 1`; const localItem = await db`SELECT id, username, dest, title, mime, width, height, stamp FROM items WHERE id = ${localId} LIMIT 1`;
if (localItem.length > 0) { if (localItem.length > 0) {
const li = localItem[0]; const li = localItem[0];
localStamp = li.stamp;
if (li.title) localTitle = li.title; if (li.title) localTitle = li.title;
if (li.dest) { if (li.dest) {
localDest = `/b/${li.dest}`; localDest = `/b/${li.dest}`;
@@ -368,6 +381,7 @@ export default (router, tpl) => {
} }
// Construct item matching f0ckm's standard item view expectations // Construct item matching f0ckm's standard item view expectations
const effectiveStamp = (localId && localStamp) ? Number(localStamp) : targetPost.time;
const item = { const item = {
id: targetPost.no, id: targetPost.no,
slug: null, slug: null,
@@ -379,10 +393,10 @@ export default (router, tpl) => {
height: targetPost.h || null, height: targetPost.h || null,
size: targetPost.fsize ? lib.formatSize(targetPost.fsize) : '0 B', size: targetPost.fsize ? lib.formatSize(targetPost.fsize) : '0 B',
username: targetPost.name || 'Anonymous', username: targetPost.name || 'Anonymous',
stamp: targetPost.time, stamp: effectiveStamp,
timestamp: { timestamp: {
timeago: lib.timeAgo(targetPost.time * 1000, req.lang || 'en'), timeago: lib.timeAgo(effectiveStamp * 1000, req.lang || 'en'),
timefull: new Date(targetPost.time * 1000).toISOString() timefull: new Date(effectiveStamp * 1000).toISOString()
}, },
comment: targetPost.com ? targetPost.com.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, '') : '', comment: targetPost.com ? targetPost.com.replace(/<br\s*\/?>/gi, '\n').replace(/<[^>]+>/g, '') : '',
is_chan: true, is_chan: true,
@@ -466,6 +480,7 @@ export default (router, tpl) => {
link, link,
chan_thread: chanThreadMeta, chan_thread: chanThreadMeta,
session: req.session ? { ...req.session } : false, session: req.session ? { ...req.session } : false,
csrf_token: req.session?.csrf_token || '',
domain: cfg.main.url.domain, domain: cfg.main.url.domain,
tmp: null, tmp: null,
hidePagination: true, hidePagination: true,
+36 -19
View File
@@ -618,7 +618,7 @@ export default (router) => {
return res.reply({ code: 401, body: JSON.stringify({ success: false, msg: 'Unauthorized' }) }); return res.reply({ code: 401, body: JSON.stringify({ success: false, msg: 'Unauthorized' }) });
} }
const { item_id, rating, tags, comment } = req.post || {}; const { item_id, rating, tags, comment } = req.post || req.body || {};
const itemId = parseInt(item_id, 10); const itemId = parseInt(item_id, 10);
if (!itemId || isNaN(itemId)) { if (!itemId || isNaN(itemId)) {
return res.reply({ code: 400, body: JSON.stringify({ success: false, msg: 'Valid item_id is required' }) }); return res.reply({ code: 400, body: JSON.stringify({ success: false, msg: 'Valid item_id is required' }) });
@@ -636,11 +636,12 @@ export default (router) => {
return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: 'Unauthorized' }) }); return res.reply({ code: 403, body: JSON.stringify({ success: false, msg: 'Unauthorized' }) });
} }
const nsflTagRow = await db`SELECT id FROM tags WHERE normalized = 'nsfl' LIMIT 1`;
const nsflId = nsflTagRow.length > 0 ? nsflTagRow[0].id : (cfg.nsfl_tag_id || 11517);
// 1. Update Rating if provided // 1. Update Rating if provided
if (rating !== undefined && rating !== null && rating !== '') { if (rating !== undefined && rating !== null && rating !== '') {
const r = String(rating).toLowerCase().trim(); const r = String(rating).toLowerCase().trim();
const nsflTagRow = await db`SELECT id FROM tags WHERE normalized = 'nsfl' LIMIT 1`;
const nsflId = nsflTagRow.length > 0 ? nsflTagRow[0].id : (cfg.nsfl_tag_id || 11517);
await db` await db`
DELETE FROM tags_assign DELETE FROM tags_assign
@@ -658,22 +659,30 @@ export default (router) => {
// If r === 'untagged', no rating tag is inserted // If r === 'untagged', no rating tag is inserted
} }
// 2. Add Tags if provided // 2. Sync Tags if provided
const rawList = Array.isArray(tags) ? tags : (typeof tags === 'string' ? tags.split(',') : []); if (tags !== undefined && tags !== null) {
const cleanTags = rawList.map(t => t.trim()).filter(t => t.length > 0 && !['sfw', 'nsfw', 'nsfl'].includes(t.toLowerCase())); const rawList = Array.isArray(tags) ? tags : (typeof tags === 'string' ? tags.split(',') : []);
for (const tagName of cleanTags) { const cleanTags = rawList.map(t => t.trim()).filter(t => t.length > 0 && !['sfw', 'nsfw', 'nsfl'].includes(t.toLowerCase()));
let tagRow = await db`SELECT id FROM tags WHERE normalized = slugify(${tagName}) LIMIT 1`;
if (tagRow.length === 0) { // Delete existing non-rating tags for this item by this user (or all non-rating tags if owner)
await db`INSERT INTO tags ${db({ tag: tagName }, 'tag')} ON CONFLICT DO NOTHING`; await db`
tagRow = await db`SELECT id FROM tags WHERE normalized = slugify(${tagName}) LIMIT 1`; DELETE FROM tags_assign
WHERE item_id = ${itemId}
AND tag_id NOT IN (1, 2, ${nsflId})
AND tag_id NOT IN (SELECT id FROM tags WHERE normalized IN ('sfw', 'nsfw', 'nsfl'))
${isOwner ? db`` : db`AND user_id = ${session.id}`}
`;
for (const tagName of cleanTags) {
let tagRow = await db`SELECT id FROM tags WHERE normalized = slugify(${tagName}) LIMIT 1`;
if (tagRow.length === 0) {
await db`INSERT INTO tags ${db({ tag: tagName }, 'tag')} ON CONFLICT DO NOTHING`;
tagRow = await db`SELECT id FROM tags WHERE normalized = slugify(${tagName}) LIMIT 1`;
}
if (tagRow.length) {
await db`INSERT INTO tags_assign ${db({ item_id: itemId, tag_id: tagRow[0].id, user_id: session.id })} ON CONFLICT DO NOTHING`;
}
} }
if (tagRow.length) {
await db`INSERT INTO tags_assign ${db({ item_id: itemId, tag_id: tagRow[0].id, user_id: session.id })} ON CONFLICT DO NOTHING`;
}
}
if (cleanTags.length > 0) {
const freshTags = await lib.getTags(itemId, session);
await db.notify('tags', JSON.stringify({ item_id: itemId, fresh: true, tags: freshTags }));
} }
// 3. Add Comment if provided // 3. Add Comment if provided
@@ -689,7 +698,15 @@ export default (router) => {
} }
const freshTags = await lib.getTags(itemId, session); const freshTags = await lib.getTags(itemId, session);
await db.notify('tags', JSON.stringify({ item_id: itemId, fresh: true, tags: freshTags })).catch(() => {});
const ratingTag = freshTags.find(t => ['sfw', 'nsfw', 'nsfl'].includes(t.normalized)); const ratingTag = freshTags.find(t => ['sfw', 'nsfw', 'nsfl'].includes(t.normalized));
const cleanTagObjects = freshTags.filter(t => !['sfw', 'nsfw', 'nsfl'].includes(t.normalized)).map(t => ({
id: t.id,
tag: t.tag,
normalized: t.normalized,
badge: t.badge
}));
return res.reply({ return res.reply({
headers: { 'Content-Type': 'application/json' }, headers: { 'Content-Type': 'application/json' },
@@ -697,7 +714,7 @@ export default (router) => {
success: true, success: true,
item_id: itemId, item_id: itemId,
rating: ratingTag ? ratingTag.normalized : 'untagged', rating: ratingTag ? ratingTag.normalized : 'untagged',
tags: freshTags.filter(t => !['sfw', 'nsfw', 'nsfl'].includes(t.normalized)).map(t => t.tag) tags: cleanTagObjects
}) })
}); });
} catch (err) { } catch (err) {
+7 -5
View File
@@ -665,17 +665,19 @@ export default (router, tpl) => {
// Used when the user receives a live notification while already viewing that item. // Used when the user receives a live notification while already viewing that item.
// System-type notifications (item_deleted, deny, report, admin_pending) are excluded — // System-type notifications (item_deleted, deny, report, admin_pending) are excluded —
// they require explicit user acknowledgment. // they require explicit user acknowledgment.
router.post(/\/api\/notifications\/item\/(?<itemId>\d+)\/read/, async (req, res) => { router.post(/\/api\/notifications\/item\/(?<itemId>[a-zA-Z0-9_-]{11}|\d+)\/read/, async (req, res) => {
if (!req.session) return res.reply({ code: 401, body: JSON.stringify({ success: false }) }); if (!req.session?.id) return res.reply({ code: 401, body: JSON.stringify({ success: false }) });
const itemId = req.params.itemId; const rawItemId = req.params.itemId;
const numericId = await f0cklib.resolveNumericItemId(rawItemId);
if (!numericId) return res.reply({ code: 404, body: JSON.stringify({ success: false }) });
const SYSTEM_TYPES = ['item_deleted', 'deny', 'admin_pending', 'report', 'warning']; const SYSTEM_TYPES = ['item_deleted', 'deny', 'admin_pending', 'report', 'warning'];
console.log(`[NotificationRoute] Marking comment notifications for item ${itemId} as read for user ${req.session.id}`); console.log(`[NotificationRoute] Marking notifications for item ${numericId} (${rawItemId}) as read for user ${req.session.id}`);
try { try {
await db` await db`
UPDATE notifications UPDATE notifications
SET is_read = true SET is_read = true
WHERE user_id = ${req.session.id} WHERE user_id = ${req.session.id}
AND item_id = ${+itemId} AND item_id = ${numericId}
AND NOT (type = ANY(${SYSTEM_TYPES})) AND NOT (type = ANY(${SYSTEM_TYPES}))
`; `;
return res.reply({ return res.reply({
+1 -1
View File
@@ -144,7 +144,7 @@ export default (router, tpl) => {
}); });
// Main tags page // Main tags page
router.get(/^\/tags$/, async (req, res) => { router.get(/^\/tags?\/?$/, async (req, res) => {
const phrase = cfg.websrv.phrases[~~(Math.random() * cfg.websrv.phrases.length)]; const phrase = cfg.websrv.phrases[~~(Math.random() * cfg.websrv.phrases.length)];
const mode = req.mode ?? 0; const mode = req.mode ?? 0;
const query = req.url.qs || {}; const query = req.url.qs || {};
+6
View File
@@ -268,6 +268,9 @@
} }
preview.src = data.url; preview.src = data.url;
// Immediately update navbar brand logo
if (window.updateBrandImage) window.updateBrandImage(data.url);
// Enable remove button // Enable remove button
removeBtn.disabled = false; removeBtn.disabled = false;
removeBtn.style.cssText = 'background:rgba(220,53,69,0.15);border:1px solid rgba(220,53,69,0.4);color:#dc3545;padding:7px 16px;border-radius:4px;cursor:pointer;font-size:0.82em;font-weight:bold;transition:background 0.2s;'; removeBtn.style.cssText = 'background:rgba(220,53,69,0.15);border:1px solid rgba(220,53,69,0.4);color:#dc3545;padding:7px 16px;border-radius:4px;cursor:pointer;font-size:0.82em;font-weight:bold;transition:background 0.2s;';
@@ -310,6 +313,9 @@
const wrap = document.getElementById('brand-preview-wrap'); const wrap = document.getElementById('brand-preview-wrap');
wrap.innerHTML = '<span id="brand-preview" style="font-size:0.75em;color:#666;font-style:italic;">No image set</span>'; wrap.innerHTML = '<span id="brand-preview" style="font-size:0.75em;color:#666;font-style:italic;">No image set</span>';
// Immediately update navbar brand logo (revert to text)
if (window.updateBrandImage) window.updateBrandImage(null);
// Disable remove button // Disable remove button
removeBtn.disabled = true; removeBtn.disabled = true;
removeBtn.style.cssText = 'background:rgba(220,53,69,0.05);border:1px solid rgba(220,53,69,0.15);color:#884040;padding:7px 16px;border-radius:4px;cursor:not-allowed;font-size:0.82em;font-weight:bold;'; removeBtn.style.cssText = 'background:rgba(220,53,69,0.05);border:1px solid rgba(220,53,69,0.15);color:#884040;padding:7px 16px;border-radius:4px;cursor:not-allowed;font-size:0.82em;font-weight:bold;';
+35 -9
View File
@@ -64,7 +64,7 @@
@if(p.trip) @if(p.trip)
<span class="chan-post-trip">{{ p.trip }}</span> <span class="chan-post-trip">{{ p.trip }}</span>
@endif @endif
<time class="chan-post-time" title="{{ p.date_str }}">{{ p.timeago }}</time> <time class="chan-post-time timeago" data-iso="{{ p.iso_str }}" tooltip="{{ p.iso_str }}" title="{{ p.date_str }}">{{ p.timeago }}</time>
<a href="#p{{ p.no }}" class="chan-post-no">No. {{ p.no }}</a> <a href="#p{{ p.no }}" class="chan-post-no">No. {{ p.no }}</a>
@if(p.local_id) @if(p.local_id)
<a href="/{{ p.local_id }}" target="_blank" class="chan-post-rehosted-pill" title="Already rehosted on f0ckm as Post #{{ p.local_id }}"> <a href="/{{ p.local_id }}" target="_blank" class="chan-post-rehosted-pill" title="Already rehosted on f0ckm as Post #{{ p.local_id }}">
@@ -674,6 +674,20 @@
'<i class="fa-solid fa-check"></i> Rehosted #' + data.item_id + '</a>' + '<i class="fa-solid fa-check"></i> Rehosted #' + data.item_id + '</a>' +
'<button type="button" class="chan-rehost-edit-btn" data-item-id="' + data.item_id + '" title="Edit rating, tags & comment" style="padding:4px 7px;border-radius:4px;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;font-size:0.75rem;">' + '<button type="button" class="chan-rehost-edit-btn" data-item-id="' + data.item_id + '" title="Edit rating, tags & comment" style="padding:4px 7px;border-radius:4px;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;font-size:0.75rem;">' +
'<i class="fa-solid fa-pen"></i></button></span>'; '<i class="fa-solid fa-pen"></i></button></span>';
const postCard = document.getElementById('p' + postNo) || btn.closest('.chan-post-card');
if (postCard) {
const timeEl = postCard.querySelector('.chan-post-time, .timeago');
if (timeEl) {
const nowIso = new Date().toISOString();
timeEl.dataset.iso = nowIso;
const fullDate = typeof window.f0ckFormatDateFull === 'function' ? window.f0ckFormatDateFull(nowIso) : new Date().toLocaleString();
timeEl.setAttribute('tooltip', fullDate);
timeEl.setAttribute('title', fullDate);
timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now'));
}
}
if (typeof window.showToast === 'function') { if (typeof window.showToast === 'function') {
window.showToast(data.repost ? 'Already on site — linked to existing post' : 'Rehosted successfully!'); window.showToast(data.repost ? 'Already on site — linked to existing post' : 'Rehosted successfully!');
} }
@@ -696,6 +710,7 @@
const editBtn = e.target.closest('.chan-rehost-edit-btn'); const editBtn = e.target.closest('.chan-rehost-edit-btn');
if (editBtn) { if (editBtn) {
e.preventDefault(); e.preventDefault();
e.stopImmediatePropagation();
const itemId = editBtn.dataset.itemId; const itemId = editBtn.dataset.itemId;
if (itemId && typeof window.openChanRehostModal === 'function') { if (itemId && typeof window.openChanRehostModal === 'function') {
window.openChanRehostModal({ itemId: Number(itemId), isPlayerView: false }); window.openChanRehostModal({ itemId: Number(itemId), isPlayerView: false });
@@ -762,14 +777,25 @@
} }
} }
// Add thumb badge if missing // Add thumb badge if missing
const postCard = favBtn.closest('.chan-post'); const postCard = document.getElementById('p' + postNo) || favBtn.closest('.chan-post-card');
const thumbLink = postCard?.querySelector('.chan-thumb-link'); if (postCard) {
if (thumbLink && !thumbLink.querySelector('.chan-thumb-rehost-badge')) { const thumbLink = postCard.querySelector('.chan-thumb-link');
const badge = document.createElement('span'); if (thumbLink && !thumbLink.querySelector('.chan-thumb-rehost-badge')) {
badge.className = 'chan-thumb-rehost-badge'; const badge = document.createElement('span');
badge.title = 'Rehosted on f0ckm as Post #' + localId; badge.className = 'chan-thumb-rehost-badge';
badge.innerHTML = '<i class="fa-solid fa-check"></i> #' + localId; badge.title = 'Rehosted on f0ckm as Post #' + localId;
thumbLink.appendChild(badge); badge.innerHTML = '<i class="fa-solid fa-check"></i> #' + localId;
thumbLink.appendChild(badge);
}
const timeEl = postCard.querySelector('.chan-post-time, .timeago');
if (timeEl) {
const nowIso = new Date().toISOString();
timeEl.dataset.iso = nowIso;
const fullDate = typeof window.f0ckFormatDateFull === 'function' ? window.f0ckFormatDateFull(nowIso) : new Date().toLocaleString();
timeEl.setAttribute('tooltip', fullDate);
timeEl.setAttribute('title', fullDate);
timeEl.textContent = (window.f0ckTimeAgo ? window.f0ckTimeAgo(nowIso) : (window.f0ckI18n?.timeago_just_now || 'just now'));
}
} }
} else { } else {
throw new Error(rehostData.msg || 'Rehost failed'); throw new Error(rehostData.msg || 'Rehost failed');
+11 -11
View File
@@ -1,12 +1,12 @@
{{-- LEGACY LAYOUT — 2-column (main + fixed right sidebar) --}} {{-- LEGACY LAYOUT — 2-column (main + fixed right sidebar) --}}
{{-- Used by: members with use_new_layout = false --}} {{-- Used by: members with use_new_layout = false --}}
<div class="item-layout-container" data-item-id="{{ item.id }}" data-requested-subf0ck="{{ item.requested_subf0ck_slug || '' }}"> <div class="item-layout-container" data-item-id="{{ item.local_id || item.id }}" data-requested-subf0ck="{{ item.requested_subf0ck_slug || '' }}">
{{-- MAIN CONTENT: media + navigation + metadata + comments --}} {{-- MAIN CONTENT: media + navigation + metadata + comments --}}
<div class="item-main-content"> <div class="item-main-content">
<div class="_204863"> <div class="_204863">
<div class="location">@if(item.is_chan)<a href="/4/{{ item.external_board }}/catalogue" class="location-link" title="Board /{{ item.external_board }}/ catalogue">/4</a><a href="/4/{{ item.external_board }}/catalogue" class="location-link" title="Board /{{ item.external_board }}/ catalogue">/{{ item.external_board }}</a><a href="/4/{{ item.external_board }}/{{ item.external_tid }}" class="location-link" title="Thread #{{ item.external_tid }} overview">/{{ item.external_tid }}</a><a href="/4/{{ item.external_board }}/{{ item.external_tid }}/{{ item.external_id }}" class="location-link" data-inherit-context="false" title="Post #{{ item.external_id }}">/{{ item.external_id }}</a>@if(item.local_id)<span class="location-sep" style="opacity: 0.6; margin: 0 4px;">→</span><a href="/{{ item.local_id }}" class="location-link is-rehosted" title="View rehosted post #{{ item.local_id }} on f0ckm" style="color: #4ade80; font-weight: 700;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>@endif@else{{ link.mainDisplay || link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}@endif</div> <div class="location">@if(item.is_chan)<a href="/4/{{ item.external_board }}/catalogue" class="location-link" title="Board /{{ item.external_board }}/ catalogue">/4</a><a href="/4/{{ item.external_board }}/catalogue" class="location-link" title="Board /{{ item.external_board }}/ catalogue">/{{ item.external_board }}</a><a href="/4/{{ item.external_board }}/{{ item.external_tid }}" class="location-link" title="Thread #{{ item.external_tid }} overview">/{{ item.external_tid }}</a><a href="/4/{{ item.external_board }}/{{ item.external_tid }}/{{ item.external_id }}" class="location-link" data-inherit-context="false" title="Post #{{ item.external_id }}">/{{ item.external_id }}</a>@if(item.local_id)<span class="location-sep" style="opacity: 0.6; margin: 0 4px;">→</span><a href="/{{ item.local_id }}" class="location-link is-rehosted" title="View rehosted post #{{ item.local_id }} on f0ckm" style="color: #4ade80; font-weight: 700;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>@endif@elseif(tmp && tmp.tag)<a href="/tags" class="location-link" title="All tags">/tag</a><a href="/tag/{{ encodeURIComponent(tmp.tag) }}{{ link.suffix || '' }}" class="location-link" title="Tag: {{ tmp.tag }}">/{{ tmp.tag }}</a><a href="/tag/{{ encodeURIComponent(tmp.tag) }}/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix || '' }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}" class="location-link" title="Post #{{ item.id }}">/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}</a>@else{{ link.mainDisplay || link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}@endif</div>
<div class="gapLeft"></div> <div class="gapLeft"></div>
</div> </div>
@if(enable_item_title) @if(enable_item_title)
@@ -14,7 +14,7 @@
<span class="item_title_text@if(!item.title) no-title@endif"@if(can_manage_item) title="Click to edit"@endif>@if(item.title){!! item.title !!}@elseif(can_manage_item)<em>Add a title</em>@endif</span> <span class="item_title_text@if(!item.title) no-title@endif"@if(can_manage_item) title="Click to edit"@endif>@if(item.title){!! item.title !!}@elseif(can_manage_item)<em>Add a title</em>@endif</span>
@if(can_manage_item) @if(can_manage_item)
<div class="info-title-edit-wrap" style="display:none;"> <div class="info-title-edit-wrap" style="display:none;">
<input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" /> <input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.local_id || item.id }}" />
<button type="button" id="info-title-save" class="info-title-save-btn" title="Save Title"><i class="fa-solid fa-check"></i></button> <button type="button" id="info-title-save" class="info-title-save-btn" title="Save Title"><i class="fa-solid fa-check"></i></button>
</div> </div>
<span id="info-title-status" class="info-title-status" style="display:none"></span> <span id="info-title-status" class="info-title-status" style="display:none"></span>
@@ -129,11 +129,11 @@
@if(item.is_chan) @if(item.is_chan)
@if(item.local_id) @if(item.local_id)
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;"> <span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a> <a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="{{ item.local_id }}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button> <button type="button" class="chan-rehost-edit-btn" data-item-id="{{ item.local_id }}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button>
</span> </span>
@else @else
<button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button> <button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 16px;min-width:100px;box-sizing:border-box;white-space:nowrap;flex-shrink:0;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button>
@endif @endif
@endif @endif
@if(session) @if(session)
@@ -190,9 +190,9 @@
@if(!user_alternative_infobox && item.is_chan) — <span class="badge badge-dark chan-post-badge" style="background: rgba(74, 222, 128, 0.15); border: 1px solid rgba(74, 222, 128, 0.3); display: inline-flex; align-items: center; gap: 6px;"> @if(!user_alternative_infobox && item.is_chan) — <span class="badge badge-dark chan-post-badge" style="background: rgba(74, 222, 128, 0.15); border: 1px solid rgba(74, 222, 128, 0.3); display: inline-flex; align-items: center; gap: 6px;">
<a href="/4/{{ item.external_board }}/{{ item.external_tid }}" style="color: #4ade80; text-decoration: none; font-weight: 600;" title="View thread overview"><i class="fa-solid fa-clover"></i> /{{ item.external_board }}/{{ item.external_tid }}</a> <a href="/4/{{ item.external_board }}/{{ item.external_tid }}" style="color: #4ade80; text-decoration: none; font-weight: 600;" title="View thread overview"><i class="fa-solid fa-clover"></i> /{{ item.external_board }}/{{ item.external_tid }}</a>
@if(item.local_id) @if(item.local_id)
<a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:4px;font-size:0.75rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a> <a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:3px 10px;border-radius:4px;font-size:0.75rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>
@else @else
<button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;gap:4px;padding:2px 6px;border-radius:4px;font-size:0.75rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button> <button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;justify-content:center;gap:5px;padding:3px 12px;min-width:88px;box-sizing:border-box;white-space:nowrap;flex-shrink:0;border-radius:4px;font-size:0.75rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button>
@endif @endif
</span>@endif </span>@endif
@@ -201,17 +201,17 @@
</span>@endif</span> </span>@endif</span>
<span class="badge badge-dark" id="tags" data-item-id="{{ item.id }}" data-can-manage="{{ can_manage_item ? 'true' : 'false' }}"> <span class="badge badge-dark" id="tags" data-item-id="{{ item.local_id || item.id }}" data-can-manage="{{ can_manage_item ? 'true' : 'false' }}">
<span class="tags-inner"> <span class="tags-inner">
@if(!item.is_sfw && !item.is_nsfw && !item.is_nsfl) @if(!item.is_sfw && !item.is_nsfw && !item.is_nsfl)
<span class="badge badge-untagged rating-tag is-untagged @if(can_manage_item) can-cycle @endif" data-item-id="{{ item.id }}" data-rating="untagged"> <span class="badge badge-untagged rating-tag is-untagged @if(can_manage_item) can-cycle @endif" data-item-id="{{ item.local_id || item.id }}" data-rating="untagged">
<span class="rating-label">?</span> <span class="rating-label">?</span>
</span> </span>
@endif @endif
@if(typeof item.tags !== "undefined") @if(typeof item.tags !== "undefined")
@each(item.tags as tag) @each(item.tags as tag)
@if(tag.normalized === 'sfw' || tag.normalized === 'nsfw' || tag.normalized === 'nsfl') @if(tag.normalized === 'sfw' || tag.normalized === 'nsfw' || tag.normalized === 'nsfl')
<span class="badge {{ tag.badge }} rating-tag is-{{ tag.normalized }} @if(can_manage_item) can-cycle @endif" data-item-id="{{ item.id }}" data-rating="{{ tag.normalized }}"> <span class="badge {{ tag.badge }} rating-tag is-{{ tag.normalized }} @if(can_manage_item) can-cycle @endif" data-item-id="{{ item.local_id || item.id }}" data-rating="{{ tag.normalized }}">
<span class="rating-label">{!! tag.tag !!}</span> <span class="rating-label">{!! tag.tag !!}</span>
</span> </span>
@else @else
@@ -256,7 +256,7 @@
@if(enable_comments && (session || !hide_comments_from_public)) @if(enable_comments && (session || !hide_comments_from_public))
<div id="comments-container" <div id="comments-container"
data-item-id="{{ item.id }}" data-item-id="{{ item.local_id || item.id }}"
@if(session) data-user="{{ session.user }}" @endif @if(session) data-user="{{ session.user }}" @endif
@if(session && (session.admin || session.is_moderator)) data-is-admin="true" @endif @if(session && (session.admin || session.is_moderator)) data-is-admin="true" @endif
@if(item.is_comments_locked) data-is-locked="true" @endif> @if(item.is_comments_locked) data-is-locked="true" @endif>
+9 -9
View File
@@ -1,6 +1,6 @@
{{-- MODERN LAYOUT — 3-column grid --}} {{-- MODERN LAYOUT — 3-column grid --}}
{{-- Used by: guests (no session) + members with use_new_layout = true --}} {{-- Used by: guests (no session) + members with use_new_layout = true --}}
<div class="item-layout-container" data-item-id="{{ item.id }}" data-requested-subf0ck="{{ item.requested_subf0ck_slug || '' }}"> <div class="item-layout-container" data-item-id="{{ item.local_id || item.id }}" data-requested-subf0ck="{{ item.requested_subf0ck_slug || '' }}">
{{-- LEFT SIDEBAR: comments + tags --}} {{-- LEFT SIDEBAR: comments + tags --}}
<div class="item-sidebar-left"> <div class="item-sidebar-left">
@@ -15,7 +15,7 @@
@if(enable_comments && (session || !hide_comments_from_public)) @if(enable_comments && (session || !hide_comments_from_public))
<div id="comments-container" <div id="comments-container"
data-item-id="{{ item.id }}" data-item-id="{{ item.local_id || item.id }}"
@if(session) data-user="{{ session.user }}" @endif @if(session) data-user="{{ session.user }}" @endif
@if(session && is_mod_or_admin) data-is-admin="true" @endif @if(session && is_mod_or_admin) data-is-admin="true" @endif
@if(item.is_comments_locked) data-is-locked="true" @endif> @if(item.is_comments_locked) data-is-locked="true" @endif>
@@ -40,16 +40,16 @@
@endif @endif
<div class="sidebar-tags-container"> <div class="sidebar-tags-container">
<span class="badge badge-dark" id="tags" data-item-id="{{ item.id }}" data-can-manage="{{ can_manage_item ? 'true' : 'false' }}" style="display: flex; flex-wrap: wrap; gap: 5px; background: transparent; padding: 0; text-align: left; white-space: normal;"> <span class="badge badge-dark" id="tags" data-item-id="{{ item.local_id || item.id }}" data-can-manage="{{ can_manage_item ? 'true' : 'false' }}" style="display: flex; flex-wrap: wrap; gap: 5px; background: transparent; padding: 0; text-align: left; white-space: normal;">
@if(!item.is_sfw && !item.is_nsfw && !item.is_nsfl) @if(!item.is_sfw && !item.is_nsfw && !item.is_nsfl)
<span class="badge badge-untagged rating-tag is-untagged @if(can_manage_item) can-cycle @endif" data-item-id="{{ item.id }}" data-rating="untagged"> <span class="badge badge-untagged rating-tag is-untagged @if(can_manage_item) can-cycle @endif" data-item-id="{{ item.local_id || item.id }}" data-rating="untagged">
<span class="rating-label">?</span> <span class="rating-label">?</span>
</span> </span>
@endif @endif
@if(typeof item.tags !== "undefined") @if(typeof item.tags !== "undefined")
@each(item.tags as tag) @each(item.tags as tag)
@if(tag.normalized === 'sfw' || tag.normalized === 'nsfw' || tag.normalized === 'nsfl') @if(tag.normalized === 'sfw' || tag.normalized === 'nsfw' || tag.normalized === 'nsfl')
<span class="badge {{ tag.badge }} rating-tag is-{{ tag.normalized }} @if(can_manage_item) can-cycle @endif" data-item-id="{{ item.id }}" data-rating="{{ tag.normalized }}"> <span class="badge {{ tag.badge }} rating-tag is-{{ tag.normalized }} @if(can_manage_item) can-cycle @endif" data-item-id="{{ item.local_id || item.id }}" data-rating="{{ tag.normalized }}">
<span class="rating-label">{!! tag.tag !!}</span> <span class="rating-label">{!! tag.tag !!}</span>
</span> </span>
@else @else
@@ -70,7 +70,7 @@
<div class="item-main-content"> <div class="item-main-content">
<div class="_204863"> <div class="_204863">
<div class="location">@if(item.is_chan)<a href="/4/{{ item.external_board }}/catalogue" class="location-link" title="Board /{{ item.external_board }}/ catalogue">/4</a><a href="/4/{{ item.external_board }}/catalogue" class="location-link" title="Board /{{ item.external_board }}/ catalogue">/{{ item.external_board }}</a><a href="/4/{{ item.external_board }}/{{ item.external_tid }}" class="location-link" title="Thread #{{ item.external_tid }} overview">/{{ item.external_tid }}</a><a href="/4/{{ item.external_board }}/{{ item.external_tid }}/{{ item.external_id }}" class="location-link" data-inherit-context="false" title="Post #{{ item.external_id }}">/{{ item.external_id }}</a>@if(item.local_id)<span class="location-sep" style="opacity: 0.6; margin: 0 4px;">→</span><a href="/{{ item.local_id }}" class="location-link is-rehosted" title="View rehosted post #{{ item.local_id }} on f0ckm" style="color: #4ade80; font-weight: 700;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>@endif@else{{ link.mainDisplay || link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}@endif</div> <div class="location">@if(item.is_chan)<a href="/4/{{ item.external_board }}/catalogue" class="location-link" title="Board /{{ item.external_board }}/ catalogue">/4</a><a href="/4/{{ item.external_board }}/catalogue" class="location-link" title="Board /{{ item.external_board }}/ catalogue">/{{ item.external_board }}</a><a href="/4/{{ item.external_board }}/{{ item.external_tid }}" class="location-link" title="Thread #{{ item.external_tid }} overview">/{{ item.external_tid }}</a><a href="/4/{{ item.external_board }}/{{ item.external_tid }}/{{ item.external_id }}" class="location-link" data-inherit-context="false" title="Post #{{ item.external_id }}">/{{ item.external_id }}</a>@if(item.local_id)<span class="location-sep" style="opacity: 0.6; margin: 0 4px;">→</span><a href="/{{ item.local_id }}" class="location-link is-rehosted" title="View rehosted post #{{ item.local_id }} on f0ckm" style="color: #4ade80; font-weight: 700;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>@endif@elseif(tmp && tmp.tag)<a href="/tags" class="location-link" title="All tags">/tag</a><a href="/tag/{{ encodeURIComponent(tmp.tag) }}{{ link.suffix || '' }}" class="location-link" title="Tag: {{ tmp.tag }}">/{{ tmp.tag }}</a><a href="/tag/{{ encodeURIComponent(tmp.tag) }}/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix || '' }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}" class="location-link" title="Post #{{ item.id }}">/{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}</a>@else{{ link.mainDisplay || link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}{{ link.suffix }}{{ item.requested_subf0ck_slug ? ('#' + item.requested_subf0ck_slug) : '' }}@endif</div>
<div class="gapLeft"></div> <div class="gapLeft"></div>
</div> </div>
@if(enable_item_title) @if(enable_item_title)
@@ -78,7 +78,7 @@
<span class="item_title_text@if(!item.title) no-title@endif"@if(can_manage_item) title="Click to edit"@endif>@if(item.title){!! item.title !!}@elseif(can_manage_item)<em>Add a title</em>@endif</span> <span class="item_title_text@if(!item.title) no-title@endif"@if(can_manage_item) title="Click to edit"@endif>@if(item.title){!! item.title !!}@elseif(can_manage_item)<em>Add a title</em>@endif</span>
@if(can_manage_item) @if(can_manage_item)
<div class="info-title-edit-wrap" style="display:none;"> <div class="info-title-edit-wrap" style="display:none;">
<input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.id }}" /> <input type="text" id="info-title-input" class="info-title-input" value="{!! item.title || '' !!}" placeholder="Add a title…" maxlength="500" data-item-id="{{ item.local_id || item.id }}" />
<button type="button" id="info-title-save" class="info-title-save-btn" title="Save Title"><i class="fa-solid fa-check"></i></button> <button type="button" id="info-title-save" class="info-title-save-btn" title="Save Title"><i class="fa-solid fa-check"></i></button>
</div> </div>
<span id="info-title-status" class="info-title-status" style="display:none"></span> <span id="info-title-status" class="info-title-status" style="display:none"></span>
@@ -161,11 +161,11 @@
@if(item.is_chan) @if(item.is_chan)
@if(item.local_id) @if(item.local_id)
<span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;"> <span class="chan-rehosted-wrap" style="display:inline-flex;gap:4px;align-items:center;">
<a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a> <a href="/{{ item.local_id }}" class="chan-rehost-action-btn rehosted" title="Already rehosted on f0ckm (Post #{{ item.local_id }})" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 12px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.2);color:#4ade80;border:1px solid rgba(74,222,128,0.4);text-decoration:none;font-weight:600;white-space:nowrap;flex-shrink:0;"><i class="fa-solid fa-check"></i> #{{ item.local_id }}</a>
<button type="button" class="chan-rehost-edit-btn" data-item-id="{{ item.local_id }}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button> <button type="button" class="chan-rehost-edit-btn" data-item-id="{{ item.local_id }}" title="Edit rating, tags & comment" style="display:inline-flex;align-items:center;padding:3px 7px;border-radius:6px;font-size:0.8rem;background:rgba(255,255,255,0.1);color:#fff;border:1px solid rgba(255,255,255,0.2);cursor:pointer;"><i class="fa-solid fa-pen"></i></button>
</span> </span>
@else @else
<button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;gap:5px;padding:3px 9px;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button> <button type="button" class="chan-rehost-action-btn" id="chan-item-rehost-btn" data-url="{{ item.external_media_url }}" data-board="{{ item.external_board }}" data-tid="{{ item.external_tid }}" data-post="{{ item.external_id }}" data-filename="{{ item.original_filename || '' }}" data-width="{{ item.width || '' }}" data-height="{{ item.height || '' }}" title="Rehost to f0ckm" style="display:inline-flex;align-items:center;justify-content:center;gap:6px;padding:4px 16px;min-width:100px;box-sizing:border-box;white-space:nowrap;flex-shrink:0;border-radius:6px;font-size:0.8rem;background:rgba(74,222,128,0.15);color:#4ade80;border:1px solid rgba(74,222,128,0.3);cursor:pointer;font-weight:600;"><i class="fa-solid fa-cloud-arrow-up"></i> Rehost</button>
@endif @endif
@endif @endif
@if(session) @if(session)
+1 -1
View File
@@ -506,7 +506,7 @@
anon_anonymize: @if(anon_anonymize) true @else false @endif, anon_anonymize: @if(anon_anonymize) true @else false @endif,
is_anonymized: @if(is_anonymized) true @else false @endif, is_anonymized: @if(is_anonymized) 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 || (session && session.csrf_token) || '' }}",
default_theme: "{{ default_theme }}", default_theme: "{{ default_theme }}",
private_society: @if(private_society) true @else false @endif, private_society: @if(private_society) true @else false @endif,
show_content_warning: @if(show_content_warning) true @else false @endif, show_content_warning: @if(show_content_warning) true @else false @endif,
+1 -1
View File
@@ -67,7 +67,7 @@
@endif @endif
<link rel="stylesheet" href="/s/css/upload.css?v={{ ts }}"> <link rel="stylesheet" href="/s/css/upload.css?v={{ ts }}">
@endif @endif
<script>window.f0ckThemes = {{ themes_json }}; window.f0ckDefaultTheme = "{{ default_theme }}"; window.f0ckDefaultLayout = "{{ default_layout }}"; window.f0ckDomain = "{{ domain }}"; window.f0ckGitHash = "{{ git_hash }}"; window.f0ckAllowedImages = {{ allowed_comment_images_json }}; window.f0ckEmbedYoutubeInComments = {{ embed_youtube_in_comments ? 'true' : 'false' }}; window.f0ckEnableYoutubeUpload = {{ enable_youtube_upload ? 'true' : 'false' }}; window.f0ckBrandImages = {{ custom_brand_images_json }}; window.f0ckMediaBase = "{{ paths_images }}"; window.f0ckShitpostMode = {{ shitpost_mode ? 'true' : 'false' }}; window.f0ckShitpostRequireRating = {{ shitpost_require_rating ? 'true' : 'false' }}; window.f0ckShitpostMinTags = {{ shitpost_min_tags || 0 }}; window.f0ckEnableItemTitle = {{ enable_item_title ? 'true' : 'false' }}; window.f0ckCommentBannerEnabled = @if(comment_banner_enabled) true @else false @endif; window.f0ckServerAudioTuner = {{ audio_tuner_json || 'null' }}; window.f0ckEnableComments = {{ enable_comments ? 'true' : 'false' }};</script> <script>window.f0ckThemes = {{ themes_json }}; window.f0ckDefaultTheme = "{{ default_theme }}"; window.f0ckDefaultLayout = "{{ default_layout }}"; window.f0ckDomain = "{{ domain }}"; window.f0ckGitHash = "{{ git_hash }}"; window.f0ckAllowedImages = {{ allowed_comment_images_json }}; window.f0ckEmbedYoutubeInComments = {{ embed_youtube_in_comments ? 'true' : 'false' }}; window.f0ckEnableYoutubeUpload = {{ enable_youtube_upload ? 'true' : 'false' }}; window.f0ckBrandImages = {{ custom_brand_images_json }}; window.f0ckCustomBrandText = "{{ custom_navbar_brand_text || '' }}"; window.f0ckMediaBase = "{{ paths_images }}"; window.f0ckShitpostMode = {{ shitpost_mode ? 'true' : 'false' }}; window.f0ckShitpostRequireRating = {{ shitpost_require_rating ? 'true' : 'false' }}; window.f0ckShitpostMinTags = {{ shitpost_min_tags || 0 }}; window.f0ckEnableItemTitle = {{ enable_item_title ? 'true' : 'false' }}; window.f0ckCommentBannerEnabled = @if(comment_banner_enabled) true @else false @endif; window.f0ckServerAudioTuner = {{ audio_tuner_json || 'null' }}; window.f0ckEnableComments = {{ enable_comments ? 'true' : 'false' }};</script>
@if(!private_society || session) @if(!private_society || session)
@if(enable_comments) @if(enable_comments)
<script src="/s/js/marked.min.js" defer></script> <script src="/s/js/marked.min.js" defer></script>
+2 -2
View File
@@ -5,7 +5,7 @@
<!-- Brand: always visible, flush left --> <!-- Brand: always visible, flush left -->
<a class="navbar-brand" href="/"> <a class="navbar-brand" href="/">
@if(custom_brand_image) @if(custom_brand_image)
<img id="navbar-logo" src="{{ custom_brand_image }}" alt="{{ domain }}" style="max-height: 40px; vertical-align: middle; max-width: 180px; width: auto;"> <img id="navbar-logo" src="{{ custom_brand_image }}" alt="{{ domain }}" onload="if(window.syncNavbarHeight)window.syncNavbarHeight();" style="max-height: 40px; vertical-align: middle; max-width: 180px; width: auto;">
@else @else
<span class="f0ck">{{ custom_navbar_brand_text || domain }}</span> <span class="f0ck">{{ custom_navbar_brand_text || domain }}</span>
@endif @endif
@@ -249,7 +249,7 @@
<a class="navbar-brand" href="/"> <a class="navbar-brand" href="/">
@if(custom_brand_image) @if(custom_brand_image)
<img id="navbar-logo" src="{{ custom_brand_image }}" alt="{{ domain }}" style="max-height: 40px; vertical-align: middle; max-width: 180px; width: auto;"> <img id="navbar-logo" src="{{ custom_brand_image }}" alt="{{ domain }}" onload="if(window.syncNavbarHeight)window.syncNavbarHeight();" style="max-height: 40px; vertical-align: middle; max-width: 180px; width: auto;">
@else @else
<span class="f0ck">{{ custom_navbar_brand_text || domain }}</span> <span class="f0ck">{{ custom_navbar_brand_text || domain }}</span>
@endif @endif
+10 -10
View File
@@ -1,6 +1,6 @@
@each(notifications as n) @each(notifications as n)
@if(n.type === 'approve') @if(n.type === 'approve')
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}"> <a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}"> <div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};};"/> <img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};};"/>
</div> </div>
@@ -11,7 +11,7 @@
</div> </div>
</a> </a>
@elseif(n.type === 'admin_pending') @elseif(n.type === 'admin_pending')
<a href="/mod/approve" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}"> <a href="/mod/approve" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}"> <div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};"/> <img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};"/>
</div> </div>
@@ -22,7 +22,7 @@
</div> </div>
</a> </a>
@elseif(n.type === 'report') @elseif(n.type === 'report')
<a href="/mod/reports" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}"> <a href="/mod/reports" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}"> <div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};};"/> <img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.onerror=null;this.src='/mod/deleted/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none';};};"/>
</div> </div>
@@ -33,7 +33,7 @@
</div> </div>
</a> </a>
@elseif(n.type === 'deny') @elseif(n.type === 'deny')
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}"> <a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}"> <div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" /> <img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
</div> </div>
@@ -47,7 +47,7 @@
</div> </div>
</a> </a>
@elseif(n.type === 'item_deleted') @elseif(n.type === 'item_deleted')
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}"> <a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}"> <div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" /> <img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
</div> </div>
@@ -61,7 +61,7 @@
</div> </div>
</a> </a>
@elseif(n.type === 'upload_comment') @elseif(n.type === 'upload_comment')
<a href="/{{ n.item_slug || n.item_id }}#c{{ n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}"> <a href="/{{ n.item_slug || n.item_id }}#c{{ n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}"> <div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumbnail" onerror="this.style.display='none'"> <img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumbnail" onerror="this.style.display='none'">
</div> </div>
@@ -72,7 +72,7 @@
</div> </div>
</a> </a>
@elseif(n.type === 'upload_success') @elseif(n.type === 'upload_success')
<a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}"> <a href="/{{ n.item_slug || n.item_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}"> <div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.style.display='none'" /> <img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.style.display='none'" />
</div> </div>
@@ -83,7 +83,7 @@
</div> </div>
</a> </a>
@elseif(n.type === 'upload_error') @elseif(n.type === 'upload_error')
<a href="{{ n.item_id ? '/' + (n.item_slug || n.item_id) : '#' }}" class="notif-item {{ n.is_read ? '' : 'unread' }} {{ n.item_id ? 'notif-with-thumb' : '' }}" data-id="{{ n.id }}"> <a href="{{ n.item_id ? '/' + (n.item_slug || n.item_id) : '#' }}" class="notif-item {{ n.is_read ? '' : 'unread' }} {{ n.item_id ? 'notif-with-thumb' : '' }}" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
@if(n.item_id) @if(n.item_id)
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}"> <div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" /> <img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.onerror=null;this.src='/mod/pending/t/{{ n.item_id }}.webp';this.onerror=function(){this.style.display='none'};" />
@@ -104,7 +104,7 @@
</div> </div>
</a> </a>
@elseif(n.type === 'warning') @elseif(n.type === 'warning')
<div class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}"> <div class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
<div class="notif-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div> <div class="notif-thumb" style="display:flex;align-items:center;justify-content:center;background:var(--bg-lighter);color:var(--danger);font-size:1.5em;"><i class="fa-solid fa-triangle-exclamation"></i></div>
<div class="notif-content"> <div class="notif-content">
<div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div> <div class="notif-user"><strong>{{ t('notifications.system') }}</strong></div>
@@ -116,7 +116,7 @@
</div> </div>
</div> </div>
@else @else
<a href="/{{ n.item_slug || n.item_id }}#c{{ n.comment_id || n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}"> <a href="/{{ n.item_slug || n.item_id }}#c{{ n.comment_id || n.reference_id }}" class="notif-item {{ n.is_read ? '' : 'unread' }} notif-with-thumb" data-id="{{ n.id }}" data-item-id="{{ n.item_id || '' }}" data-item-slug="{{ n.item_slug || '' }}">
@if(n.item_id) @if(n.item_id)
<div class="notif-thumb" data-mode="{{ n.item_mode || '' }}"> <div class="notif-thumb" data-mode="{{ n.item_mode || '' }}">
<img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.style.display='none'" /> <img src="/t/{{ n.item_id }}{{ ((active_mode===0&&(n.item_mode==='nsfw'||n.item_mode==='nsfl'))||(active_mode===1&&n.item_mode==='nsfl')||(active_mode===4&&(n.item_mode==='sfw'||n.item_mode==='nsfw'))) ? '_blur' : '' }}.webp" data-orig-src="/t/{{ n.item_id }}.webp" alt="thumb" onerror="this.style.display='none'" />