This commit is contained in:
2026-09-11 15:49:06 +02:00
parent 118eda1de0
commit d0bae284ec
11 changed files with 394 additions and 193 deletions
+142 -86
View File
@@ -3226,7 +3226,12 @@ body.sidebar-right-hidden #sidebar-drag-zone {
#main:fullscreen .item-layout-container .item-main-content>.item_title, #main:fullscreen .item-layout-container .item-main-content>.item_title,
#main:fullscreen .item-layout-container .item-main-content>.content, #main:fullscreen .item-layout-container .item-main-content>.content,
#main:fullscreen .item-layout-container .item-main-content>.metadata, #main:fullscreen .item-layout-container .item-main-content>.metadata,
#main:fullscreen .item-layout-container .item-main-content #comments-container { #main:fullscreen .item-layout-container .item-main-content #comments-container,
#onara-modal:fullscreen .item-layout-container .item-main-content>._204863,
#onara-modal:fullscreen .item-layout-container .item-main-content>.item_title,
#onara-modal:fullscreen .item-layout-container .item-main-content>.content,
#onara-modal:fullscreen .item-layout-container .item-main-content>.metadata,
#onara-modal:fullscreen .item-layout-container .item-main-content #comments-container {
z-index: auto; z-index: auto;
} }
@@ -5060,10 +5065,25 @@ html[res="fullscreen"] span#favs {
} }
/* Linux Firefox "thin" is often too small and hard to see. Use auto there. */ /* Linux Firefox "thin" is often too small and hard to see. Use auto there. */
html.is-linux.is-firefox * { html.is-linux.is-firefox *:not(html):not(body) {
scrollbar-width: auto; scrollbar-width: auto;
} }
/* Strictly hide root body and html scrollbar everywhere - eliminates gutter spacing */
html,
body {
scrollbar-gutter: auto !important;
scrollbar-width: none !important;
-ms-overflow-style: none !important;
}
html::-webkit-scrollbar,
body::-webkit-scrollbar {
display: none !important;
width: 0 !important;
height: 0 !important;
}
*, *,
::before, ::before,
::after { ::after {
@@ -5092,12 +5112,16 @@ body {
line-height: 2; line-height: 2;
overflow-x: hidden; overflow-x: hidden;
font-size: medium; font-size: medium;
scrollbar-width: none !important;
-ms-overflow-style: none !important;
} }
html { html {
width: 100%; width: 100%;
height: 100%; height: 100%;
scrollbar-gutter: stable; scrollbar-gutter: auto !important;
scrollbar-width: none !important;
-ms-overflow-style: none !important;
} }
@@ -5877,15 +5901,56 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
padding-left: 5px; padding-left: 5px;
} */ } */
/* ===== Viewport Locking for Index/Feed Page Layout ===== */
html:has(.index-container),
body:has(.index-container) {
height: 100vh !important;
height: 100dvh !important;
max-height: 100vh !important;
max-height: 100dvh !important;
overflow: hidden !important;
}
.pagewrapper:has(.index-container),
body:has(.index-container) > .pagewrapper {
height: calc(100vh - var(--navbar-h, 50px)) !important;
height: calc(100dvh - var(--navbar-h, 50px)) !important;
max-height: calc(100vh - var(--navbar-h, 50px)) !important;
max-height: calc(100dvh - var(--navbar-h, 50px)) !important;
overflow: hidden !important;
padding: 0 !important;
display: flex !important;
flex-direction: column !important;
}
#main:has(.index-container),
body:has(.index-container) > .pagewrapper > #main {
height: 100% !important;
max-height: 100% !important;
overflow: hidden !important;
display: flex !important;
flex-direction: column !important;
flex: 1 1 auto !important;
min-height: 0 !important;
}
/* Index page layout with sidebar */ /* Index page layout with sidebar */
.index-layout-wrapper { .index-layout-wrapper {
display: flex; display: flex;
width: 100%; width: 100%;
height: calc(100vh - var(--navbar-h, 50px)) !important;
height: calc(100dvh - var(--navbar-h, 50px)) !important;
max-height: calc(100vh - var(--navbar-h, 50px)) !important;
max-height: calc(100dvh - var(--navbar-h, 50px)) !important;
min-height: 0 !important;
flex: 1 1 auto !important;
overflow: hidden !important;
box-sizing: border-box;
gap: 0; gap: 0;
} }
.index-layout-wrapper .index-container { .index-layout-wrapper .index-container {
flex: 1; flex: 1 1 auto;
min-width: 0; min-width: 0;
} }
@@ -5909,8 +5974,19 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
.index-container { .index-container {
width: 100%; width: 100%;
padding: 5px; height: calc(100vh - var(--navbar-h, 50px)) !important;
margin: 0px 20px 0px 20px; height: calc(100dvh - var(--navbar-h, 50px)) !important;
max-height: calc(100vh - var(--navbar-h, 50px)) !important;
max-height: calc(100dvh - var(--navbar-h, 50px)) !important;
min-height: 0 !important;
flex: 1 1 auto !important;
overflow-y: auto !important;
overflow-x: hidden !important;
-webkit-overflow-scrolling: touch;
overscroll-behavior-y: contain;
box-sizing: border-box;
padding: 5px 20px 75px 20px;
margin: 0;
} }
@media (min-width: 361px) { @media (min-width: 361px) {
@@ -6767,8 +6843,9 @@ a.removetag i {
@media (max-width: 600px) { @media (max-width: 600px) {
.index-container { .index-container {
margin-left: 0; padding-left: 5px;
margin-right: 0; padding-right: 5px;
margin: 0;
} }
} }
@@ -11181,6 +11258,7 @@ html[theme="orange"] .image-brand {
text-align: left; text-align: left;
max-width: 100vw; max-width: 100vw;
overflow-x: clip; overflow-x: clip;
scrollbar-width: none !important;
} }
} }
@@ -11194,12 +11272,6 @@ html[theme="orange"] .image-brand {
.navbar {
border-bottom: none;
/* padding: 5px; */
/* testing if it works good */
}
.image-brand { .image-brand {
width: 4cm; width: 4cm;
} }
@@ -11248,35 +11320,7 @@ html[theme="orange"] .image-brand {
visibility: hidden; visibility: hidden;
} }
@media (max-width: 1056px) { /* Navbar grid layout removed for modern-navbar compatibility */
.navigation-links {
display: grid;
grid-row: 1;
grid-column: 2;
}
.navbar-brand {
grid-row: 1;
grid-column: 1;
}
#navbarSupportedContent {
grid-row: 1;
grid-column: 3;
}
.navbar {
grid-template-columns: 0fr 1fr auto !important;
grid-template-rows: 1fr;
}
.navbar-nav {
display: grid !important;
grid-template-rows: auto;
grid-template-columns: auto auto 1fr;
grid-gap: 0.25em;
}
}
.bg-black { .bg-black {
background: #0000008a !important; background: #0000008a !important;
@@ -11321,48 +11365,40 @@ div.sbt {
width: 100%; width: 100%;
} }
.navigation-links {
display: grid;
grid-row: 1;
grid-column: 2;
grid-template-columns: auto auto 1fr;
}
.navigation-links-guest,
ol {
margin: 5px;
margin-block-start: 0;
margin-block-end: 0;
padding-inline-start: 0;
}
/* === VIEW-SPECIFIC STYLES === */ /* === VIEW-SPECIFIC STYLES === */
/* === NAVBAR STYLES === */ /* === NAVBAR STYLES === */
/* ── Desktop layout: single flex row ── */ /* ── Always-visible sticky navbar ── */
.navbar { .navbar,
position: sticky; body > nav.navbar {
top: 0; position: sticky !important;
z-index: 1005; top: 0 !important;
width: 100%; z-index: 1005 !important;
display: flex; width: 100% !important;
flex-direction: row; display: flex !important;
align-items: center; flex-direction: row !important;
flex-wrap: wrap; align-items: center !important;
/* allows motd-container to wrap to its own full-width row */ flex-wrap: wrap !important;
flex-shrink: 0 !important;
min-height: 42px !important;
box-sizing: border-box !important;
font-family: var(--font); font-family: var(--font);
border-bottom: 1px solid var(--nav-border-color); border-bottom: 1px solid var(--nav-border-color);
background: var(--nav-bg); background: var(--nav-bg) !important;
visibility: visible !important;
opacity: 1 !important;
} }
/* Brand: always visible, flush left */ /* Brand: always visible, flush left */
.navbar-brand { .navbar-brand {
flex-shrink: 0; flex-shrink: 0 !important;
display: flex; display: flex !important;
align-items: center; align-items: center !important;
height: 40px; height: 40px !important;
max-width: 180px; max-width: 180px;
visibility: visible !important;
opacity: 1 !important;
} }
/* Nav collapse: fills middle, always visible on desktop */ /* Nav collapse: fills middle, always visible on desktop */
@@ -12619,12 +12655,6 @@ div.posts>a::after {
} }
} }
.navbar {
background: rgba(0, 0, 0, 0.2);
border-bottom: 1px solid var(--nav-bg);
box-shadow: 1px 1px 1px #000;
}
body.layout-modern .navbar-header, body.layout-modern .navbar-header,
body.layout-legacy .navbar-header { body.layout-legacy .navbar-header {
background: transparent; background: transparent;
@@ -12647,8 +12677,8 @@ body.layout-legacy .navbar-header {
overflow-y: auto; overflow-y: auto;
} }
/* Viewport lock to enable full app view and prevent scrollbars */ /* Viewport lock to enable full app view and prevent scrollbars (standalone item view only) */
body.layout-modern:has(.item-layout-container) { body.layout-modern:not(.onara-modal-open):has(> .pagewrapper .item-layout-container) {
overflow: hidden; overflow: hidden;
height: 100vh; height: 100vh;
} }
@@ -12665,15 +12695,15 @@ body.layout-legacy .navbar-header {
/* overridden to 300px by sidebar rule below for generic pages */ /* overridden to 300px by sidebar rule below for generic pages */
} }
body.layout-modern:has(.item-layout-container)>.pagewrapper { body.layout-modern:not(.onara-modal-open):has(> .pagewrapper .item-layout-container)>.pagewrapper {
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
height: calc(100vh - var(--navbar-h, 50px)); height: calc(100vh - var(--navbar-h, 50px));
} }
body.layout-modern:has(.item-layout-container)>.pagewrapper>#main, body.layout-modern:not(.onara-modal-open):has(> .pagewrapper .item-layout-container)>.pagewrapper>#main,
body.layout-modern:has(.item-layout-container)>#main { body.layout-modern:not(.onara-modal-open):has(> .pagewrapper .item-layout-container)>#main {
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
height: 100%; height: 100%;
@@ -18701,7 +18731,12 @@ a.lazy-thumb.filtered-upload-ghost:hover::after {
/* Prevent scrolling on background document when Onara modal is active */ /* Prevent scrolling on background document when Onara modal is active */
body.onara-modal-open { body.onara-modal-open {
overflow: hidden !important; overflow: hidden !important;
scrollbar-gutter: stable !important; }
/* Override html's scrollbar-gutter: stable so the page fills full viewport width */
html:has(body.onara-modal-open) {
overflow: hidden !important;
scrollbar-gutter: auto !important;
} }
/* Background main page elements (outside posts) are dimmed and softly blurred */ /* Background main page elements (outside posts) are dimmed and softly blurred */
@@ -18756,9 +18791,16 @@ body.onara-modal-open nav.navbar {
z-index: 10060 !important; z-index: 10060 !important;
position: sticky !important; position: sticky !important;
top: 0 !important; top: 0 !important;
left: 0 !important;
width: 100% !important; width: 100% !important;
} }
/* Ensure hamburger dropdown is interactive and above the Onara modal */
body.onara-modal-open nav.navbar .nav-collapse {
z-index: 10065 !important;
pointer-events: auto !important;
}
body.onara-modal-open .global-sidebar-right { body.onara-modal-open .global-sidebar-right {
opacity: 1 !important; opacity: 1 !important;
filter: none !important; filter: none !important;
@@ -18984,3 +19026,17 @@ body.onara-modal-open #rethumb-capture-modal,
body.onara-modal-open #gchat-img-modal { body.onara-modal-open #gchat-img-modal {
z-index: 100100 !important; z-index: 100100 !important;
} }
#onara-modal:fullscreen {
overflow: hidden !important;
background: #000 !important;
}
#onara-modal:fullscreen .onara-backdrop {
display: none !important;
}
#onara-modal:fullscreen .onara-modal-content {
overflow: hidden !important;
padding: 0 !important;
}
+12 -11
View File
@@ -14,15 +14,10 @@
display: block; display: block;
} }
.v0ck.v0ck_fullscreen { .v0ck:fullscreen,
max-width: none; .v0ck.v0ck_fullscreen,
max-height: none; #main:fullscreen .v0ck.v0ck_fullscreen,
width: 100%; #onara-modal:fullscreen .v0ck.v0ck_fullscreen {
height: 100%;
background-color: black;
}
#main:fullscreen .v0ck.v0ck_fullscreen {
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
@@ -39,7 +34,10 @@
justify-content: center; justify-content: center;
} }
#main:fullscreen .v0ck.v0ck_fullscreen video { .v0ck:fullscreen video,
.v0ck.v0ck_fullscreen video,
#main:fullscreen .v0ck.v0ck_fullscreen video,
#onara-modal:fullscreen .v0ck.v0ck_fullscreen video {
width: 100%; width: 100%;
height: 100%; height: 100%;
object-fit: contain; object-fit: contain;
@@ -47,7 +45,10 @@
} }
/* Audio in fullscreen: hide the invisible audio element, show cover art via background */ /* Audio in fullscreen: hide the invisible audio element, show cover art via background */
#main:fullscreen .v0ck.v0ck_fullscreen audio { .v0ck:fullscreen audio,
.v0ck.v0ck_fullscreen audio,
#main:fullscreen .v0ck.v0ck_fullscreen audio,
#onara-modal:fullscreen .v0ck.v0ck_fullscreen audio {
display: none; display: none;
} }
+202 -58
View File
@@ -118,10 +118,11 @@ window.cancelAnimFrame = (function () {
}; };
const ensureLayoutClass = () => { const ensureLayoutClass = () => {
if (window.f0ckSession && window.f0ckSession.use_new_layout === false) { const isLegacy = window.f0ckSession ? !window.f0ckSession.use_new_layout : (window.f0ckDefaultLayout === 'legacy');
document.body.classList.remove('layout-modern'); document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy');
if (isLegacy) {
document.body.classList.add('layout-legacy'); document.body.classList.add('layout-legacy');
} else if (!document.body.classList.contains('layout-legacy')) { } else {
document.body.classList.add('layout-modern'); document.body.classList.add('layout-modern');
} }
}; };
@@ -1196,18 +1197,8 @@ window.cancelAnimFrame = (function () {
window.closeOnaraModal = closeOnaraModal; window.closeOnaraModal = closeOnaraModal;
window.isOnaraActive = isOnaraActive; window.isOnaraActive = isOnaraActive;
// Onara modal close click delegation: clicking on empty/free space closes the modal // Content selector for protected elements inside onara modal
document.addEventListener('click', (e) => { const _onaraContentSelector = [
if (!document.body.classList.contains('onara-modal-open')) return;
// Do not close if clicking auxiliary modals, dropdowns, tooltips, or emoji pickers
if (e.target.closest('.modal-overlay:not(#onara-modal), .modal-content, .swal2-container, .tippy-box, #comments-container .emoji-picker, .dropdown-menu, .custom-confirm-overlay')) return;
// Do not close if clicking the global sidebar right or navbar
if (e.target.closest('.global-sidebar-right, nav.navbar, #sidebar-drag-zone')) return;
// Do not close if clicking interactive content elements or actual item components
const contentElementSelector = [
'.media-object', '.media-object',
'.previous-post', '.previous-post',
'.next-post', '.next-post',
@@ -1247,7 +1238,41 @@ window.cancelAnimFrame = (function () {
'i[class*="fa-"]' 'i[class*="fa-"]'
].join(', '); ].join(', ');
if (e.target.closest(contentElementSelector)) return; let _onaraMouseDownInside = false;
document.addEventListener('mousedown', (e) => {
if (!document.body.classList.contains('onara-modal-open')) return;
if (e.target.closest('.modal-overlay:not(#onara-modal), .modal-content, .swal2-container, .tippy-box, #comments-container .emoji-picker, .dropdown-menu, .custom-confirm-overlay') ||
e.target.closest('.global-sidebar-right, nav.navbar, #sidebar-drag-zone') ||
e.target.closest(_onaraContentSelector)) {
_onaraMouseDownInside = true;
} else {
_onaraMouseDownInside = false;
}
}, { passive: true });
// Onara modal close click delegation: clicking on empty/free space closes the modal
document.addEventListener('click', (e) => {
if (!document.body.classList.contains('onara-modal-open')) return;
// If mouse press originated on a protected element or if text is currently selected, do not close
if (_onaraMouseDownInside) {
_onaraMouseDownInside = false;
return;
}
const sel = window.getSelection();
if (sel && sel.toString().trim().length > 0) {
return;
}
// Do not close if clicking auxiliary modals, dropdowns, tooltips, or emoji pickers
if (e.target.closest('.modal-overlay:not(#onara-modal), .modal-content, .swal2-container, .tippy-box, #comments-container .emoji-picker, .dropdown-menu, .custom-confirm-overlay')) return;
// Do not close if clicking the global sidebar right or navbar
if (e.target.closest('.global-sidebar-right, nav.navbar, #sidebar-drag-zone')) return;
// Do not close if clicking interactive content elements or actual item components
if (e.target.closest(_onaraContentSelector)) return;
// Any click on free space (backdrop, modal wrapper, container background, empty space in item-main-content) closes // Any click on free space (backdrop, modal wrapper, container background, empty space in item-main-content) closes
if (e.target.closest('#onara-modal') || e.target.id === 'onara-backdrop') { if (e.target.closest('#onara-modal') || e.target.id === 'onara-backdrop') {
@@ -1728,6 +1753,61 @@ window.cancelAnimFrame = (function () {
} }
const _preloadedMediaUrls = new Set();
const preloadMediaFromHtml = (html) => {
if (!html) return;
try {
const parser = new DOMParser();
const doc = parser.parseFromString(html, 'text/html');
// 1. Preload Image
const img = doc.querySelector('#f0ck-image, .embed-responsive-image img, .media-object img');
if (img) {
const src = img.getAttribute('src');
if (src && !_preloadedMediaUrls.has(src)) {
_preloadedMediaUrls.add(src);
const preloadImg = new Image();
preloadImg.decoding = 'async';
preloadImg.src = src;
window.f0ckDebug('[prefetch] Preloaded image asset:', src);
}
}
// 2. Preload Audio Cover Art
const cover = doc.querySelector('#f0ck-audio-cover');
if (cover) {
const coverSrc = cover.getAttribute('src');
if (coverSrc && coverSrc !== '/s/img/200.gif' && !_preloadedMediaUrls.has(coverSrc)) {
_preloadedMediaUrls.add(coverSrc);
const preloadCover = new Image();
preloadCover.src = coverSrc;
}
}
// 3. Preload Video / Audio Media (fetch initial bytes into browser cache)
const mediaEl = doc.querySelector('video#my-video, audio#my-video, .media-object video, .media-object audio');
if (mediaEl) {
const mediaSrc = mediaEl.getAttribute('src');
if (mediaSrc && !mediaSrc.startsWith('blob:') && !_preloadedMediaUrls.has(mediaSrc)) {
_preloadedMediaUrls.add(mediaSrc);
fetch(mediaSrc, {
headers: { Range: 'bytes=0-2097151' }, // 2MB initial chunk preload
credentials: 'include',
priority: 'low'
}).catch(() => {});
window.f0ckDebug('[prefetch] Preloaded media buffer:', mediaSrc);
}
}
if (_preloadedMediaUrls.size > 150) {
_preloadedMediaUrls.clear();
}
} catch (e) {
console.error('[prefetch] Error preloading media from html:', e);
}
};
const prefetchNextPrevItems = () => { const prefetchNextPrevItems = () => {
const nextBtn = document.getElementById('next'); const nextBtn = document.getElementById('next');
const prevBtn = document.getElementById('prev'); const prevBtn = document.getElementById('prev');
@@ -1769,7 +1849,11 @@ window.cancelAnimFrame = (function () {
ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + params.toString(); ajaxUrl += (ajaxUrl.includes('?') ? '&' : '?') + params.toString();
} }
if (itemCacheMap.has(ajaxUrl)) return; if (itemCacheMap.has(ajaxUrl)) {
const cached = itemCacheMap.get(ajaxUrl);
if (cached && cached.html) preloadMediaFromHtml(cached.html);
return;
}
fetch(ajaxUrl, { credentials: 'include', priority: 'low' }) fetch(ajaxUrl, { credentials: 'include', priority: 'low' })
.then(r => r.ok ? r.text() : null) .then(r => r.ok ? r.text() : null)
@@ -1787,6 +1871,7 @@ window.cancelAnimFrame = (function () {
itemCacheMap.delete(itemCacheMap.keys().next().value); itemCacheMap.delete(itemCacheMap.keys().next().value);
} }
window.f0ckDebug('[prefetch] Cached AJAX template for item', itemId); window.f0ckDebug('[prefetch] Cached AJAX template for item', itemId);
preloadMediaFromHtml(html);
} }
}) })
.catch(() => {}); .catch(() => {});
@@ -2745,7 +2830,8 @@ window.cancelAnimFrame = (function () {
window.dispatchEvent(new Event('pjax:start')); window.dispatchEvent(new Event('pjax:start'));
const currentScroll = window.scrollY; const currentContainer = document.querySelector('.index-container');
const currentScroll = currentContainer ? currentContainer.scrollTop : window.scrollY;
// Save scroll position for the current page before we leave it (only for new navigations) // Save scroll position for the current page before we leave it (only for new navigations)
if (!options.skipPush) { if (!options.skipPush) {
@@ -2835,7 +2921,9 @@ window.cancelAnimFrame = (function () {
if (paginationWrapper && cache.pagination) paginationWrapper.innerHTML = cache.pagination; if (paginationWrapper && cache.pagination) paginationWrapper.innerHTML = cache.pagination;
const restoredPosts = cache.node.querySelector('.posts'); const restoredPosts = cache.node.querySelector('.posts');
if (restoredPosts && restoredPosts._scrollHandler) { if (restoredPosts && restoredPosts._scrollHandler) {
window.addEventListener('scroll', restoredPosts._scrollHandler, { passive: true }); const sc = restoredPosts.closest('.index-container') || window;
restoredPosts._scrollContainer = sc;
sc.addEventListener('scroll', restoredPosts._scrollHandler, { passive: true });
if (restoredPosts._infiniteState) restoredPosts._infiniteState.loading = false; if (restoredPosts._infiniteState) restoredPosts._infiniteState.loading = false;
} }
} }
@@ -2844,7 +2932,14 @@ window.cancelAnimFrame = (function () {
if (!options.skipPush && !document.body.classList.contains('onara-modal-open')) history.pushState({}, '', url); if (!options.skipPush && !document.body.classList.contains('onara-modal-open')) history.pushState({}, '', url);
// Restore scroll position // Restore scroll position
requestAnimationFrame(() => window.scrollTo(0, cache.scroll)); requestAnimationFrame(() => {
const ic = document.querySelector('.index-container');
if (ic && cache.scroll !== undefined) {
ic.scrollTo(0, cache.scroll);
} else {
window.scrollTo(0, cache.scroll || 0);
}
});
// Update title // Update title
const urlParamsC = new URLSearchParams(urlObj.search); const urlParamsC = new URLSearchParams(urlObj.search);
@@ -3423,10 +3518,16 @@ window.cancelAnimFrame = (function () {
// Handle Scroll Position (Restore or Reset) // Handle Scroll Position (Restore or Reset)
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(() => window.scrollTo(0, history.state.scroll)); requestAnimationFrame(() => {
const ic = document.querySelector('.index-container');
if (ic) ic.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); const ic = document.querySelector('.index-container');
if (ic) ic.scrollTo(0, 0);
else window.scrollTo(0, 0);
} }
// Trigger fade-in // Trigger fade-in
@@ -3544,10 +3645,11 @@ window.cancelAnimFrame = (function () {
// Navbar scroll effect - make background black when scrolling // Navbar scroll effect - make background black when scrolling
if (navbar) { if (navbar) {
window.addEventListener('scroll', () => { document.addEventListener('scroll', () => {
const isScrolled = window.scrollY > 10; const ic = document.querySelector('.index-container');
const isScrolled = (ic ? ic.scrollTop : window.scrollY) > 10;
navbar.classList.toggle('scrolled', isScrolled); navbar.classList.toggle('scrolled', isScrolled);
}); }, { passive: true, capture: true });
} }
// Helper to immediately abort media downloads // Helper to immediately abort media downloads
@@ -3674,7 +3776,8 @@ window.cancelAnimFrame = (function () {
window.dispatchEvent(new Event('pjax:start')); window.dispatchEvent(new Event('pjax:start'));
} }
const currentScroll = window.scrollY; const currentContainer = document.querySelector('.index-container');
const currentScroll = currentContainer ? currentContainer.scrollTop : window.scrollY;
// Save scroll position for current page (only for new navigations) // Save scroll position for current page (only for new navigations)
if (!options.skipPush && !options.keepMedia && !useOnara) { if (!options.skipPush && !options.keepMedia && !useOnara) {
@@ -3885,12 +3988,7 @@ window.cancelAnimFrame = (function () {
_ntc.remove(); _ntc.remove();
} }
main.className = 'item-view'; main.className = 'item-view';
document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy'); ensureLayoutClass();
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(); if (window.syncNavbarHeight) window.syncNavbarHeight();
main.innerHTML = ''; main.innerHTML = '';
_container = main; _container = main;
@@ -3981,6 +4079,12 @@ window.cancelAnimFrame = (function () {
}) })
.catch(() => {}); .catch(() => {});
if (typeof requestIdleCallback === 'function') {
requestIdleCallback(() => prefetchNextPrevItems());
} else {
setTimeout(prefetchNextPrevItems, 100);
}
return; // Done — served from cache return; // Done — served from cache
} }
// ── END CACHE HIT ─────────────────────────────────────────────────────── // ── END CACHE HIT ───────────────────────────────────────────────────────
@@ -4078,12 +4182,7 @@ window.cancelAnimFrame = (function () {
main.className = 'item-view'; main.className = 'item-view';
// Correctly handle body classes for the current layout mode // Correctly handle body classes for the current layout mode
document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy'); ensureLayoutClass();
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(); 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
@@ -5172,6 +5271,16 @@ window.cancelAnimFrame = (function () {
// Don't fire global shortcuts on the scroller/abyss page — it has its own handler // Don't fire global shortcuts on the scroller/abyss page — it has its own handler
if (document.body && document.body.classList.contains('scroller-active')) return; if (document.body && document.body.classList.contains('scroller-active')) return;
// Shift+F: shortcut for favorite when "F is for Fullscreen" is active
if ((e.key === 'F' || (e.key === 'f' && e.shiftKey)) && e.shiftKey && !e.ctrlKey && !e.metaKey && !e.altKey && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA" && !e.target.isContentEditable) {
if (localStorage.getItem('fForFullscreen') === 'true') {
if (e.repeat) return;
e.preventDefault();
clickOnElementBinding("#a_favo")();
return;
}
}
if (e.key in keybindings && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") { if (e.key in keybindings && e.target.tagName !== "INPUT" && e.target.tagName !== "TEXTAREA") {
const isHelpKey = e.key === '?' || e.key === 'ß'; const isHelpKey = e.key === '?' || e.key === 'ß';
if (e.repeat || (e.shiftKey && e.key !== '?') || e.ctrlKey || e.metaKey || e.altKey) if (e.repeat || (e.shiftKey && e.key !== '?') || e.ctrlKey || e.metaKey || e.altKey)
@@ -5210,11 +5319,21 @@ window.cancelAnimFrame = (function () {
const initInfiniteScroll = (postsContainer) => { const initInfiniteScroll = (postsContainer) => {
if (!postsContainer || postsContainer.classList.contains('no-infinite-scroll')) return; if (!postsContainer || postsContainer.classList.contains('no-infinite-scroll')) return;
const scrollContainer = postsContainer.closest('.index-container') || window;
const isElementScroll = scrollContainer !== window;
const getScrollTop = () => isElementScroll ? scrollContainer.scrollTop : window.scrollY;
const getViewportHeight = () => isElementScroll ? scrollContainer.clientHeight : window.innerHeight;
const getScrollHeight = () => isElementScroll ? scrollContainer.scrollHeight : (document.documentElement.scrollHeight || document.body.scrollHeight);
const scrollToPos = (top) => isElementScroll ? scrollContainer.scrollTo(0, top) : window.scrollTo(0, top);
// If already initialized, clean up the old scroll listener first // If already initialized, clean up the old scroll listener first
if (postsContainer._infiniteState) { if (postsContainer._infiniteState) {
if (postsContainer._scrollHandler) { if (postsContainer._scrollHandler) {
window.removeEventListener('scroll', postsContainer._scrollHandler); const sc = postsContainer._scrollContainer || scrollContainer;
sc.removeEventListener('scroll', postsContainer._scrollHandler);
delete postsContainer._scrollHandler; delete postsContainer._scrollHandler;
delete postsContainer._scrollContainer;
} }
delete postsContainer._infiniteState; delete postsContainer._infiniteState;
} }
@@ -5227,7 +5346,7 @@ window.cancelAnimFrame = (function () {
hasPrev: false, hasPrev: false,
currentPage: 1, currentPage: 1,
firstPage: 1, firstPage: 1,
lastScrollY: window.scrollY, lastScrollY: getScrollTop(),
navigationGracePeriod: Date.now() + 500, // No URL updates for 500ms after load navigationGracePeriod: Date.now() + 500, // No URL updates for 500ms after load
loadedPages: new Set() loadedPages: new Set()
}; };
@@ -5331,13 +5450,15 @@ window.cancelAnimFrame = (function () {
// Find the marker that is currently most visible/active // Find the marker that is currently most visible/active
let maxVisibleHeight = 0; let maxVisibleHeight = 0;
const viewportHeight = window.innerHeight; const viewportHeight = getViewportHeight();
const scrollTop = window.scrollY; const scrollTop = getScrollTop();
const scrollHeight = getScrollHeight();
// Special case: if we are close to the bottom of the entire document, // Special case: if we are close to the bottom of the entire document/container,
// give priority to the last marker that is at least partially visible. // give priority to the last marker that is at least partially visible.
// Increasing threshold to 150px for better resilience on high-res monitors. // Increasing threshold to 150px for better resilience on high-res monitors.
const isNearBottom = (viewportHeight + window.scrollY) >= (document.documentElement.scrollHeight - 150); const isNearBottom = (viewportHeight + scrollTop) >= (scrollHeight - 150);
const containerTop = isElementScroll ? scrollContainer.getBoundingClientRect().top : 0;
// Heuristic: iterate markers and calculate how much vertical space each "page section" occupies in the viewport // Heuristic: iterate markers and calculate how much vertical space each "page section" occupies in the viewport
for (let i = 0; i < markers.length; i++) { for (let i = 0; i < markers.length; i++) {
@@ -5345,11 +5466,12 @@ window.cancelAnimFrame = (function () {
const rect = marker.getBoundingClientRect(); const rect = marker.getBoundingClientRect();
const nextMarker = markers[i + 1]; const nextMarker = markers[i + 1];
// Start of this section (relative to viewport top) // Start of this section (relative to viewport/container top)
const start = rect.top; const start = rect.top - containerTop;
// End of this section (relative to viewport top). If no next marker, assume bottom of document/container // End of this section (relative to viewport/container top). If no next marker, assume bottom of document/container
// rect.top of next marker, or very large number // rect.top of next marker, or very large number
const end = nextMarker ? nextMarker.getBoundingClientRect().top : 99999; const nextRect = nextMarker ? nextMarker.getBoundingClientRect() : null;
const end = nextRect ? (nextRect.top - containerTop) : 99999;
// Intersection with viewport [0, viewportHeight] // Intersection with viewport [0, viewportHeight]
const visibleStart = Math.max(0, start); const visibleStart = Math.max(0, start);
@@ -5564,7 +5686,7 @@ window.cancelAnimFrame = (function () {
// Capture height/scroll RIGHT BEFORE insertion to account for any user scrolling during fetch // Capture height/scroll RIGHT BEFORE insertion to account for any user scrolling during fetch
const oldHeight = postsContainer.scrollHeight; const oldHeight = postsContainer.scrollHeight;
const oldScrollTop = window.scrollY; const oldScrollTop = getScrollTop();
if (data.success && data.html) { if (data.success && data.html) {
const temp = document.createElement('div'); const temp = document.createElement('div');
@@ -5595,7 +5717,7 @@ window.cancelAnimFrame = (function () {
// Restore scroll position // Restore scroll position
const newHeight = postsContainer.scrollHeight; const newHeight = postsContainer.scrollHeight;
window.scrollTo(0, oldScrollTop + (newHeight - oldHeight)); scrollToPos(oldScrollTop + (newHeight - oldHeight));
infiniteState.firstPage = prevPage; infiniteState.firstPage = prevPage;
infiniteState.hasPrev = prevPage > 1; infiniteState.hasPrev = prevPage > 1;
@@ -5629,7 +5751,7 @@ window.cancelAnimFrame = (function () {
const currentContainer = postsContainer; const currentContainer = postsContainer;
// Only run if THIS container is the active one and still in DOM // Only run if THIS container is the active one and still in DOM
if (!currentContainer || !document.body.contains(currentContainer)) { if (!currentContainer || !document.body.contains(currentContainer)) {
window.removeEventListener('scroll', onScroll); scrollContainer.removeEventListener('scroll', onScroll);
return; return;
} }
if (currentContainer.classList.contains('no-infinite-scroll')) return; if (currentContainer.classList.contains('no-infinite-scroll')) return;
@@ -5637,9 +5759,10 @@ window.cancelAnimFrame = (function () {
updateUrlAndPagination(); updateUrlAndPagination();
const scrollPosition = window.innerHeight + window.scrollY; const scrollTop = getScrollTop();
const pageHeight = main.offsetHeight; // use cached ref, not querySelector every scroll const vHeight = getViewportHeight();
const distanceFromBottom = pageHeight - scrollPosition; const sHeight = getScrollHeight();
const distanceFromBottom = sHeight - (vHeight + scrollTop);
// Load more when within PRELOAD_OFFSET pixels of bottom // Load more when within PRELOAD_OFFSET pixels of bottom
if (distanceFromBottom < PRELOAD_OFFSET && infiniteState.hasMore && !infiniteState.loading) { if (distanceFromBottom < PRELOAD_OFFSET && infiniteState.hasMore && !infiniteState.loading) {
@@ -5647,17 +5770,18 @@ window.cancelAnimFrame = (function () {
} }
// Load previous when within PRELOAD_OFFSET pixels of top // Load previous when within PRELOAD_OFFSET pixels of top
if (window.scrollY < PRELOAD_OFFSET && infiniteState.hasPrev && !infiniteState.loadingUp) { if (scrollTop < PRELOAD_OFFSET && infiniteState.hasPrev && !infiniteState.loadingUp) {
loadPreviousItems(); loadPreviousItems();
} }
infiniteState.lastScrollY = window.scrollY; infiniteState.lastScrollY = scrollTop;
}); });
}; };
// Store scroll handler reference for cleanup // Store scroll handler and container reference for cleanup
postsContainer._scrollHandler = onScroll; postsContainer._scrollHandler = onScroll;
window.addEventListener('scroll', onScroll, { passive: true }); postsContainer._scrollContainer = scrollContainer;
scrollContainer.addEventListener('scroll', onScroll, { passive: true });
// Initial check (in case we loaded at top/bottom) // Initial check (in case we loaded at top/bottom)
setTimeout(onScroll, 100); setTimeout(onScroll, 100);
@@ -11516,7 +11640,17 @@ document.addEventListener('click', (e) => {
const link = e.target.closest('.steuerung-scroll-down'); const link = e.target.closest('.steuerung-scroll-down');
if (!link) return; if (!link) return;
e.preventDefault(); e.preventDefault();
const onaraModal = e.target.closest('#onara-modal') || (document.body.classList.contains('onara-modal-open') && document.getElementById('onara-modal'));
if (onaraModal) {
onaraModal.scrollTo({ top: onaraModal.scrollHeight, behavior: 'smooth' });
} else {
const ic = document.querySelector('.index-container');
if (ic) {
ic.scrollTo({ top: ic.scrollHeight, behavior: 'smooth' });
} else {
window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }); window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
}
}
}); });
// ── Mobile scroll-to-top button ─────────────────────────────────────────────── // ── Mobile scroll-to-top button ───────────────────────────────────────────────
@@ -11525,7 +11659,17 @@ document.addEventListener('click', (e) => {
document.addEventListener('click', (e) => { document.addEventListener('click', (e) => {
if (!e.target.closest('.mobile-scroll-to-top')) return; if (!e.target.closest('.mobile-scroll-to-top')) return;
e.preventDefault(); e.preventDefault();
const onaraModal = e.target.closest('#onara-modal') || (document.body.classList.contains('onara-modal-open') && document.getElementById('onara-modal'));
if (onaraModal) {
onaraModal.scrollTo({ top: 0, behavior: 'smooth' });
} else {
const ic = document.querySelector('.index-container');
if (ic) {
ic.scrollTo({ top: 0, behavior: 'smooth' });
} else {
window.scrollTo({ top: 0, behavior: 'smooth' }); window.scrollTo({ top: 0, behavior: 'smooth' });
}
}
if (navigator.vibrate) navigator.vibrate(20); if (navigator.vibrate) navigator.vibrate(20);
}); });
+1 -1
View File
@@ -267,7 +267,7 @@ class v0ck {
} }
function enterFullScreen() { function enterFullScreen() {
if (document.fullscreenElement) return; if (document.fullscreenElement) return;
const target = document.getElementById('main') || player; const target = player;
if (/(iPad|iPhone|iPod)/gi.test(navigator.platform)) if (/(iPad|iPhone|iPod)/gi.test(navigator.platform))
video.webkitEnterFullscreen(); video.webkitEnterFullscreen();
else else
+1 -1
View File
@@ -158,7 +158,7 @@
"alternative_steuerung": "Icon-Navigationsstil", "alternative_steuerung": "Icon-Navigationsstil",
"alternative_steuerung_hint": "Ersetzt die Text-Navigation (← zurück | Zufall | weiter →) durch kompakte Chevron-Icons", "alternative_steuerung_hint": "Ersetzt die Text-Navigation (← zurück | Zufall | weiter →) durch kompakte Chevron-Icons",
"f_for_fullscreen": "F is for Fullscreen", "f_for_fullscreen": "F is for Fullscreen",
"f_for_fullscreen_hint": "Das Drücken von F schaltet das Vollbild im Videoplayer statt den Beitrag zu favorisieren.", "f_for_fullscreen_hint": "Das Drücken von F schaltet das Vollbild im Videoplayer statt den Beitrag zu favorisieren (Shift+F zum Favorisieren).",
"disable_autoplay": "Automatische Wiedergabe deaktivieren", "disable_autoplay": "Automatische Wiedergabe deaktivieren",
"disable_autoplay_hint": "Verhindert die automatische Wiedergabe von Videos und Audio", "disable_autoplay_hint": "Verhindert die automatische Wiedergabe von Videos und Audio",
"hide_item_ratings": "Item-Bewertungen ausblenden", "hide_item_ratings": "Item-Bewertungen ausblenden",
+1 -1
View File
@@ -158,7 +158,7 @@
"alternative_steuerung": "Icon nav style", "alternative_steuerung": "Icon nav style",
"alternative_steuerung_hint": "Replace text navigation (← prev | random | next →) with compact chevron icons", "alternative_steuerung_hint": "Replace text navigation (← prev | random | next →) with compact chevron icons",
"f_for_fullscreen": "F is for Fullscreen", "f_for_fullscreen": "F is for Fullscreen",
"f_for_fullscreen_hint": "Pressing F will toggle fullscreen in the video player instead of favoriting the post.", "f_for_fullscreen_hint": "Pressing F will toggle fullscreen in the video player instead of favoriting the post (Shift+F to favorite).",
"disable_autoplay": "Disable Autoplay", "disable_autoplay": "Disable Autoplay",
"disable_autoplay_hint": "Prevent videos and audio from playing automatically", "disable_autoplay_hint": "Prevent videos and audio from playing automatically",
"hide_item_ratings": "Hide Item Ratings", "hide_item_ratings": "Hide Item Ratings",
+1 -1
View File
@@ -158,7 +158,7 @@
"alternative_steuerung": "Icoon-navigatiestijl", "alternative_steuerung": "Icoon-navigatiestijl",
"alternative_steuerung_hint": "Vervangt tekstnavigatie (← terug | willekeurig | verder →) door compacte chevron-iconen", "alternative_steuerung_hint": "Vervangt tekstnavigatie (← terug | willekeurig | verder →) door compacte chevron-iconen",
"f_for_fullscreen": "F is for Fullscreen", "f_for_fullscreen": "F is for Fullscreen",
"f_for_fullscreen_hint": "Drukken op F schakelt het volledige scherm in de videospeler in plaats van de post te favorieten.", "f_for_fullscreen_hint": "Drukken op F schakelt het volledige scherm in de videospeler in plaats van de post te favorieten (Shift+F om te favorieten).",
"disable_autoplay": "Automatisch afspelen uitschakelen", "disable_autoplay": "Automatisch afspelen uitschakelen",
"disable_autoplay_hint": "Voorkomen dat video's en audio automatisch worden afgespeeld", "disable_autoplay_hint": "Voorkomen dat video's en audio automatisch worden afgespeeld",
"hide_item_ratings": "Itembeoordelingen verbergen", "hide_item_ratings": "Itembeoordelingen verbergen",
+1 -1
View File
@@ -158,7 +158,7 @@
"alternative_steuerung": "Icon-Navigationsstil", "alternative_steuerung": "Icon-Navigationsstil",
"alternative_steuerung_hint": "Ersetzt die Text-Navigation (← zurück | Zufall | weiter →) durch kompakte Chevron-Icons", "alternative_steuerung_hint": "Ersetzt die Text-Navigation (← zurück | Zufall | weiter →) durch kompakte Chevron-Icons",
"f_for_fullscreen": "F is for Fullscreen", "f_for_fullscreen": "F is for Fullscreen",
"f_for_fullscreen_hint": "Das Drücken der F-Taste leitet die Vollbildlichkeit des Abspielers ein, statt den Pfosten zu favorisieren.", "f_for_fullscreen_hint": "Das Drücken der F-Taste leitet die Vollbildlichkeit des Abspielers ein, statt den Pfosten zu favorisieren (Shift+F zur Favorisierung).",
"disable_autoplay": "Automatische Wiedergabe deaktivieren", "disable_autoplay": "Automatische Wiedergabe deaktivieren",
"disable_autoplay_hint": "Vermeiden Sie das automatische Abspielen von Videos und Tondateien", "disable_autoplay_hint": "Vermeiden Sie das automatische Abspielen von Videos und Tondateien",
"hide_item_ratings": "Item-Bewertungen ausblenden", "hide_item_ratings": "Item-Bewertungen ausblenden",
+1 -1
View File
@@ -1486,7 +1486,7 @@ process.on('uncaughtException', err => {
get cleanup_include_engaged() { return getCleanupIncludeEngaged(); }, get cleanup_include_engaged() { return getCleanupIncludeEngaged(); },
matrix_enabled: cfg.clients.find(c => c.type === 'matrix')?.enabled || false, matrix_enabled: cfg.clients.find(c => c.type === 'matrix')?.enabled || false,
telegram_enabled: cfg.clients.find(c => c.type === 'tg')?.enabled || false, telegram_enabled: cfg.clients.find(c => c.type === 'tg')?.enabled || false,
ts: Date.now(), get ts() { return Date.now(); },
get default_layout() { return getDefaultLayout(); }, get default_layout() { return getDefaultLayout(); },
show_koepfe: !!cfg.websrv.show_koepfe, show_koepfe: !!cfg.websrv.show_koepfe,
hide_sidebar_default: !!cfg.websrv.hide_sidebar_default, hide_sidebar_default: !!cfg.websrv.hide_sidebar_default,
+1 -1
View File
@@ -334,7 +334,7 @@
<input type="checkbox" id="f_for_fullscreen_toggle"> <input type="checkbox" id="f_for_fullscreen_toggle">
<span>{{ t('settings.f_for_fullscreen') || 'F is for Fullscreen' }}</span> <span>{{ t('settings.f_for_fullscreen') || 'F is for Fullscreen' }}</span>
</label> </label>
<small class="text-muted" style="margin-left: 25px;">{{ t('settings.f_for_fullscreen_hint') || 'Pressing F will toggle fullscreen in the video player instead of favoriting the post.' }}</small> <small class="text-muted" style="margin-left: 25px;">{{ t('settings.f_for_fullscreen_hint') || 'Pressing F will toggle fullscreen in the video player instead of favoriting the post (Shift+F to favorite).' }}</small>
</div> </div>
</fieldset> </fieldset>
+1 -1
View File
@@ -66,7 +66,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.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;</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.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;</script>
@if(!private_society || session) @if(!private_society || session)
<script src="/s/js/marked.min.js" defer></script> <script src="/s/js/marked.min.js" defer></script>
<script src="/s/js/comments.js?v={{ ts }}" defer></script> <script src="/s/js/comments.js?v={{ ts }}" defer></script>