This commit is contained in:
2026-09-10 00:07:22 +02:00
parent 43ff87ed62
commit 73ffe02bbb
12 changed files with 1327 additions and 275 deletions
+300 -22
View File
@@ -9111,7 +9111,7 @@ input#s_avatar {
height: 100%;
background: rgba(0, 0, 0, 0.85);
backdrop-filter: blur(5px);
z-index: 10001;
z-index: 100100;
display: flex;
align-items: center;
justify-content: center;
@@ -11765,7 +11765,7 @@ input:checked+.slider:before {
display: flex;
align-items: center;
justify-content: center;
z-index: 10000;
z-index: 100100;
}
body.modal-open {
@@ -12941,7 +12941,7 @@ body.layout-modern>.pagewrapper:not(:has(.index-layout-wrapper)):not(:has(.item-
left: 0;
width: 100vw;
height: 100vh;
z-index: 10000;
z-index: 100100;
overflow: auto;
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
@@ -14866,29 +14866,16 @@ span.gchat-post-card--loading {
}
}
body.layout-modern .gapRight {
justify-content: center;
}
body.layout-modern .gapRight>svg.iconset,
body.layout-modern .gapRight>i.iconset {
height: 30px;
width: 30px;
margin: 5px;
padding: 5px;
right: 0px;
background: var(--badge-tag);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 18px;
box-sizing: border-box;
}
.gapRight,
body.layout-modern .gapRight,
body.layout-legacy .gapRight {
justify-content: center;
}
.gapRight>svg.iconset,
.gapRight>i.iconset,
body.layout-modern .gapRight>svg.iconset,
body.layout-modern .gapRight>i.iconset,
body.layout-legacy .gapRight>svg.iconset,
body.layout-legacy .gapRight>i.iconset {
height: 30px;
@@ -14904,6 +14891,7 @@ body.layout-legacy .gapRight>i.iconset {
box-sizing: border-box;
}
.gapRight>.iconset#a_favo,
body.layout-legacy .gapRight>.iconset#a_favo {
fill: #ff009b;
}
@@ -18613,4 +18601,294 @@ a.lazy-thumb.filtered-upload-ghost:hover::after {
.item-main-content {
padding-left: 15px;
padding-right: 15px;
}
/* ==========================================================================
ONARA ALTERNATIVE FLAVOR: Fullscreen See-Through Modal Wrapper
========================================================================== */
/* Prevent scrolling on background document when Onara modal is active */
body.onara-modal-open {
overflow: hidden !important;
}
/* Background main page elements (outside posts) are dimmed and softly blurred */
body.onara-modal-open .pagewrapper {
pointer-events: none !important;
user-select: none !important;
}
body.onara-modal-open .pagewrapper > *:not(.index-layout-wrapper):not(#main),
body.onara-modal-open .index-container > *:not(.posts),
body.onara-modal-open #footbar,
body.onara-modal-open .pagination-container-fluid {
opacity: 0.25 !important;
filter: blur(1.5px) !important;
transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1),
filter 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important;
}
/* All non-active background thumbnails are strictly dimmed, blurred, with no outlines or badges */
body.onara-modal-open .posts > a.thumb:not(.onara-active) {
opacity: 0.25 !important;
filter: blur(1.5px) !important;
box-shadow: none !important;
outline: none !important;
transition: opacity 0.25s cubic-bezier(0.16, 1, 0.3, 1),
filter 0.25s cubic-bezier(0.16, 1, 0.3, 1) !important;
}
body.onara-modal-open .posts > a.thumb:not(.onara-active)::after {
visibility: hidden !important;
opacity: 0 !important;
}
/* ONLY the single currently active thumbnail in the background shines through with TAB highlight */
body.onara-modal-open .posts > a.thumb.onara-active {
opacity: 1 !important;
filter: none !important;
box-shadow: 0 0 0 2px var(--white, #fff), 0 0 16px rgba(255, 255, 255, 0.6) !important;
z-index: 25 !important;
}
body.onara-modal-open .posts > a.thumb.onara-active::after {
visibility: visible !important;
opacity: 1 !important;
}
/* Navbar and right sidebar stay fully visible, sharp, and interactive */
body.onara-modal-open nav.navbar {
opacity: 1 !important;
filter: none !important;
pointer-events: auto !important;
z-index: 10060 !important;
}
body.onara-modal-open .global-sidebar-right {
opacity: 1 !important;
filter: none !important;
pointer-events: auto !important;
z-index: 10060 !important;
}
body.onara-modal-open #sidebar-drag-zone {
opacity: 1 !important;
pointer-events: auto !important;
z-index: 10061 !important;
}
/* Modal wrapper covers main content area with subtle dark glassmorphism, respecting sidebar */
.onara-modal-wrapper {
position: fixed !important;
top: var(--navbar-h, 50px) !important;
bottom: 0 !important;
left: 0 !important;
right: 300px !important;
width: auto !important;
height: calc(100vh - var(--navbar-h, 50px)) !important;
z-index: 10050 !important;
display: none;
flex-direction: column !important;
background: rgba(10, 12, 16, 0.72) !important;
backdrop-filter: blur(5px) saturate(130%) !important;
-webkit-backdrop-filter: blur(5px) saturate(130%) !important;
overflow-y: auto !important;
overflow-x: hidden !important;
box-sizing: border-box !important;
animation: onaraFadeIn 0.22s cubic-bezier(0.16, 1, 0.3, 1) forwards;
transition: right 0.3s ease-in-out !important;
cursor: pointer;
}
body.onara-modal-open .onara-modal-wrapper {
display: flex !important;
}
/* Sidebar hidden: modal expands to right edge */
body.sidebar-right-hidden .onara-modal-wrapper {
right: 0 !important;
}
/* Tablet (600px - 999px): sidebar is min(280px, 80vw) */
@media (min-width: 600px) and (max-width: 999px) {
.onara-modal-wrapper {
right: min(280px, 80vw) !important;
}
body.sidebar-right-hidden .onara-modal-wrapper {
right: 0 !important;
}
}
/* Mobile (<= 599px): sidebar is pure overlay, modal uses full width */
@media (max-width: 599px) {
.onara-modal-wrapper {
right: 0 !important;
}
}
@keyframes onaraFadeIn {
from {
opacity: 0;
transform: scale(0.99);
}
to {
opacity: 1;
transform: scale(1);
}
}
.onara-backdrop {
position: absolute !important;
top: 0 !important;
left: 0 !important;
width: 100% !important;
height: 100% !important;
min-height: 100% !important;
z-index: 1 !important;
cursor: pointer;
}
.onara-modal-content {
position: relative !important;
z-index: 2 !important;
width: 100% !important;
min-height: 100% !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: flex-start !important;
flex: 1 0 auto !important;
padding: 20px 0 80px 0 !important;
box-sizing: border-box !important;
cursor: pointer;
}
#onara-item-mount {
width: 100% !important;
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: flex-start !important;
cursor: pointer;
}
/* Center the item layout inside modal and make it 100% scrollable */
#onara-modal .item-layout-container,
body.layout-legacy #onara-modal .item-layout-container,
body.layout-modern #onara-modal .item-layout-container {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
justify-content: flex-start !important;
width: 100% !important;
max-width: 100% !important;
height: auto !important;
min-height: auto !important;
max-height: none !important;
overflow: visible !important;
margin: 0 auto !important;
padding: 0 15px !important;
box-sizing: border-box !important;
grid-template-columns: none !important;
grid-template-rows: none !important;
cursor: pointer;
}
#onara-modal .item-main-content,
body.layout-legacy #onara-modal .item-layout-container .item-main-content,
body.layout-modern #onara-modal .item-layout-container .item-main-content {
cursor: default !important;
}
#onara-modal .item-main-content,
body.layout-legacy #onara-modal .item-layout-container .item-main-content,
body.layout-modern #onara-modal .item-layout-container .item-main-content {
display: flex !important;
flex-direction: column !important;
align-items: center !important;
width: 100% !important;
max-width: 850px !important;
height: auto !important;
min-height: auto !important;
max-height: none !important;
overflow: visible !important;
margin: 0 auto !important;
padding: 0 !important;
box-sizing: border-box !important;
grid-column: auto !important;
}
#onara-modal .item-main-content::before,
#onara-modal .item-main-content::after {
display: none !important;
}
#onara-modal .item-main-content > * {
width: 100% !important;
max-width: 800px !important;
margin-left: auto !important;
margin-right: auto !important;
}
#onara-modal .item-main-content .metadata {
width: 100% !important;
max-width: 600px !important;
margin-left: auto !important;
margin-right: auto !important;
}
#onara-modal #comments-container {
display: flex !important;
flex-direction: column !important;
flex: 0 0 auto !important;
height: auto !important;
max-height: none !important;
overflow: visible !important;
width: 100% !important;
max-width: 600px !important;
}
/* Modern layout: left sidebar flows below media inside modal */
body.layout-modern #onara-modal .item-sidebar-left {
display: flex !important;
flex-direction: column !important;
width: 100% !important;
max-width: 600px !important;
height: auto !important;
max-height: none !important;
position: static !important;
order: 2 !important;
margin: 20px auto 0 auto !important;
border-right: none !important;
box-shadow: none !important;
overflow: visible !important;
}
body.layout-modern #onara-modal .item-main-content {
order: 1 !important;
}
/* Auxiliary modals, overlays, and dialogs MUST open in front of Onara modal and fixed bars */
body.onara-modal-open .modal-overlay,
body.onara-modal-open .image-modal-overlay,
body.onara-modal-open .dm-modal-overlay,
body.onara-modal-open .swal2-container,
body.onara-modal-open .modal-backdrop,
body.onara-modal-open .modal,
body.onara-modal-open #image-modal,
body.onara-modal-open #report-modal,
body.onara-modal-open #info-modal,
body.onara-modal-open #visibility-modal,
body.onara-modal-open #halls-modal,
body.onara-modal-open #mod-action-modal,
body.onara-modal-open #warning-modal,
body.onara-modal-open #content-warning-modal,
body.onara-modal-open #login-modal,
body.onara-modal-open #register-modal,
body.onara-modal-open #shortcuts-modal,
body.onara-modal-open #excluded-tags-overlay,
body.onara-modal-open #upload-drag-modal,
body.onara-modal-open #rethumb-capture-modal,
body.onara-modal-open #gchat-img-modal {
z-index: 100100 !important;
}
+487 -117
View File
@@ -117,6 +117,17 @@ window.cancelAnimFrame = (function () {
} catch(e) { console.error('Visit tracking error:', e); }
};
const ensureLayoutClass = () => {
if (window.f0ckSession && window.f0ckSession.use_new_layout === false) {
document.body.classList.remove('layout-modern');
document.body.classList.add('layout-legacy');
} else if (!document.body.classList.contains('layout-legacy')) {
document.body.classList.add('layout-modern');
}
};
window.ensureLayoutClass = ensureLayoutClass;
ensureLayoutClass();
window.applyThumbCacheBust = (bgUrlStr) => {
if (!bgUrlStr) return bgUrlStr;
try {
@@ -178,8 +189,24 @@ window.cancelAnimFrame = (function () {
} catch(e) {}
});
const currentId = typeof window.getCurrentItemId === 'function' ? window.getCurrentItemId() : null;
// Update audio cover in player if viewing this audio item
const audioCover = document.getElementById('f0ck-audio-cover');
if (audioCover && currentId === idStr) {
const coverUrl = `/ca/${idStr}.webp?t=${timestamp}`;
audioCover.src = coverUrl;
const parent = audioCover.parentElement;
if (parent) {
parent.style.background = `url('${coverUrl}') no-repeat center / contain black`;
}
const audioEl = document.querySelector('audio#my-video');
if (audioEl) {
audioEl.setAttribute('poster', coverUrl);
}
}
// Refresh background canvas if it matches the current item
const currentId = window.getCurrentItemId();
if (currentId === idStr && window.initBackground) {
window.initBackground();
}
@@ -732,10 +759,16 @@ window.cancelAnimFrame = (function () {
if (data.success) {
window.flashMessage('ALL MODE ACTIVATED');
gridCacheMap.clear();
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
const isGridView = document.querySelector('.posts, .tags-grid');
const isItemView = document.getElementById('prev') || document.getElementById('next') || /^\/\d+/.test(window.location.pathname);
let reloadPromise = null;
if (isGridView) {
if (isOnaraOpen) {
const bgUrl = window._onaraReturnUrl || getOnaraBaseUrl();
const urlObj = new URL(bgUrl, window.location.origin);
urlObj.searchParams.delete('mode');
reloadPromise = window.loadPageAjax ? window.loadPageAjax(urlObj.toString(), true, { skipCache: true, skipPush: true }) : null;
} else if (isGridView) {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.delete('mode');
reloadPromise = window.loadPageAjax ? window.loadPageAjax(currentUrl.toString(), true, { skipCache: true }) : null;
@@ -790,10 +823,16 @@ window.cancelAnimFrame = (function () {
: 'ALL MODE ACTIVATED';
window.flashMessage(label);
gridCacheMap.clear();
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
const isGridView = document.querySelector('.posts, .tags-grid');
const isItemView = document.getElementById('prev') || document.getElementById('next') || /^\/\d+/.test(window.location.pathname);
let reloadPromise = null;
if (isGridView) {
if (isOnaraOpen) {
const bgUrl = window._onaraReturnUrl || getOnaraBaseUrl();
const urlObj = new URL(bgUrl, window.location.origin);
urlObj.searchParams.delete('mode');
reloadPromise = window.loadPageAjax ? window.loadPageAjax(urlObj.toString(), true, { skipCache: true, skipPush: true }) : null;
} else if (isGridView) {
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.delete('mode');
reloadPromise = window.loadPageAjax ? window.loadPageAjax(currentUrl.toString(), true, { skipCache: true }) : null;
@@ -1014,6 +1053,159 @@ window.cancelAnimFrame = (function () {
});
};
// ── Onara Alternative Flavor: Fullscreen Modal Wrapper ─────────────────────
const isOnaraActive = () => !!(window.f0ckSession?.onara || window.onara);
const getOrCreateOnaraModal = () => {
let modal = document.getElementById('onara-modal');
if (!modal) {
modal = document.createElement('div');
modal.id = 'onara-modal';
modal.className = 'onara-modal-wrapper';
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
modal.innerHTML = `
<div class="onara-backdrop" id="onara-backdrop"></div>
<div class="onara-modal-content" id="onara-modal-content">
<div id="onara-item-mount" class="item-view"></div>
</div>
`;
document.body.appendChild(modal);
}
return modal;
};
const updateOnaraActiveItem = (itemid, url) => {
if (!isOnaraActive()) return;
// Clear onara-active from any and all elements to guarantee only 1 item is selected
document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active'));
let targetThumb = null;
if (url) {
try {
const parsedPath = new URL(url, window.location.origin).pathname;
targetThumb = document.querySelector(`.posts > a.thumb[href="${parsedPath}"], .posts > a.thumb[href="${url}"]`);
} catch {}
}
if (!targetThumb && itemid) {
targetThumb = document.querySelector(`.posts > a.thumb[href$="/${itemid}"], .posts > a.thumb[data-bg*="/${itemid}."]`);
}
// Blur any other active element so browser native focus cannot highlight a second thumbnail
if (document.activeElement && document.activeElement !== targetThumb && typeof document.activeElement.blur === 'function') {
document.activeElement.blur();
}
if (targetThumb) {
targetThumb.classList.add('onara-active');
targetThumb.scrollIntoView({ block: 'center', inline: 'nearest', behavior: 'instant' });
}
};
window.updateOnaraActiveItem = updateOnaraActiveItem;
const getOnaraBaseUrl = (itemUrl = window.location.href) => {
try {
const urlObj = new URL(itemUrl, window.location.origin);
let path = urlObj.pathname.replace(/\/+$/, '');
path = path.replace(/\/(?:\d+|[a-zA-Z0-9_-]{11})$/, '');
const postsEl = document.querySelector('.posts[data-current-page]');
const currentPage = postsEl ? parseInt(postsEl.dataset.currentPage, 10) : 1;
if (currentPage > 1 && !path.includes('/p/')) {
path = (path ? path : '') + '/p/' + currentPage;
}
if (!path) path = '/';
return path + (urlObj.search || '') + (urlObj.hash || '');
} catch {
return '/';
}
};
window.getOnaraBaseUrl = getOnaraBaseUrl;
const promoteOnaraModals = () => {
const mount = document.getElementById('onara-item-mount');
if (!mount) return;
const modals = mount.querySelectorAll('#info-modal, #visibility-modal, .modal-overlay');
modals.forEach(m => {
const existing = document.body.querySelector(`:scope > #${m.id}`);
if (existing && existing !== m) {
existing.remove();
}
document.body.appendChild(m);
});
};
window.promoteOnaraModals = promoteOnaraModals;
const closeOnaraModal = (options = {}) => {
const modal = document.getElementById('onara-modal');
if (!modal || modal.style.display === 'none') return;
// Close any auxiliary modals that may be open
const infoModal = document.getElementById('info-modal');
if (infoModal) infoModal.style.display = 'none';
const visModal = document.getElementById('visibility-modal');
if (visModal) visModal.style.display = 'none';
const reportModal = document.getElementById('report-modal');
if (reportModal) reportModal.style.display = 'none';
const hallsModal = document.getElementById('halls-modal');
if (hallsModal) hallsModal.style.display = 'none';
if (typeof window.closeImageModal === 'function') window.closeImageModal();
document.body.classList.remove('modal-open');
stopMedia();
const mount = document.getElementById('onara-item-mount');
if (mount) mount.innerHTML = '';
modal.style.display = 'none';
modal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('onara-modal-open');
// Restore background canvas if needed
const canvas = document.getElementById('bg');
if (canvas) {
canvas.classList.remove('fader-out', 'fast-fade');
if (window.initBackground) window.initBackground();
}
if (!options.skipHistory) {
const returnUrl = window._onaraReturnUrl || getOnaraBaseUrl();
history.pushState({}, '', returnUrl);
if (window._onaraReturnTitle) {
document.title = window._onaraReturnTitle;
} else if (window.f0ckDomain) {
document.title = window.f0ckDomain;
}
}
window._onaraReturnUrl = null;
window._onaraReturnTitle = null;
// Maintain focus on active item so user TAB navigation seamlessly resumes from current progress
const activeThumb = document.querySelector('.posts > a.thumb.onara-active');
document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active'));
if (activeThumb) {
try {
activeThumb.focus({ preventScroll: true });
} catch {}
}
};
window.closeOnaraModal = closeOnaraModal;
window.isOnaraActive = isOnaraActive;
// Onara modal close click delegation: clicking outside .item-main-content closes modal
document.addEventListener('click', (e) => {
if (!document.body.classList.contains('onara-modal-open')) return;
// Do not close if clicking inside the item content
if (e.target.closest('.item-main-content')) return;
// Do not close if clicking auxiliary modals, dropdowns, or notifications
if (e.target.closest('.modal-overlay:not(#onara-modal), .modal-content, .swal2-container, .tippy-box, #comments-container .emoji-picker')) return;
// Do not close if clicking the global sidebar right or navbar (as modal respects them)
if (e.target.closest('.global-sidebar-right, nav.navbar, #sidebar-drag-zone')) return;
// Any click outside .item-main-content (e.g. left side, right side, backdrop, modal background) closes
if (e.target.closest('#onara-modal') || e.target.id === 'onara-backdrop') {
e.preventDefault();
closeOnaraModal();
}
});
if (loginModal) {
if (loginBtn) {
loginBtn.addEventListener('click', (e) => {
@@ -1372,8 +1564,49 @@ window.cancelAnimFrame = (function () {
closeModal(loginModal);
closeModal(registerModal);
closeModal(shortcutsModal);
const imageModal = document.getElementById('image-modal');
if (imageModal && (imageModal.classList.contains('visible') || imageModal.style.display === 'flex' || imageModal.style.display === 'block')) {
if (typeof window.closeImageModal === 'function') window.closeImageModal();
else {
imageModal.classList.remove('visible');
imageModal.style.display = 'none';
document.body.classList.remove('modal-open');
}
return;
}
const infoModal = document.getElementById('info-modal');
if (infoModal) closeModal(infoModal);
if (infoModal && infoModal.style.display !== 'none') {
infoModal.style.display = 'none';
document.body.classList.remove('modal-open');
return;
}
const visModal = document.getElementById('visibility-modal');
if (visModal && visModal.style.display !== 'none') {
visModal.style.display = 'none';
document.body.classList.remove('modal-open');
return;
}
const reportModal = document.getElementById('report-modal');
if (reportModal && reportModal.style.display !== 'none') {
reportModal.style.display = 'none';
document.body.classList.remove('modal-open');
return;
}
const hallsModal = document.getElementById('halls-modal');
if (hallsModal && hallsModal.style.display !== 'none') {
hallsModal.style.display = 'none';
document.body.classList.remove('modal-open');
return;
}
const anyOtherModal = document.querySelector('.modal-overlay:not(#onara-modal):not([style*="display: none"]), .modal.show, .swal2-container');
if (anyOtherModal && window.getComputedStyle(anyOtherModal).display !== 'none') {
anyOtherModal.style.display = 'none';
document.body.classList.remove('modal-open');
return;
}
if (document.body.classList.contains('onara-modal-open')) {
closeOnaraModal();
}
}
});
@@ -1519,15 +1752,42 @@ window.cancelAnimFrame = (function () {
}
};
const initOnaraInitialState = () => {
if (!isOnaraActive()) return;
promoteOnaraModals();
if (document.body.classList.contains('onara-modal-open')) {
const modal = document.getElementById('onara-modal');
if (modal) {
modal.style.display = 'flex';
modal.setAttribute('aria-hidden', 'false');
}
if (!window._onaraReturnUrl) {
window._onaraReturnUrl = getOnaraBaseUrl();
window._onaraReturnTitle = window.f0ckDomain || 'f0ck';
}
const pathSegments = window.location.pathname.split('/');
const keySegments = pathSegments.filter(s => /^\d+$/.test(s) || /^[a-zA-Z0-9_-]{11}$/.test(s));
const activeKey = keySegments.length ? keySegments[keySegments.length - 1] : null;
if (activeKey) {
updateOnaraActiveItem(activeKey, window.location.href);
if (typeof window.trackVisit === 'function') {
window.trackVisit(activeKey);
}
}
}
};
// Initial Load
document.addEventListener('DOMContentLoaded', () => {
setupMedia();
initOnaraInitialState();
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(() => prefetchNextPrevItems());
} else {
setTimeout(prefetchNextPrevItems, 500);
}
});
initOnaraInitialState();
// Export init function for dynamic calls
window.initBackground = () => {
@@ -2380,6 +2640,7 @@ window.cancelAnimFrame = (function () {
// Immediately restore scrollability and hide modals
if (window.resetGlobalScrollState) window.resetGlobalScrollState();
if (window.hideAllModals) window.hideAllModals();
if (window.closeOnaraModal) window.closeOnaraModal({ skipHistory: true });
isNavigating = true;
@@ -2493,7 +2754,7 @@ window.cancelAnimFrame = (function () {
// Keep cache alive — cleared on mode/filter changes via gridCacheMap.clear()
if (navbar) navbar.classList.add('pbwork');
stopMedia();
if (!document.body.classList.contains('onara-modal-open')) stopMedia();
// Fade out background canvas (item view blur)
const canvas = document.getElementById('bg');
@@ -2514,9 +2775,12 @@ window.cancelAnimFrame = (function () {
cache.node.style.display = '';
cache.node.classList.remove('cached-grid');
main.className = '';
document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy');
document.body.style.overflow = '';
document.body.style.height = '';
document.body.classList.remove('legacy-view');
ensureLayoutClass();
if (!document.body.classList.contains('onara-modal-open')) {
document.body.style.overflow = '';
document.body.style.height = '';
}
const paginationWrapper = document.querySelector('.pagination-wrapper');
if (paginationWrapper && cache.pagination) paginationWrapper.innerHTML = cache.pagination;
const restoredPosts = cache.node.querySelector('.posts');
@@ -2527,7 +2791,7 @@ window.cancelAnimFrame = (function () {
}
// Update URL bar (only for explicit clicks, not browser back/forward which already did it)
if (!options.skipPush) history.pushState({}, '', url);
if (!options.skipPush && !document.body.classList.contains('onara-modal-open')) history.pushState({}, '', url);
// Restore scroll position
requestAnimationFrame(() => window.scrollTo(0, cache.scroll));
@@ -2576,11 +2840,13 @@ window.cancelAnimFrame = (function () {
main.className = ''; // Reset class
// Clear legacy-view state (layout-lock) now that we are swapping to a list view
document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy');
document.body.style.overflow = '';
document.body.style.height = '';
// Toggle button lives permanently in body via header.html — no removal needed
stopMedia(); // Ensure media stops when leaving item view
document.body.classList.remove('legacy-view');
ensureLayoutClass();
if (!document.body.classList.contains('onara-modal-open')) {
document.body.style.overflow = '';
document.body.style.height = '';
stopMedia(); // Ensure media stops when leaving item view
}
// Fade out background canvas for smooth transition to black
const canvas = document.getElementById('bg');
@@ -2737,6 +3003,7 @@ window.cancelAnimFrame = (function () {
// Ensure we keep a layout class but reset others
document.body.classList.remove('item-layout-active', 'meme-layout-active'); // Kill known breakers
ensureLayoutClass();
}
// Reset navbar to clean state
@@ -2745,9 +3012,11 @@ window.cancelAnimFrame = (function () {
navbar.classList.remove('scrolled', 'pbwork');
}
document.body.style.overflow = '';
document.body.style.height = '';
document.body.style.minHeight = '';
if (!document.body.classList.contains('onara-modal-open')) {
document.body.style.overflow = '';
document.body.style.height = '';
document.body.style.minHeight = '';
}
window.f0ckDebug("[loadPageAjax] State synced for " + (isFullPage ? "full page" : "partial"));
if (window.updateMimeLabel) window.updateMimeLabel();
@@ -2949,7 +3218,7 @@ window.cancelAnimFrame = (function () {
}
// UPDATE HISTORY BEFORE INITIALIZING SCRIPTS
if (!options.skipPush) {
if (!options.skipPush && !document.body.classList.contains('onara-modal-open')) {
history.pushState({}, '', url);
}
@@ -3319,6 +3588,15 @@ window.cancelAnimFrame = (function () {
isNavigating = true;
const isGridPage = !!document.querySelector('.index-layout-wrapper, .index-container, .posts');
const isModalOpen = document.body.classList.contains('onara-modal-open');
const useOnara = isOnaraActive() && (isGridPage || isModalOpen);
if (useOnara && !isModalOpen) {
window._onaraReturnUrl = window.location.href;
window._onaraReturnTitle = document.title;
}
// ── Scroller-active cleanup (same as loadPageAjax) ───────────────────
if (document.body.classList.contains('scroller-active')) {
document.head.querySelectorAll('[data-pjax-abyss]').forEach(el => el.remove());
@@ -3349,7 +3627,7 @@ window.cancelAnimFrame = (function () {
const currentScroll = window.scrollY;
// Save scroll position for current page (only for new navigations)
if (!options.skipPush && !options.keepMedia) {
if (!options.skipPush && !options.keepMedia && !useOnara) {
const currentState = history.state || {};
history.replaceState({ ...currentState, scroll: currentScroll }, document.title, window.location.href);
}
@@ -3521,39 +3799,54 @@ window.cancelAnimFrame = (function () {
// ── Render cached HTML (same DOM setup as the normal path below) ──
const _html = _cachedItem.html;
let _container = document.querySelector('#main .container') || (document.getElementById('main')?.classList.contains('item-view') ? document.getElementById('main') : null);
const _isStructuralPage = !!document.querySelector('.pagewrapper');
const _isModeratorPage = !!document.querySelector('.approval-grid, .audit-log-container, .mod-reports-page');
const _isNotificationsPage = !!document.querySelector('.notifications-page, .notif-history-container');
const _isIndexPage = !!document.querySelector('.index-container, .posts');
const _isStaticPage = !!document.querySelector('.static-page, .about-container, .upload-container');
let _container;
if (!options.keepMedia && (!_container || _isModeratorPage || _isNotificationsPage || _isIndexPage || _isStaticPage || _isStructuralPage)) {
if (main) {
const _iw = main.querySelector('.index-layout-wrapper');
const _ic = main.querySelector('.index-container');
const _ntc = _iw || _ic;
if (_ntc && _isIndexPage) {
const _ck = window.location.pathname + window.location.search;
const _pw = document.querySelector('.pagination-wrapper');
gridCacheMap.set(_ck, { node: _ntc, scroll: currentScroll, pagination: _pw ? _pw.innerHTML : '' });
_ntc.remove();
if (useOnara) {
const onaraModal = getOrCreateOnaraModal();
const onaraMount = document.getElementById('onara-item-mount');
document.body.classList.add('onara-modal-open');
onaraModal.style.display = 'flex';
onaraModal.setAttribute('aria-hidden', 'false');
if (!options.keepMedia) {
onaraMount.innerHTML = '';
}
_container = onaraMount;
updateOnaraActiveItem(itemid, url);
} else {
_container = document.querySelector('#main .container') || (document.getElementById('main')?.classList.contains('item-view') ? document.getElementById('main') : null);
const _isStructuralPage = !!document.querySelector('.pagewrapper');
const _isModeratorPage = !!document.querySelector('.approval-grid, .audit-log-container, .mod-reports-page');
const _isNotificationsPage = !!document.querySelector('.notifications-page, .notif-history-container');
const _isIndexPage = !!document.querySelector('.index-container, .posts');
const _isStaticPage = !!document.querySelector('.static-page, .about-container, .upload-container');
if (!options.keepMedia && (!_container || _isModeratorPage || _isNotificationsPage || _isIndexPage || _isStaticPage || _isStructuralPage)) {
if (main) {
const _iw = main.querySelector('.index-layout-wrapper');
const _ic = main.querySelector('.index-container');
const _ntc = _iw || _ic;
if (_ntc && _isIndexPage) {
const _ck = window.location.pathname + window.location.search;
const _pw = document.querySelector('.pagination-wrapper');
gridCacheMap.set(_ck, { node: _ntc, scroll: currentScroll, pagination: _pw ? _pw.innerHTML : '' });
_ntc.remove();
}
main.className = 'item-view';
document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy');
if (window.f0ckSession && !window.f0ckSession.use_new_layout) {
document.body.classList.add('layout-legacy');
} else {
document.body.classList.add('layout-modern');
}
if (window.syncNavbarHeight) window.syncNavbarHeight();
main.innerHTML = '';
_container = main;
}
main.className = 'item-view';
document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy');
if (window.f0ckSession && !window.f0ckSession.use_new_layout) {
document.body.classList.add('layout-legacy');
} else {
document.body.classList.add('layout-modern');
}
if (window.syncNavbarHeight) window.syncNavbarHeight();
main.innerHTML = '';
_container = main;
} else if (_container && !options.keepMedia) {
_container.innerHTML = '';
_container.removeAttribute('style');
if (main) main.className = 'item-view';
}
} else if (_container && !options.keepMedia) {
_container.innerHTML = '';
_container.removeAttribute('style');
if (main) main.className = 'item-view';
}
if (options.keepMedia) {
@@ -3568,14 +3861,18 @@ window.cancelAnimFrame = (function () {
_container.insertAdjacentHTML('beforeend', _html);
}
if (options.skipPush && history.state?.scroll !== undefined) {
if (useOnara) {
promoteOnaraModals();
const onaraModal = document.getElementById('onara-modal');
if (onaraModal) onaraModal.scrollTo(0, 0);
} else if (options.skipPush && history.state?.scroll !== undefined) {
requestAnimationFrame(() => window.scrollTo(0, history.state.scroll));
} else if (!options.keepMedia) {
window.scrollTo(0, 0);
}
const _hash = new URL(url, window.location.origin).hash;
const itemKey = _cachedItem.slug || itemid;
const itemKey = (window.f0ckSession?.enable_item_slugs !== false && _cachedItem.slug) ? _cachedItem.slug : (_cachedItem.slug || itemid);
let _pushUrl = `/${itemKey}`;
if (userHall && userHallOwner) _pushUrl = `/user/${encodeURIComponent(userHallOwner)}/hall/${encodeURIComponent(userHall)}/${itemKey}`;
else if (user) { _pushUrl = `/user/${encodeURIComponent(user)}/${itemKey}`; if (isFavs) _pushUrl = `/user/${encodeURIComponent(user)}/favs/${itemKey}`; }
@@ -3585,7 +3882,7 @@ window.cancelAnimFrame = (function () {
if (mime) _pushUrl = _pushUrl.replace(new RegExp(`/${itemKey}$`), `/${mime}/${itemKey}`);
if (_hash) _pushUrl += _hash;
if (!options.keepMedia && !options.skipPush) history.pushState({}, '', _pushUrl);
if (!options.keepMedia && !options.skipPush) history.pushState({ onara: useOnara }, '', _pushUrl);
document.title = `${window.f0ckDomain} - ${itemKey}`;
if (navbar) navbar.classList.remove('pbwork');
@@ -3678,55 +3975,69 @@ window.cancelAnimFrame = (function () {
// Track visit for guests
window.trackVisit(itemid);
let container = document.querySelector('#main .container') || (document.getElementById('main') && document.getElementById('main').classList.contains('item-view') ? document.getElementById('main') : null);
const isStructuralPage = !!document.querySelector('.pagewrapper');
const isModeratorPage = !!document.querySelector('.approval-grid, .audit-log-container, .mod-reports-page');
const isNotificationsPage = !!document.querySelector('.notifications-page, .notif-history-container');
const isIndexPage = !!document.querySelector('.index-container, .posts');
const isStaticPage = !!document.querySelector('.static-page, .about-container, .upload-container');
let container;
if (useOnara) {
const onaraModal = getOrCreateOnaraModal();
const onaraMount = document.getElementById('onara-item-mount');
document.body.classList.add('onara-modal-open');
onaraModal.style.display = 'flex';
onaraModal.setAttribute('aria-hidden', 'false');
if (!options.keepMedia) {
onaraMount.innerHTML = '';
}
container = onaraMount;
updateOnaraActiveItem(itemid, url);
} else {
container = document.querySelector('#main .container') || (document.getElementById('main') && document.getElementById('main').classList.contains('item-view') ? document.getElementById('main') : null);
const isStructuralPage = !!document.querySelector('.pagewrapper');
const isModeratorPage = !!document.querySelector('.approval-grid, .audit-log-container, .mod-reports-page');
const isNotificationsPage = !!document.querySelector('.notifications-page, .notif-history-container');
const isIndexPage = !!document.querySelector('.index-container, .posts');
const isStaticPage = !!document.querySelector('.static-page, .about-container, .upload-container');
if (!options.keepMedia && (!container || isModeratorPage || isNotificationsPage || isIndexPage || isStaticPage || isStructuralPage)) {
// Transition to Item View
if (main) {
// Check if we can cache the existing grid (Index Page only)
const indexWrapper = main.querySelector('.index-layout-wrapper');
const indexContainer = main.querySelector('.index-container');
const nodeToCache = indexWrapper || indexContainer;
if (!options.keepMedia && (!container || isModeratorPage || isNotificationsPage || isIndexPage || isStaticPage || isStructuralPage)) {
// Transition to Item View
if (main) {
// Check if we can cache the existing grid (Index Page only)
const indexWrapper = main.querySelector('.index-layout-wrapper');
const indexContainer = main.querySelector('.index-container');
const nodeToCache = indexWrapper || indexContainer;
if (nodeToCache && isIndexPage) {
const cacheKey = window.location.pathname + window.location.search;
const paginationWrapper = document.querySelector('.pagination-wrapper');
gridCacheMap.set(cacheKey, {
node: nodeToCache,
scroll: currentScroll,
pagination: paginationWrapper ? paginationWrapper.innerHTML : ''
});
nodeToCache.remove();
}
main.className = 'item-view';
if (nodeToCache && isIndexPage) {
const cacheKey = window.location.pathname + window.location.search;
const paginationWrapper = document.querySelector('.pagination-wrapper');
gridCacheMap.set(cacheKey, {
node: nodeToCache,
scroll: currentScroll,
pagination: paginationWrapper ? paginationWrapper.innerHTML : ''
});
nodeToCache.remove();
}
main.className = 'item-view';
// Correctly handle body classes for the current layout mode
document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy');
if (window.f0ckSession && !window.f0ckSession.use_new_layout) {
document.body.classList.add('layout-legacy');
} else {
document.body.classList.add('layout-modern');
}
if (window.syncNavbarHeight) window.syncNavbarHeight();
// Correctly handle body classes for the current layout mode
document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy');
if (window.f0ckSession && !window.f0ckSession.use_new_layout) {
document.body.classList.add('layout-legacy');
} else {
document.body.classList.add('layout-modern');
}
if (window.syncNavbarHeight) window.syncNavbarHeight();
// Toggle button lives permanently in body via header.html — no creation needed
// Toggle button lives permanently in body via header.html — no creation needed
// HTML goes directly into #main without outdated .container wrapper for New Layout
main.innerHTML = '';
container = main; // HTML goes directly into #main
// HTML goes directly into #main without outdated .container wrapper for New Layout
main.innerHTML = '';
container = main; // HTML goes directly into #main
}
} else if (container && !options.keepMedia) {
// Already in some kind of item view or compatible layout, just clear it
container.innerHTML = '';
container.removeAttribute('style'); // Ensure no carry-over styles
if (main) main.className = 'item-view';
}
} else if (container && !options.keepMedia) {
// Already in some kind of item view or compatible layout, just clear it
container.innerHTML = '';
container.removeAttribute('style'); // Ensure no carry-over styles
if (main) main.className = 'item-view';
}
if (options.keepMedia) {
@@ -3763,7 +4074,11 @@ window.cancelAnimFrame = (function () {
// Handle Scroll Position (Restore or Reset)
if (options.skipPush && history.state && history.state.scroll !== undefined) {
if (useOnara) {
promoteOnaraModals();
const onaraModal = document.getElementById('onara-modal');
if (onaraModal) onaraModal.scrollTo(0, 0);
} else if (options.skipPush && history.state && history.state.scroll !== undefined) {
requestAnimationFrame(() => window.scrollTo(0, history.state.scroll));
} else if (!options.keepMedia) {
window.scrollTo(0, 0);
@@ -3775,7 +4090,7 @@ window.cancelAnimFrame = (function () {
// Construct proper History URL (Context Aware)
// If we inherited context, we should reflect it in the URL
const hash = new URL(url, window.location.origin).hash;
const itemKey = responseSlug || itemid;
const itemKey = (window.f0ckSession?.enable_item_slugs !== false && responseSlug) ? responseSlug : (responseSlug || itemid);
let pushUrl = `/${itemKey}`;
// Logic from ajax.mjs context reconstruction:
if (userHall && userHallOwner) {
@@ -3799,7 +4114,7 @@ window.cancelAnimFrame = (function () {
if (!options.keepMedia && !options.skipPush) {
// We overwrite proper URL even if the link clicked was "naked"
history.pushState({}, '', pushUrl);
history.pushState({ onara: useOnara }, '', pushUrl);
}
if (!options.keepMedia) {
@@ -3900,11 +4215,17 @@ window.cancelAnimFrame = (function () {
gridCacheMap.clear();
// Refresh content
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
const isGridView = document.querySelector('.posts, .tags-grid');
const isItemView = document.getElementById('prev') || document.getElementById('next');
let reloadPromise = null;
if (isGridView) {
if (isOnaraOpen) {
const bgUrl = window._onaraReturnUrl || getOnaraBaseUrl();
const urlObj = new URL(bgUrl, window.location.origin);
urlObj.searchParams.delete('mode');
reloadPromise = loadPageAjax(urlObj.toString(), true, { skipCache: true, skipPush: true });
} else if (isGridView) {
// Reload current page without ?mode= in the URL — server picks up mode from session/cookie
const currentUrl = new URL(window.location.href);
currentUrl.searchParams.delete('mode');
@@ -3935,6 +4256,13 @@ window.cancelAnimFrame = (function () {
const thumbnail = target.closest('.posts > a:not(.notif-item)');
if (thumbnail && !e.ctrlKey && !e.shiftKey && !e.altKey && !e.metaKey) {
e.preventDefault();
if (isOnaraActive()) {
document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active'));
thumbnail.classList.add('onara-active');
if (document.activeElement && document.activeElement !== thumbnail && typeof document.activeElement.blur === 'function') {
document.activeElement.blur();
}
}
// Thumbnails inherit context (e.g. from Tag Index)
loadItemAjax(thumbnail.href, true);
return;
@@ -3945,6 +4273,25 @@ window.cancelAnimFrame = (function () {
// Special check for random
if (link.id === 'random' || link.id === 'nav-random') {
e.preventDefault();
// In Onara mode with a background grid, pick a random item from the grid so it reflects on the main page
if (isOnaraActive()) {
const gridThumbs = Array.from(document.querySelectorAll('.posts > a:not(.notif-item)'));
if (gridThumbs.length > 0) {
const pool = gridThumbs.filter(t => !t.classList.contains('onara-active'));
const chosen = (pool.length > 0 ? pool : gridThumbs)[Math.floor(Math.random() * (pool.length > 0 ? pool : gridThumbs).length)];
if (chosen) {
document.querySelectorAll('.onara-active').forEach(el => el.classList.remove('onara-active'));
chosen.classList.add('onara-active');
if (document.activeElement && document.activeElement !== chosen && typeof document.activeElement.blur === 'function') {
document.activeElement.blur();
}
loadItemAjax(chosen.href, true);
return;
}
}
}
stopMedia(); // Abort media BEFORE fetching random item
// Fade out background canvas for smooth transition
@@ -4590,6 +4937,11 @@ window.cancelAnimFrame = (function () {
window.f0ckDebug("[popstate] Navigation to:", url, { isItem, isSpecial, isGridLike });
if (document.body.classList.contains('onara-modal-open') && !isItem) {
closeOnaraModal({ skipHistory: true });
return;
}
if (isItem) {
loadItemAjax(url, true, { skipPush: true });
} else if (isSpecial || isGridLike) {
@@ -4872,7 +5224,9 @@ window.cancelAnimFrame = (function () {
if (infiniteState.currentPage !== p) {
infiniteState.currentPage = p;
infiniteState.firstPage = p;
history.replaceState({}, '', buildUrl(p));
if (!document.body.classList.contains('onara-modal-open')) {
history.replaceState({}, '', buildUrl(p));
}
}
delete postsContainer.dataset.currentPage; // Clear after reading
@@ -4948,6 +5302,11 @@ window.cancelAnimFrame = (function () {
return;
}
// Never overwrite item URL with /p/N when Onara modal is open!
if (document.body.classList.contains('onara-modal-open')) {
return;
}
// Update URL
infiniteState.visiblePage = newPage;
history.replaceState({}, '', buildUrl(newPage));
@@ -9328,8 +9687,12 @@ document.addEventListener("DOMContentLoaded", function () {
if (newPath === '') newPath = '/';
url.pathname = newPath;
// Update the browser URL without reloading
history.replaceState({}, '', url.toString());
const isOnaraOpen = document.body.classList.contains('onara-modal-open');
// Update the browser URL without reloading (only when not in onara modal)
if (!isOnaraOpen) {
history.replaceState({}, '', url.toString());
}
// Sync UI immediately (Before AJAX)
if (window.updateMimeLabel) window.updateMimeLabel();
@@ -9342,7 +9705,10 @@ document.addEventListener("DOMContentLoaded", function () {
// Set flag so hideAllModals (called inside loadPageAjax) skips the filter overlay
if (fromModal) window._keepFilterModal = true;
if (postsEl) {
if (isOnaraOpen) {
const bgUrl = window._onaraReturnUrl || getOnaraBaseUrl();
reloadPromise = window.loadPageAjax ? window.loadPageAjax(bgUrl, { skipPush: true, skipCache: true }) : null;
} else if (postsEl) {
reloadPromise = window.loadPageAjax ? window.loadPageAjax(url.toString(), { skipPush: true }) : null;
} else if (document.getElementById('main')?.classList.contains('item-view') || document.querySelector('.item-layout-container, .item-main-content')) {
// On item view, refresh the context but KEEP media playing
@@ -10191,7 +10557,7 @@ document.addEventListener('DOMContentLoaded', () => {
}
// Close logic
if (e.target.matches('#report-cancel')) {
if (e.target.matches('#report-cancel') || e.target.id === 'report-modal') {
reportModal.style.display = 'none';
document.body.classList.remove('modal-open');
}
@@ -11486,7 +11852,7 @@ document.addEventListener('click', (e) => {
}
const closeBtn = e.target.closest('#info-modal-close, #info-modal-close-icon');
if (closeBtn) {
if (closeBtn || e.target.id === 'info-modal') {
e.preventDefault();
const modal = document.getElementById('info-modal');
if (modal) {
@@ -11683,21 +12049,25 @@ document.addEventListener('click', (e) => {
if (data.success) {
if (window.flashMessage) window.flashMessage('THUMBNAIL REGENERATED');
else if (window.showFlash) window.showFlash('Thumbnail regenerated');
// Cache-bust thumbnails on page
// Cache-bust thumbnails and player media on page
const timestamp = Date.now();
document.querySelectorAll(`img[src*="/t/${itemId}.webp"]`).forEach(img => {
const url = new URL(img.src, window.location.origin);
url.searchParams.set('t', timestamp);
img.src = url.toString();
});
document.querySelectorAll(`.thumb[data-bg*="/t/${itemId}.webp"]`).forEach(thumb => {
const bg = thumb.dataset.bg;
if (bg) {
const url = new URL(bg, window.location.origin);
if (typeof window.refreshItemThumbnails === 'function') {
window.refreshItemThumbnails(itemId, timestamp);
} else {
document.querySelectorAll(`img[src*="/t/${itemId}.webp"]`).forEach(img => {
const url = new URL(img.src, window.location.origin);
url.searchParams.set('t', timestamp);
thumb.style.setProperty('--thumb-bg', `url('${url.toString()}')`);
}
});
img.src = url.toString();
});
document.querySelectorAll(`.thumb[data-bg*="/t/${itemId}.webp"]`).forEach(thumb => {
const bg = thumb.dataset.bg;
if (bg) {
const url = new URL(bg, window.location.origin);
url.searchParams.set('t', timestamp);
thumb.style.setProperty('--thumb-bg', `url('${url.toString()}')`);
}
});
}
} else {
if (window.flashError) window.flashError(data.msg || 'Regeneration failed');
else alert(data.msg || 'Regeneration failed');
+33 -7
View File
@@ -39,20 +39,42 @@ const storage = process.env.STORAGE_DIR;
const resolvePath = (defaultRel) => {
const local = path.resolve(path.join(base, defaultRel));
let target = local;
if (storage) {
const absStorage = path.resolve(storage);
if (defaultRel.startsWith('public/')) {
const sub = defaultRel.replace('public/', '');
if (sub === 's/emojis' || sub === 's/koepfe' || sub === 's/fonts') {
const storagePath = path.join(absStorage, sub.split('/').pop());
if (fs.existsSync(storagePath)) return path.resolve(storagePath);
return local;
if (fs.existsSync(storagePath)) target = path.resolve(storagePath);
else target = local;
} else {
target = path.resolve(path.join(absStorage, sub));
}
return path.resolve(path.join(absStorage, sub));
} else {
target = path.resolve(path.join(absStorage, defaultRel));
}
return path.resolve(path.join(absStorage, defaultRel));
}
return local;
try {
if (fs.existsSync(target)) {
return fs.realpathSync(target);
}
const parent = path.dirname(target);
if (fs.existsSync(parent)) {
return path.join(fs.realpathSync(parent), path.basename(target));
}
} catch (_) {}
return target;
};
const resolveImportPath = () => {
const p = storage ? path.resolve(path.join(path.resolve(storage), 'import')) : path.resolve(path.join(base, 'f0ckm-data/import'));
try {
if (fs.existsSync(p)) return fs.realpathSync(p);
const parent = path.dirname(p);
if (fs.existsSync(parent)) return path.join(fs.realpathSync(parent), path.basename(p));
} catch (_) {}
return p;
};
config.paths = {
@@ -61,7 +83,11 @@ config.paths = {
c: resolvePath('public/c'),
t: resolvePath('public/t'),
ca: resolvePath('public/ca'),
s: path.join(base, 'public/s'),
s: (() => {
const sPath = path.join(base, 'public/s');
try { if (fs.existsSync(sPath)) return fs.realpathSync(sPath); } catch (_) {}
return sPath;
})(),
emojis: resolvePath('public/s/emojis'),
koepfe: resolvePath('public/s/koepfe'),
fonts: resolvePath('public/s/fonts'),
@@ -71,7 +97,7 @@ config.paths = {
deleted: resolvePath('deleted'),
logs: resolvePath('logs'),
tmp: resolvePath('tmp'),
import: storage ? path.resolve(path.join(path.resolve(storage), 'import')) : path.resolve(path.join(base, 'f0ckm-data/import'))
import: resolveImportPath()
};
export default config;
+35 -8
View File
@@ -354,9 +354,12 @@ export default new class queue {
}
async genThumbnail(filename, mime, itemid, link, pending = false, size = 512) {
const bDir = pending ? path.join(cfg.paths.pending, 'b') : cfg.paths.b;
const tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
const cDir = pending ? path.join(cfg.paths.pending, 'ca') : cfg.paths.ca;
let bDir = pending ? path.join(cfg.paths.pending, 'b') : cfg.paths.b;
let tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
let cDir = pending ? path.join(cfg.paths.pending, 'ca') : cfg.paths.ca;
try { bDir = await fs.promises.realpath(bDir); } catch (_) {}
try { tDir = await fs.promises.realpath(tDir); } catch (_) {}
try { cDir = await fs.promises.realpath(cDir); } catch (_) {}
const tmpFile = path.join(os.tmpdir(), itemid + '.png');
const tmpJpg = path.join(os.tmpdir(), itemid + '.jpg');
const thumbSize = (size && size > 128) ? size : 128;
@@ -372,7 +375,13 @@ export default new class queue {
}
} catch (e) {}
const outPath = path.join(tDir, itemid + '.webp');
let outPath = path.join(tDir, itemid + '.webp');
try {
const outStat = await fs.promises.lstat(outPath);
if (outStat.isSymbolicLink()) {
await fs.promises.unlink(outPath).catch(() => {});
}
} catch (_) {}
try {
@@ -457,17 +466,17 @@ export default new class queue {
else if (mime.startsWith('audio/')) {
let coverExtracted = false;
this._lastCoverExtracted = false; // Reset state for this call
if (link.match(/soundcloud/)) {
if (link && typeof link === 'string' && link.match(/soundcloud/)) {
const proxyArgs = (cfg.main.socks && cfg.main.socks !== 'undefined' && cfg.main.socks !== '') ? ['--proxy', cfg.main.socks.includes('://') ? cfg.main.socks : `socks5h://${cfg.main.socks}`] : [];
let cover = (await this.spawn('yt-dlp', [...proxyArgs, '-f', 'bv*[height<=720]+ba/b[height<=720] / wv*+ba/w', '--get-thumbnail', link])).stdout.trim().split('\n').map(l => l.trim()).filter(l => l.length > 0).pop();
if (!cover.match(/default_avatar/)) {
if (cover && !cover.match(/default_avatar/)) {
cover = cover.replace(/-(large|original)\./, '-t500x500.');
try {
const curlArgs = ['-s', '-L', cover, '-o', tmpJpg];
if (proxyArgs.length > 0) curlArgs.push(...proxyArgs);
await this.spawn('curl', curlArgs);
const size = (await fs.promises.stat(tmpJpg)).size;
if (size >= 0) {
if (size > 0) {
await this.spawn('magick', [tmpJpg, tmpFile]);
await this.spawn('magick', [tmpJpg, path.join(cDir, itemid + '.webp')]);
coverExtracted = true;
@@ -497,6 +506,17 @@ export default new class queue {
}
} catch (err) { }
}
// If no new cover art extracted, check if cover art was already saved previously in cDir
if (!coverExtracted) {
const existingCover = path.join(cDir, itemid + '.webp');
try {
const stat = await fs.promises.stat(existingCover);
if (stat.size > 0) {
await this.spawn('magick', [existingCover, tmpFile]);
coverExtracted = true;
}
} catch (_) { }
}
// If no cover art found, use audio.webp as the thumbnail
if (!coverExtracted) {
const audioFallback = path.join(cfg.paths.s, 'img', 'audio.webp');
@@ -641,9 +661,16 @@ export default new class queue {
async genBlurredThumbnail(itemid, pending = false) {
const tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
let tDir = pending ? path.join(cfg.paths.pending, 't') : cfg.paths.t;
try { tDir = await fs.promises.realpath(tDir); } catch (_) {}
const src = path.join(tDir, `${itemid}.webp`);
const dst = path.join(tDir, `${itemid}_blur.webp`);
try {
const dstStat = await fs.promises.lstat(dst);
if (dstStat.isSymbolicLink()) {
await fs.promises.unlink(dst).catch(() => {});
}
} catch (_) {}
try {
await this.spawn('magick', [src, '-blur', '0x48', dst]);
return true;
+331 -111
View File
@@ -5,6 +5,7 @@ import { getEnableItemSlugs } from "../settings.mjs";
import { updateHallsCache } from "../halls_cache.mjs";
import queue from "../queue.mjs";
import fs from "fs";
import path from "path";
import url from "url";
const getGlobalfilter = () => {
@@ -234,7 +235,283 @@ async function checkFavoritesAccess(rawUser, { session, user_id, is_admin } = {}
return { isPrivate: true, isAllowed };
}
const buildFeedFilters = async ({
rawUser,
rawTag,
rawHall,
rawMime,
mode,
ratings,
session,
strict,
exclude,
newer,
minXdScore,
rawUserHall,
rawUserHallOwner,
rawTagger
}) => {
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
// --- title: prefix — search items.title instead of the tags table ---
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
const isTitleSearch = _decodedTag.startsWith('title:');
const titleQuery = isTitleSearch ? _decodedTag.substring(6).trim() : null;
const tag = isTitleSearch ? null : lib.parseTag(rawTag ?? null);
let hall = rawHall ?? null;
let hallObj = null;
if (hall) {
const hallData = await db`SELECT name, slug, description FROM halls WHERE slug = ${hall} LIMIT 1`;
if (hallData.length) {
hallObj = { name: hallData[0].name, slug: hallData[0].slug, description: hallData[0].description };
}
}
// User hall context
const userHallSlug = rawUserHall ?? null;
const userHallOwner = rawUserHallOwner ?? null;
let userHallObj = null;
if (userHallSlug && userHallOwner) {
const uhData = await db`
SELECT uh.id, uh.name, uh.slug, uh.description, uh.is_private, u."user" as owner_name
FROM user_halls uh
JOIN "user" u ON u.id = uh.user_id
WHERE u."user" ILIKE ${userHallOwner} AND uh.slug = ${userHallSlug}
LIMIT 1
`;
if (uhData.length) userHallObj = uhData[0];
}
const mime = rawMime ?? null;
// Support multiple MIME types (comma separated)
const mimeParts = (mime || "").split(',').filter(m => ['video', 'audio', 'image', 'flash', 'pdf'].includes(m));
const mimeSQL = mimeParts.length > 0
? db`and (${mimeParts.map(m => m === 'flash'
? (flashMimes.length > 0
? flashMimes.map(fm => db`items.mime = ${fm}`).reduce((a, b) => db`${a} or ${b}`)
: db`false`)
: (m === 'pdf' ? db`items.mime = 'application/pdf'` : db`items.mime ilike ${m + '/%'}`)).reduce((a, b) => db`${a} or ${b}`)})`
: db``;
const excludedTags = session && exclude ? (exclude || []) : [];
const newerThan = newer ? parseInt(newer) : null;
const minXd = (minXdScore && +minXdScore > 0) ? +minXdScore : 0;
const xdFilter = minXd > 0 ? db`and items.xd_score >= ${minXd}` : db``;
const strictParams = ((strict || (tag && tag.includes(','))) && tag) ? tag.split(',').map(t => lib.slugify(t)).filter(t => t) : [];
const isStrict = strictParams.length > 0;
const tagger = rawTagger ? lib.escapeLike(rawTagger) : null;
const modequery = computeBaseMode(mode, ratings, session);
let tagFilter = db``;
let titleFilter = db``;
if (isTitleSearch && titleQuery) {
titleFilter = db`and items.title ILIKE ${'%' + titleQuery + '%'} and items.title IS NOT NULL`;
} else if (tagger && tag) {
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
if (terms.length > 0) {
const conditions = terms.map(term => {
return db`and items.id in (
select ta.item_id from tags_assign ta
join tags t on t.id = ta.tag_id
join "user" u on u.id = ta.user_id
where t.normalized like '%' || slugify(${term}) || '%'
and u.user ilike ${tagger}
)`;
});
tagFilter = db`${conditions}`;
}
} else if (tag) {
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
if (terms.length > 0) {
if (isStrict) {
tagFilter = db`and items.id in (
select ta.item_id
from tags_assign ta
join tags t on t.id = ta.tag_id
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
group by ta.item_id
having count(distinct t.normalized) = ${terms.length}
)`;
} else {
const conditions = terms.map(term => {
return db`and items.id in (select ta.item_id from tags_assign ta join tags t on t.id = ta.tag_id where t.normalized like '%' || slugify(${term}) || '%')`;
});
tagFilter = db`${conditions}`;
}
}
}
let hallFilter = db``;
if (hall) {
hallFilter = db`and items.id in (select item_id from halls_assign join halls on halls.id = halls_assign.hall_id where halls.slug = ${(hall && typeof hall === 'object') ? hall.slug : hall})`;
}
let userHallFilter = db``;
if (userHallObj) {
userHallFilter = db`and items.id in (select uha.item_id from user_halls_assign uha where uha.hall_id = ${userHallObj.id})`;
}
const isOwnerOrAdmin = (session && user && typeof user === 'string' && session.user && session.user.toLowerCase() === user.toLowerCase()) || (session && (session.admin || session.is_moderator));
const visibilityFilter = isOwnerOrAdmin
? db``
: (session && session.user
? db`and (coalesce(items.visibility, 0) = 0 or lower(items.username) = ${session.user.toLowerCase()})`
: db`and coalesce(items.visibility, 0) = 0`);
return {
user,
_decodedTag,
isTitleSearch,
titleQuery,
tag,
hall,
hallObj,
userHallSlug,
userHallOwner,
userHallObj,
mime,
mimeSQL,
excludedTags,
newerThan,
minXd,
xdFilter,
strictParams,
isStrict,
tagger,
modequery,
tagFilter,
titleFilter,
hallFilter,
userHallFilter,
visibilityFilter
};
};
export default {
getItemPage: async ({
targetItemId,
targetItemPinned,
user: rawUser,
tag: rawTag,
hall: rawHall,
mime: rawMime,
mode,
ratings,
fav,
session,
limit,
strict,
newer,
exclude,
user_id,
is_admin,
userHall: rawUserHall,
userHallOwner: rawUserHallOwner,
minXdScore,
tagger: rawTagger
} = {}) => {
const numId = await resolveNumericItemId(targetItemId);
if (!numId) return 1;
let isPinned = targetItemPinned;
if (isPinned === undefined) {
const rows = await db`SELECT is_pinned FROM items WHERE id = ${numId} LIMIT 1`;
if (!rows.length) return 1;
isPinned = Boolean(rows[0].is_pinned);
} else {
isPinned = Boolean(isPinned);
}
const eps = limit ?? cfg.websrv.eps;
const filters = await buildFeedFilters({
rawUser,
rawTag,
rawHall,
rawMime,
mode,
ratings,
session,
strict,
exclude,
newer,
minXdScore,
rawUserHall,
rawUserHallOwner,
rawTagger
});
const {
user,
mimeSQL,
excludedTags,
newerThan,
xdFilter,
modequery,
tagFilter,
titleFilter,
hallFilter,
userHallFilter,
visibilityFilter
} = filters;
// Check if the target item actually matches this feed's criteria
const itemMatch = await db`
select 1 from items
${fav ? db`inner join favorites on favorites.item_id = items.id inner join "user" fav_u on fav_u.id = favorites.user_id` : db``}
where
items.id = ${numId}
and ${db.unsafe(modequery)}
and items.active = true
${visibilityFilter}
${tagFilter}
${titleFilter}
${fav ? db`and fav_u.user ilike ${user}` : db``}
${!fav && user ? db`and items.username ilike ${user}` : db``}
${mimeSQL}
${hallFilter}
${userHallFilter}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
${newerThan ? db`and items.id > ${newerThan}` : db``}
${xdFilter}
limit 1
`;
if (!itemMatch.length) {
return 1;
}
const countRows = await db`
select count(distinct items.id) as total_before
from items
${fav ? db`inner join favorites on favorites.item_id = items.id inner join "user" fav_u on fav_u.id = favorites.user_id` : db``}
where
${db.unsafe(modequery)}
and items.active = true
${visibilityFilter}
${tagFilter}
${titleFilter}
${fav ? db`and fav_u.user ilike ${user}` : db``}
${!fav && user ? db`and items.username ilike ${user}` : db``}
${mimeSQL}
${hallFilter}
${userHallFilter}
${!session && getGlobalfilter() ? db`and not exists (select 1 from tags_assign where item_id = items.id and (${db.unsafe(getGlobalfilter())}))` : db``}
${excludedTags.length > 0 ? db`and not exists (select 1 from tags_assign where item_id = items.id and tag_id = any(${excludedTags}::int[]))` : db``}
${newerThan ? db`and items.id > ${newerThan}` : db``}
${xdFilter}
and (
(items.is_pinned is true and ${isPinned} is false)
or (items.is_pinned = ${isPinned} and items.id > ${numId})
)
`;
const totalBefore = Number(countRows[0]?.total_before || 0);
return Math.floor(totalBefore / eps) + 1;
},
getf0cks: async ({ user: rawUser, tag: rawTag, hall: rawHall, mime: rawMime, page, mode, ratings, fav, session, limit, strict, newer, exclude, user_id, is_admin, random, userHall: rawUserHall, userHallOwner: rawUserHallOwner, minXdScore, tagger: rawTagger } = {}) => {
if (fav && rawUser) {
const { isPrivate, isAllowed } = await checkFavoritesAccess(rawUser, { session, user_id, is_admin });
@@ -247,121 +524,54 @@ export default {
}
}
const user = rawUser ? lib.escapeLike(decodeURI(rawUser)) : null;
const filters = await buildFeedFilters({
rawUser,
rawTag,
rawHall,
rawMime,
mode,
ratings,
session,
strict,
exclude,
newer,
minXdScore,
rawUserHall,
rawUserHallOwner,
rawTagger
});
// --- title: prefix — search items.title instead of the tags table ---
const _decodedTag = rawTag ? decodeURIComponent(rawTag) : '';
const isTitleSearch = _decodedTag.startsWith('title:');
const titleQuery = isTitleSearch ? _decodedTag.substring(6).trim() : null;
const {
user,
_decodedTag,
isTitleSearch,
titleQuery,
tag,
hall,
hallObj,
userHallSlug,
userHallOwner,
userHallObj,
mime,
mimeSQL,
excludedTags,
newerThan,
minXd,
xdFilter,
strictParams,
isStrict,
tagger,
modequery,
tagFilter,
titleFilter,
hallFilter,
userHallFilter,
visibilityFilter
} = filters;
const tag = isTitleSearch ? null : lib.parseTag(rawTag ?? null);
let hall = rawHall ?? null;
let hallObj = null;
if (hall) {
const hallData = await db`SELECT name, slug, description FROM halls WHERE slug = ${hall} LIMIT 1`;
if (hallData.length) {
hallObj = { name: hallData[0].name, slug: hallData[0].slug, description: hallData[0].description };
}
}
// User hall context
const userHallSlug = rawUserHall ?? null;
const userHallOwner = rawUserHallOwner ?? null;
let userHallObj = null;
if (userHallSlug && userHallOwner) {
const uhData = await db`
SELECT uh.id, uh.name, uh.slug, uh.description, uh.is_private, u."user" as owner_name
FROM user_halls uh
JOIN "user" u ON u.id = uh.user_id
WHERE u."user" ILIKE ${userHallOwner} AND uh.slug = ${userHallSlug}
LIMIT 1
`;
if (uhData.length) userHallObj = uhData[0];
}
const mime = rawMime ?? null;
const actPage = +(page ?? 1);
// Support multiple MIME types (comma separated)
const mimeParts = (mime || "").split(',').filter(m => ['video', 'audio', 'image', 'flash', 'pdf'].includes(m));
const mimeSQL = mimeParts.length > 0
? db`and (${mimeParts.map(m => m === 'flash'
? (flashMimes.length > 0
? flashMimes.map(fm => db`items.mime = ${fm}`).reduce((a, b) => db`${a} or ${b}`)
: db`false`)
: (m === 'pdf' ? db`items.mime = 'application/pdf'` : db`items.mime ilike ${m + '/%'}`)).reduce((a, b) => db`${a} or ${b}`)})`
: db``;
const eps = limit ?? cfg.websrv.eps;
const excludedTags = session && exclude ? (exclude || []) : [];
const newerThan = newer ? parseInt(newer) : null;
const minXd = (minXdScore && +minXdScore > 0) ? +minXdScore : 0;
// xD filter: use materialized items.xd_score column (kept live by trigger) for fast indexed lookup
const xdFilter = minXd > 0 ? db`and items.xd_score >= ${minXd}` : db``;
const strictParams = ((strict || (tag && tag.includes(','))) && tag) ? tag.split(',').map(t => lib.slugify(t)).filter(t => t) : [];
const isStrict = strictParams.length > 0;
const tagger = rawTagger ? lib.escapeLike(rawTagger) : null;
const tmp = { user, tag: isTitleSearch ? _decodedTag : tag, hall: hallObj || hall, mime, page: actPage, mode: mode, view_mode: fav ? 'favs' : 'uploads', strict: strict, userHall: userHallObj || userHallSlug, userHallOwner, tagger };
const modequery = computeBaseMode(mode, ratings, session);
let tagFilter = db``;
let titleFilter = db``;
if (isTitleSearch && titleQuery) {
// Title search: match items.title ILIKE '%query%'
titleFilter = db`and items.title ILIKE ${'%' + titleQuery + '%'} and items.title IS NOT NULL`;
} else if (tagger && tag) {
// Tagger+tag filter: items where the specific user applied this specific tag
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
if (terms.length > 0) {
const conditions = terms.map(term => {
return db`and items.id in (
select ta.item_id from tags_assign ta
join tags t on t.id = ta.tag_id
join "user" u on u.id = ta.user_id
where t.normalized like '%' || slugify(${term}) || '%'
and u.user ilike ${tagger}
)`;
});
tagFilter = db`${conditions}`;
}
} else if (tag) {
const terms = tag.split(',').map(t => t.trim()).filter(Boolean);
if (terms.length > 0) {
if (isStrict) {
tagFilter = db`and items.id in (
select ta.item_id
from tags_assign ta
join tags t on t.id = ta.tag_id
where t.normalized = ANY(ARRAY(SELECT slugify(x) FROM unnest(${terms}::text[]) AS x))
group by ta.item_id
having count(distinct t.normalized) = ${terms.length}
)`;
} else {
// Non-strict intersection Logic (AND for partials)
// For each term, ensure there is AT LEAST one matching tag assigned to the item
const conditions = terms.map(term => {
return db`and items.id in (select ta.item_id from tags_assign ta join tags t on t.id = ta.tag_id where t.normalized like '%' || slugify(${term}) || '%')`;
});
tagFilter = db`${conditions}`;
}
}
}
let hallFilter = db``;
if (hall) {
hallFilter = db`and items.id in (select item_id from halls_assign join halls on halls.id = halls_assign.hall_id where halls.slug = ${(hall && typeof hall === 'object') ? hall.slug : hall})`;
}
let userHallFilter = db``;
if (userHallObj) {
userHallFilter = db`and items.id in (select uha.item_id from user_halls_assign uha where uha.hall_id = ${userHallObj.id})`;
}
const isOwnerOrAdmin = (session && user && typeof user === 'string' && session.user && session.user.toLowerCase() === user.toLowerCase()) || (session && (session.admin || session.is_moderator));
const visibilityFilter = isOwnerOrAdmin
? db``
: (session && session.user
? db`and (coalesce(items.visibility, 0) = 0 or lower(items.username) = ${session.user.toLowerCase()})`
: db`and coalesce(items.visibility, 0) = 0`);
const cacheKey = buildCountCacheKey({ modequery, tag, user, hall, mime, fav, session, excludedTags, newerThan, minXd, userHallObj, tagger });
let total = getCachedCount(cacheKey);
@@ -926,7 +1136,17 @@ export default {
// Efficient coverart fallback
const coverartUrl = actitem.has_coverart
let hasCoverart = actitem.has_coverart;
if (!hasCoverart && actitem.mime?.startsWith('audio/')) {
const caPath = path.join(cfg.paths.ca, `${actitem.id}.webp`);
try {
if (fs.existsSync(caPath)) {
hasCoverart = true;
db`UPDATE items SET has_coverart = TRUE WHERE id = ${actitem.id}`.catch(() => {});
}
} catch (_) {}
}
const coverartUrl = hasCoverart
? `${cfg.websrv.paths.coverarts}/${actitem.id}.webp`
: `/s/img/music.webp`;
+17 -4
View File
@@ -1302,7 +1302,7 @@ export default router => {
if (!itemid) return res.json({ success: false, msg: 'No itemid provided' }, 400);
const rows = await db`
SELECT id, dest, mime, username
SELECT id, dest, mime, username, src
FROM items
WHERE id = ${itemid} AND active = true AND is_deleted = false
LIMIT 1
@@ -1314,13 +1314,26 @@ export default router => {
const isAdmin = !!(req.session.admin || req.session.is_moderator);
if (!isOwner && !isAdmin) return res.json({ success: false, msg: 'Unauthorized' }, 403);
const ok = await queue.genThumbnail(item.dest, item.mime, item.id, '', false);
const ok = await queue.genThumbnail(item.dest, item.mime, item.id, item.src || '', false);
await queue.genBlurredThumbnail(item.id, false);
let hasCoverart = false;
if (item.mime && item.mime.startsWith('audio/')) {
const caFile = path.join(cfg.paths.ca, `${item.id}.webp`);
hasCoverart = !!queue._lastCoverExtracted;
if (!hasCoverart) {
try {
const stat = await fs.stat(caFile);
hasCoverart = stat.size > 0;
} catch (_) {}
}
await db`UPDATE items SET has_coverart = ${hasCoverart} WHERE id = ${item.id}`;
}
if (ok) {
db.notify('rethumb', JSON.stringify({ item_id: item.id })).catch(() => {});
db.notify('rethumb', JSON.stringify({ item_id: item.id, has_coverart: hasCoverart })).catch(() => {});
audit.log(req.session.id, 'rethumb_item', 'item', item.id, {}).catch(() => {});
return res.json({ success: true, itemid: item.id });
return res.json({ success: true, itemid: item.id, has_coverart: hasCoverart });
} else {
return res.json({ success: false, msg: 'Thumbnail regeneration failed' }, 500);
}
+99
View File
@@ -408,6 +408,105 @@ export default (router, tpl) => {
return res.reply({ body });
}
const isOnara = !!(cfg.onara !== undefined ? cfg.onara : cfg.websrv?.onara);
if (mode === 'item' && isOnara) {
const itemHtml = tpl.render('ajax-item', data, req);
const isRandom = req.cookies.random_mode === '1';
let itemPage = 1;
if (req.params.page) {
itemPage = parseInt(req.params.page, 10) || 1;
} else if (!isRandom && data.item) {
try {
itemPage = await f0cklib.getItemPage({
targetItemId: data.item.id,
targetItemPinned: data.item.is_pinned,
user: req.params.user,
tag: reqTag,
mime: req.cookies.mime !== undefined ? req.cookies.mime : (req.query?.mime || req.url.qs?.mime || req.params.mime || null),
hall: req.params.hall,
fav: req.params.mode == 'favs',
mode: req.mode,
ratings: (() => { if (req.mode === 2 || req.mode === 3) return null; const r = req.cookies.ratings; return r ? decodeURIComponent(r).split(/[|,]/).filter(x => ['sfw','nsfw','nsfl','untagged'].includes(x)) : null; })(),
session: req.session,
user_id: req.session?.id,
is_admin: req.session?.admin,
exclude: req.session ? (req.session.excluded_tags || []) : [],
strict: !!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode),
minXdScore: req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0),
tagger: req.url.qs?.tagger || null
});
} catch (err) {
console.error('[ONARA] getItemPage error:', err);
itemPage = 1;
}
}
let gridData = await f0cklib.getf0cks({
user: req.params.user,
tag: reqTag,
mime: req.cookies.mime !== undefined ? req.cookies.mime : (req.query?.mime || req.url.qs?.mime || req.params.mime || null),
page: itemPage,
hall: req.params.hall,
fav: req.params.mode == 'favs',
mode: req.mode,
ratings: (() => { if (req.mode === 2 || req.mode === 3) return null; const r = req.cookies.ratings; return r ? decodeURIComponent(r).split(/[|,]/).filter(x => ['sfw','nsfw','nsfl','untagged'].includes(x)) : null; })(),
session: req.session,
user_id: req.session?.id,
is_admin: req.session?.admin,
exclude: req.session ? (req.session.excluded_tags || []) : [],
url: decodeURIComponent(req.url.pathname || req.url),
strict: !!(req.query?.strict || req.url.qs?.strict || req.session?.strict_mode),
explicitStrict: !!(req.query?.strict || req.url.qs?.strict),
random: isRandom,
minXdScore: req.url.qs?.min_xd !== undefined ? +req.url.qs.min_xd : (req.session?.min_xd_score || 0),
lang: req.lang,
tagger: req.url.qs?.tagger || null
});
if (!gridData || !gridData.success) {
gridData = {
items: [],
pagination: { start: 1, end: 1, current: 1, page: 1, cheat: [1], prev: null, next: null },
total: 0,
success: true,
link: { main: '/', path: 'p/', suffix: '' },
tmp: {}
};
}
const activeId = data.item ? data.item.id : req.params.itemid;
const activeSlug = data.item ? data.item.slug : null;
// Mark the active item in the grid for instant highlight
let foundInGrid = false;
if (Array.isArray(gridData.items)) {
for (const it of gridData.items) {
if ((activeSlug && it.slug === activeSlug) || (it.id === activeId)) {
it.is_onara_active = true;
foundInGrid = true;
} else {
it.is_onara_active = false;
}
}
// If not in first page of feed, unshift it so thumbnail is present in background
if (!foundInGrid && data.item) {
gridData.items.unshift({ ...data.item, is_onara_active: true });
}
}
gridData.session = data.session;
gridData.is_onara_item = true;
gridData.onara_item_html = itemHtml;
gridData.item = data.item;
gridData.page_meta = data.page_meta;
gridData.hidePagination = false;
const body = tpl.render('index', gridData, req);
console.log(`[${new Date().toISOString()}] [ROUTE] Render complete (onara direct item) in ${Date.now() - tRenderStart}ms. Total route time: ${Date.now() - tRouteStart}ms`);
return res.reply({ body });
}
const body = tpl.render(mode, data, req);
console.log(`[${new Date().toISOString()}] [ROUTE] Render complete in ${Date.now() - tRenderStart}ms. Total route time: ${Date.now() - tRouteStart}ms`);
+4
View File
@@ -1463,6 +1463,8 @@ process.on('uncaughtException', err => {
default_font: cfg.websrv.default_font || "",
site_description: cfg.websrv.description || "The webs dumpster",
enable_nsfl: !!cfg.enable_nsfl,
onara: !!(cfg.onara !== undefined ? cfg.onara : cfg.websrv?.onara),
is_onara_item: false,
enable_private_uploads: cfg.enable_private_uploads !== false,
get enable_expiring_uploads() { return getEnableExpiringUploads(); },
get enable_item_slugs() { return getEnableItemSlugs(); },
@@ -1588,6 +1590,8 @@ process.on('uncaughtException', err => {
globals.t = perRequestT;
globals.lang = perRequestLang;
globals.recaptcha_enabled = perRequestRecaptcha;
globals.is_onara_item = !!(data && data.is_onara_item);
globals.onara_item_html = (data && data.onara_item_html) || '';
// Resolve per-request infobox preference
const activeReq = req || effectiveReq;
+2 -1
View File
@@ -124,7 +124,8 @@ export const handleRethumbUpload = async (req, res, itemId) => {
}
// Generate the thumbnail
const tDir = item.active ? cfg.paths.t : path.join(cfg.paths.pending, 't');
let tDir = item.active ? cfg.paths.t : path.join(cfg.paths.pending, 't');
try { tDir = await fs.realpath(tDir); } catch (_) {}
const finalPath = path.join(tDir, `${item.id}.webp`);
const thumbSpec = '512x512';
+1 -1
View File
@@ -3,7 +3,7 @@
@include(snippets/page-title)
<div class="posts" data-current-page="{{ pagination.current }}" data-has-more="{{ pagination.next ? 'true' : 'false' }}">
@each(items as item)
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
<a href="{{ link.main }}{{ (enable_item_slugs && item.slug) ? item.slug : item.id }}" class="{{ item.is_pinned ? 'anim-boxshadow ' : '' }}thumb lazy-thumb {{ item.has_notification ? 'has-notif' : '' }} {{ item.is_pinned ? 'is-pinned' : '' }} {{ item.is_onara_active ? 'onara-active' : '' }}" data-file="{{ item.dest }}" data-mime="{{ item.mime }}" data-user="{!! item.display_name || item.username !!}" data-ext="{{ item.mime.split('/')[1].replace('youtube', 'yt').replace('x-shockwave-flash', 'flash').replace('vnd.adobe.flash.movie', 'flash').toUpperCase() }}" data-mode="{{ item.tag_id == nsfl_tag_id ? 'nsfl' : (item.tag_id == 2 ? 'nsfw' : (item.tag_id == 1 ? 'sfw' : 'null')) }}" data-bg="/t/{{ item.id }}.webp" data-size="{{ enable_dynamic_thumbs ? (item.thumb_size || 1) : 1 }}">
<div class="thumb-indicators">
@if(item.is_pinned)
<i class="fa-solid fa-thumbtack pin-indicator anim"></i>
+16 -2
View File
@@ -82,7 +82,7 @@
</div>
</div>
<div id="warning-modal" class="modal-overlay" style="display:none; z-index: 10000;">
<div id="warning-modal" class="modal-overlay" style="display:none; z-index: 100100;">
<div class="modal-content content-warning-content">
<h3 style="color: var(--danger);">{{ t('account_warning.title') }}</h3>
<p>{{ t('account_warning.text') }}</p>
@@ -99,6 +99,18 @@
</div>
</div>
@endif
@if(onara)
<div id="onara-modal" class="onara-modal-wrapper" @if(is_onara_item)style="display:flex;" aria-hidden="false"@else style="display:none;" aria-hidden="true"@endif>
<div class="onara-backdrop" id="onara-backdrop"></div>
<div class="onara-modal-content" id="onara-modal-content">
<div id="onara-item-mount" class="item-view">
@if(is_onara_item)
{{ onara_item_html }}
@endif
</div>
</div>
</div>
@endif
@if(session)
@include(snippets/metadata-modal)
@endif
@@ -127,7 +139,7 @@
@endif
@if(private_society && !session)
<script>
window.f0ckSession = { logged_in: false, hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }}, development: @if(development) true @else false @endif, allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif, hide_sidebar_default: @if(hide_sidebar_default) true @else false @endif };
window.f0ckSession = { logged_in: false, onara: @if(onara) true @else false @endif, enable_item_slugs: @if(enable_item_slugs) true @else false @endif, hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif, enable_xd_score: @if(enable_xd_score) true @else false @endif, default_theme: "{{ default_theme }}", show_content_warning: @if(show_content_warning) true @else false @endif, use_new_layout: @if(default_layout === 'legacy')false @else true @endif, comment_display_mode: {{ comment_display_mode }}, comment_max_length: {{ comment_max_length !== null && comment_max_length !== undefined ? comment_max_length : 'null' }}, development: @if(development) true @else false @endif, allow_comment_deletion: @if(allow_comment_deletion) true @else false @endif, hide_sidebar_default: @if(hide_sidebar_default) true @else false @endif };
window.f0ckDebug = window.f0ckSession.development ? console.log.bind(console) : () => {};
(() => {
const loginModal = document.getElementById('login-modal');
@@ -365,6 +377,8 @@
@endif
<script>
window.f0ckSession = {
onara: @if(onara) true @else false @endif,
enable_item_slugs: @if(enable_item_slugs) true @else false @endif,
strict_mode: @if(session && session.strict_mode) true @else false @endif,
logged_in: @if(session) true @else false @endif,
hide_comments_from_public: @if(hide_comments_from_public) true @else false @endif,
+2 -2
View File
@@ -97,7 +97,7 @@
@endif
</head>
<body class="@if(session)@if(session.use_new_layout)layout-modern@else layout-legacy @endif@else @if(default_layout === 'legacy')layout-legacy@else layout-modern@endif @endif @if(private_society && !session)private-gate-active@endif">
<body class="@if(session)@if(session.use_new_layout)layout-modern@else layout-legacy @endif@else @if(default_layout === 'legacy')layout-legacy@else layout-modern@endif @endif @if(private_society && !session)private-gate-active@endif @if(is_onara_item) onara-modal-open @endif">
<script>var sRH = localStorage.getItem('sidebarRightHidden'); if(sRH==='true' || (sRH===null && @if(hide_sidebar_default) true @else false @endif)) document.body.classList.add('sidebar-right-hidden'); if(localStorage.getItem('comments_hidden')==='true')document.body.classList.add('sidebar-left-hidden');(function(){var d=window.devicePixelRatio||1;console.log('[DPR] devicePixelRatio='+d);var t='dpr-1x';if(d>=1.55)t='dpr-high';else if(d>=1.39)t='dpr-150';else if(d>=1.26)t='dpr-130';else if(d>=1.20)t='dpr-120';else if(d>=1.05)t='dpr-110';document.documentElement.setAttribute('data-dpr',t);})();</script>
@if(!private_society || session)
<canvas class="hidden-xs" id="bg"></canvas>
@@ -117,7 +117,7 @@
overflow: hidden !important;
}
</style>
<div id="force-password-modal" class="modal-overlay" style="display: flex; align-items: center; justify-content: center; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 10000; background: #000;">
<div id="force-password-modal" class="modal-overlay" style="display: flex; align-items: center; justify-content: center; position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 100100; background: #000;">
<div class="modal-content" style="background: #1a1a1a; padding: 40px; border-radius: 12px; width: 100%; max-width: 500px; box-shadow: 0 10px 40px rgba(0,0,0,0.5); border: 1px solid rgba(0, 150, 255, 0.3);">
<h2 style="margin-top: 0; color: #fff; font-weight: 800; letter-spacing: -0.5px;">{{ t('auth.password_change_required') }}</h2>
<p style="color: #ccc; line-height: 1.6;">{{ t('auth.password_change_desc') }}</p>