diff --git a/public/s/css/f0ckm.css b/public/s/css/f0ckm.css
index 36b239e..5cd27da 100644
--- a/public/s/css/f0ckm.css
+++ b/public/s/css/f0ckm.css
@@ -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>.content,
#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;
}
@@ -5060,10 +5065,25 @@ html[res="fullscreen"] span#favs {
}
/* 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;
}
+/* 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,
::after {
@@ -5092,12 +5112,16 @@ body {
line-height: 2;
overflow-x: hidden;
font-size: medium;
+ scrollbar-width: none !important;
+ -ms-overflow-style: none !important;
}
html {
width: 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;
} */
+/* ===== 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-layout-wrapper {
display: flex;
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;
}
.index-layout-wrapper .index-container {
- flex: 1;
+ flex: 1 1 auto;
min-width: 0;
}
@@ -5909,8 +5974,19 @@ body.layout-modern.sidebar-right-hidden .pagination-container-fluid {
.index-container {
width: 100%;
- padding: 5px;
- margin: 0px 20px 0px 20px;
+ 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-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) {
@@ -6767,8 +6843,9 @@ a.removetag i {
@media (max-width: 600px) {
.index-container {
- margin-left: 0;
- margin-right: 0;
+ padding-left: 5px;
+ padding-right: 5px;
+ margin: 0;
}
}
@@ -11181,6 +11258,7 @@ html[theme="orange"] .image-brand {
text-align: left;
max-width: 100vw;
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 {
width: 4cm;
}
@@ -11248,35 +11320,7 @@ html[theme="orange"] .image-brand {
visibility: hidden;
}
-@media (max-width: 1056px) {
- .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;
- }
-}
+/* Navbar grid layout removed for modern-navbar compatibility */
.bg-black {
background: #0000008a !important;
@@ -11321,48 +11365,40 @@ div.sbt {
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 === */
/* === NAVBAR STYLES === */
-/* ── Desktop layout: single flex row ── */
-.navbar {
- position: sticky;
- top: 0;
- z-index: 1005;
- width: 100%;
- display: flex;
- flex-direction: row;
- align-items: center;
- flex-wrap: wrap;
- /* allows motd-container to wrap to its own full-width row */
+/* ── Always-visible sticky navbar ── */
+.navbar,
+body > nav.navbar {
+ position: sticky !important;
+ top: 0 !important;
+ z-index: 1005 !important;
+ width: 100% !important;
+ display: flex !important;
+ flex-direction: row !important;
+ align-items: center !important;
+ flex-wrap: wrap !important;
+ flex-shrink: 0 !important;
+ min-height: 42px !important;
+ box-sizing: border-box !important;
font-family: var(--font);
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 */
.navbar-brand {
- flex-shrink: 0;
- display: flex;
- align-items: center;
- height: 40px;
+ flex-shrink: 0 !important;
+ display: flex !important;
+ align-items: center !important;
+ height: 40px !important;
max-width: 180px;
+ visibility: visible !important;
+ opacity: 1 !important;
}
/* 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-legacy .navbar-header {
background: transparent;
@@ -12647,8 +12677,8 @@ body.layout-legacy .navbar-header {
overflow-y: auto;
}
- /* Viewport lock to enable full app view and prevent scrollbars */
- body.layout-modern:has(.item-layout-container) {
+ /* Viewport lock to enable full app view and prevent scrollbars (standalone item view only) */
+ body.layout-modern:not(.onara-modal-open):has(> .pagewrapper .item-layout-container) {
overflow: hidden;
height: 100vh;
}
@@ -12665,15 +12695,15 @@ body.layout-legacy .navbar-header {
/* 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;
overflow: hidden;
height: calc(100vh - var(--navbar-h, 50px));
}
- body.layout-modern:has(.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)>.pagewrapper>#main,
+ body.layout-modern:not(.onara-modal-open):has(> .pagewrapper .item-layout-container)>#main {
min-height: 0;
overflow: hidden;
height: 100%;
@@ -18701,7 +18731,12 @@ a.lazy-thumb.filtered-upload-ghost:hover::after {
/* Prevent scrolling on background document when Onara modal is active */
body.onara-modal-open {
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 */
@@ -18756,9 +18791,16 @@ body.onara-modal-open nav.navbar {
z-index: 10060 !important;
position: sticky !important;
top: 0 !important;
+ left: 0 !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 {
opacity: 1 !important;
filter: none !important;
@@ -18983,4 +19025,18 @@ 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;
+}
+
+#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;
}
\ No newline at end of file
diff --git a/public/s/css/v0ck.css b/public/s/css/v0ck.css
index c4166cd..9d72086 100644
--- a/public/s/css/v0ck.css
+++ b/public/s/css/v0ck.css
@@ -14,15 +14,10 @@
display: block;
}
-.v0ck.v0ck_fullscreen {
- max-width: none;
- max-height: none;
- width: 100%;
- height: 100%;
- background-color: black;
-}
-
-#main:fullscreen .v0ck.v0ck_fullscreen {
+.v0ck:fullscreen,
+.v0ck.v0ck_fullscreen,
+#main:fullscreen .v0ck.v0ck_fullscreen,
+#onara-modal:fullscreen .v0ck.v0ck_fullscreen {
position: fixed;
top: 0;
left: 0;
@@ -39,7 +34,10 @@
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%;
height: 100%;
object-fit: contain;
@@ -47,7 +45,10 @@
}
/* 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;
}
diff --git a/public/s/js/f0ckm.js b/public/s/js/f0ckm.js
index 30ff5af..2c5c5f5 100644
--- a/public/s/js/f0ckm.js
+++ b/public/s/js/f0ckm.js
@@ -118,10 +118,11 @@ window.cancelAnimFrame = (function () {
};
const ensureLayoutClass = () => {
- if (window.f0ckSession && window.f0ckSession.use_new_layout === false) {
- document.body.classList.remove('layout-modern');
+ const isLegacy = window.f0ckSession ? !window.f0ckSession.use_new_layout : (window.f0ckDefaultLayout === 'legacy');
+ document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy');
+ if (isLegacy) {
document.body.classList.add('layout-legacy');
- } else if (!document.body.classList.contains('layout-legacy')) {
+ } else {
document.body.classList.add('layout-modern');
}
};
@@ -1196,10 +1197,74 @@ window.cancelAnimFrame = (function () {
window.closeOnaraModal = closeOnaraModal;
window.isOnaraActive = isOnaraActive;
+ // Content selector for protected elements inside onara modal
+ const _onaraContentSelector = [
+ '.media-object',
+ '.previous-post',
+ '.next-post',
+ '.arrow-prev',
+ '.arrow-next',
+ '#prev',
+ '#next',
+ '.nav-prev',
+ '.nav-next',
+ '#random',
+ '.steuerung',
+ '.location',
+ '.item_title',
+ '.blahlol',
+ '#comments-container',
+ '.tag-controls',
+ '.sidebar-tags-container',
+ '.xd-score-wrapper',
+ '.user-infobox-block',
+ 'a',
+ 'button',
+ 'input',
+ 'textarea',
+ 'select',
+ 'label',
+ 'form',
+ 'video',
+ 'audio',
+ 'img',
+ 'canvas',
+ 'iframe',
+ '.iconset',
+ '.badge',
+ '.btn',
+ 'time',
+ '[role="button"]',
+ 'i[class*="fa-"]'
+ ].join(', ');
+
+ 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;
@@ -1207,47 +1272,7 @@ window.cancelAnimFrame = (function () {
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',
- '.previous-post',
- '.next-post',
- '.arrow-prev',
- '.arrow-next',
- '#prev',
- '#next',
- '.nav-prev',
- '.nav-next',
- '#random',
- '.steuerung',
- '.location',
- '.item_title',
- '.blahlol',
- '#comments-container',
- '.tag-controls',
- '.sidebar-tags-container',
- '.xd-score-wrapper',
- '.user-infobox-block',
- 'a',
- 'button',
- 'input',
- 'textarea',
- 'select',
- 'label',
- 'form',
- 'video',
- 'audio',
- 'img',
- 'canvas',
- 'iframe',
- '.iconset',
- '.badge',
- '.btn',
- 'time',
- '[role="button"]',
- 'i[class*="fa-"]'
- ].join(', ');
-
- if (e.target.closest(contentElementSelector)) return;
+ if (e.target.closest(_onaraContentSelector)) return;
// 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') {
@@ -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 nextBtn = document.getElementById('next');
const prevBtn = document.getElementById('prev');
@@ -1769,7 +1849,11 @@ window.cancelAnimFrame = (function () {
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' })
.then(r => r.ok ? r.text() : null)
@@ -1787,6 +1871,7 @@ window.cancelAnimFrame = (function () {
itemCacheMap.delete(itemCacheMap.keys().next().value);
}
window.f0ckDebug('[prefetch] Cached AJAX template for item', itemId);
+ preloadMediaFromHtml(html);
}
})
.catch(() => {});
@@ -2745,7 +2830,8 @@ window.cancelAnimFrame = (function () {
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)
if (!options.skipPush) {
@@ -2835,7 +2921,9 @@ window.cancelAnimFrame = (function () {
if (paginationWrapper && cache.pagination) paginationWrapper.innerHTML = cache.pagination;
const restoredPosts = cache.node.querySelector('.posts');
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;
}
}
@@ -2844,7 +2932,14 @@ window.cancelAnimFrame = (function () {
if (!options.skipPush && !document.body.classList.contains('onara-modal-open')) history.pushState({}, '', url);
// 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
const urlParamsC = new URLSearchParams(urlObj.search);
@@ -3423,10 +3518,16 @@ window.cancelAnimFrame = (function () {
// Handle Scroll Position (Restore or Reset)
if (options.skipPush && history.state && history.state.scroll !== undefined) {
// 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) {
// 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
@@ -3544,10 +3645,11 @@ window.cancelAnimFrame = (function () {
// Navbar scroll effect - make background black when scrolling
if (navbar) {
- window.addEventListener('scroll', () => {
- const isScrolled = window.scrollY > 10;
+ document.addEventListener('scroll', () => {
+ const ic = document.querySelector('.index-container');
+ const isScrolled = (ic ? ic.scrollTop : window.scrollY) > 10;
navbar.classList.toggle('scrolled', isScrolled);
- });
+ }, { passive: true, capture: true });
}
// Helper to immediately abort media downloads
@@ -3674,7 +3776,8 @@ window.cancelAnimFrame = (function () {
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)
if (!options.skipPush && !options.keepMedia && !useOnara) {
@@ -3885,12 +3988,7 @@ window.cancelAnimFrame = (function () {
_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');
- }
+ ensureLayoutClass();
if (window.syncNavbarHeight) window.syncNavbarHeight();
main.innerHTML = '';
_container = main;
@@ -3981,6 +4079,12 @@ window.cancelAnimFrame = (function () {
})
.catch(() => {});
+ if (typeof requestIdleCallback === 'function') {
+ requestIdleCallback(() => prefetchNextPrevItems());
+ } else {
+ setTimeout(prefetchNextPrevItems, 100);
+ }
+
return; // Done — served from cache
}
// ── END CACHE HIT ───────────────────────────────────────────────────────
@@ -4078,12 +4182,7 @@ window.cancelAnimFrame = (function () {
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');
- }
+ ensureLayoutClass();
if (window.syncNavbarHeight) window.syncNavbarHeight();
// 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
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") {
const isHelpKey = e.key === '?' || e.key === 'ß';
if (e.repeat || (e.shiftKey && e.key !== '?') || e.ctrlKey || e.metaKey || e.altKey)
@@ -5210,11 +5319,21 @@ window.cancelAnimFrame = (function () {
const initInfiniteScroll = (postsContainer) => {
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 (postsContainer._infiniteState) {
if (postsContainer._scrollHandler) {
- window.removeEventListener('scroll', postsContainer._scrollHandler);
+ const sc = postsContainer._scrollContainer || scrollContainer;
+ sc.removeEventListener('scroll', postsContainer._scrollHandler);
delete postsContainer._scrollHandler;
+ delete postsContainer._scrollContainer;
}
delete postsContainer._infiniteState;
}
@@ -5227,7 +5346,7 @@ window.cancelAnimFrame = (function () {
hasPrev: false,
currentPage: 1,
firstPage: 1,
- lastScrollY: window.scrollY,
+ lastScrollY: getScrollTop(),
navigationGracePeriod: Date.now() + 500, // No URL updates for 500ms after load
loadedPages: new Set()
};
@@ -5331,13 +5450,15 @@ window.cancelAnimFrame = (function () {
// Find the marker that is currently most visible/active
let maxVisibleHeight = 0;
- const viewportHeight = window.innerHeight;
- const scrollTop = window.scrollY;
+ const viewportHeight = getViewportHeight();
+ 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.
// 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
for (let i = 0; i < markers.length; i++) {
@@ -5345,11 +5466,12 @@ window.cancelAnimFrame = (function () {
const rect = marker.getBoundingClientRect();
const nextMarker = markers[i + 1];
- // Start of this section (relative to viewport top)
- const start = rect.top;
- // End of this section (relative to viewport top). If no next marker, assume bottom of document/container
+ // Start of this section (relative to viewport/container top)
+ const start = rect.top - containerTop;
+ // 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
- 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]
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
const oldHeight = postsContainer.scrollHeight;
- const oldScrollTop = window.scrollY;
+ const oldScrollTop = getScrollTop();
if (data.success && data.html) {
const temp = document.createElement('div');
@@ -5595,7 +5717,7 @@ window.cancelAnimFrame = (function () {
// Restore scroll position
const newHeight = postsContainer.scrollHeight;
- window.scrollTo(0, oldScrollTop + (newHeight - oldHeight));
+ scrollToPos(oldScrollTop + (newHeight - oldHeight));
infiniteState.firstPage = prevPage;
infiniteState.hasPrev = prevPage > 1;
@@ -5629,7 +5751,7 @@ window.cancelAnimFrame = (function () {
const currentContainer = postsContainer;
// Only run if THIS container is the active one and still in DOM
if (!currentContainer || !document.body.contains(currentContainer)) {
- window.removeEventListener('scroll', onScroll);
+ scrollContainer.removeEventListener('scroll', onScroll);
return;
}
if (currentContainer.classList.contains('no-infinite-scroll')) return;
@@ -5637,9 +5759,10 @@ window.cancelAnimFrame = (function () {
updateUrlAndPagination();
- const scrollPosition = window.innerHeight + window.scrollY;
- const pageHeight = main.offsetHeight; // use cached ref, not querySelector every scroll
- const distanceFromBottom = pageHeight - scrollPosition;
+ const scrollTop = getScrollTop();
+ const vHeight = getViewportHeight();
+ const sHeight = getScrollHeight();
+ const distanceFromBottom = sHeight - (vHeight + scrollTop);
// Load more when within PRELOAD_OFFSET pixels of bottom
if (distanceFromBottom < PRELOAD_OFFSET && infiniteState.hasMore && !infiniteState.loading) {
@@ -5647,17 +5770,18 @@ window.cancelAnimFrame = (function () {
}
// 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();
}
- infiniteState.lastScrollY = window.scrollY;
+ infiniteState.lastScrollY = scrollTop;
});
};
- // Store scroll handler reference for cleanup
+ // Store scroll handler and container reference for cleanup
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)
setTimeout(onScroll, 100);
@@ -11516,7 +11640,17 @@ document.addEventListener('click', (e) => {
const link = e.target.closest('.steuerung-scroll-down');
if (!link) return;
e.preventDefault();
- window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' });
+ 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' });
+ }
+ }
});
// ── Mobile scroll-to-top button ───────────────────────────────────────────────
@@ -11525,7 +11659,17 @@ document.addEventListener('click', (e) => {
document.addEventListener('click', (e) => {
if (!e.target.closest('.mobile-scroll-to-top')) return;
e.preventDefault();
- window.scrollTo({ top: 0, behavior: 'smooth' });
+ 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' });
+ }
+ }
if (navigator.vibrate) navigator.vibrate(20);
});
diff --git a/public/s/js/v0ck.js b/public/s/js/v0ck.js
index d9058ff..1d68d73 100644
--- a/public/s/js/v0ck.js
+++ b/public/s/js/v0ck.js
@@ -267,7 +267,7 @@ class v0ck {
}
function enterFullScreen() {
if (document.fullscreenElement) return;
- const target = document.getElementById('main') || player;
+ const target = player;
if (/(iPad|iPhone|iPod)/gi.test(navigator.platform))
video.webkitEnterFullscreen();
else
diff --git a/src/inc/locales/de.json b/src/inc/locales/de.json
index 74f12b5..b568cfe 100644
--- a/src/inc/locales/de.json
+++ b/src/inc/locales/de.json
@@ -158,7 +158,7 @@
"alternative_steuerung": "Icon-Navigationsstil",
"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_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_hint": "Verhindert die automatische Wiedergabe von Videos und Audio",
"hide_item_ratings": "Item-Bewertungen ausblenden",
diff --git a/src/inc/locales/en.json b/src/inc/locales/en.json
index 4454dfa..b3144d0 100644
--- a/src/inc/locales/en.json
+++ b/src/inc/locales/en.json
@@ -158,7 +158,7 @@
"alternative_steuerung": "Icon nav style",
"alternative_steuerung_hint": "Replace text navigation (← prev | random | next →) with compact chevron icons",
"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_hint": "Prevent videos and audio from playing automatically",
"hide_item_ratings": "Hide Item Ratings",
diff --git a/src/inc/locales/nl.json b/src/inc/locales/nl.json
index e6f74fe..41c0468 100644
--- a/src/inc/locales/nl.json
+++ b/src/inc/locales/nl.json
@@ -158,7 +158,7 @@
"alternative_steuerung": "Icoon-navigatiestijl",
"alternative_steuerung_hint": "Vervangt tekstnavigatie (← terug | willekeurig | verder →) door compacte chevron-iconen",
"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_hint": "Voorkomen dat video's en audio automatisch worden afgespeeld",
"hide_item_ratings": "Itembeoordelingen verbergen",
diff --git a/src/inc/locales/zange.json b/src/inc/locales/zange.json
index e7bd527..ae946db 100644
--- a/src/inc/locales/zange.json
+++ b/src/inc/locales/zange.json
@@ -158,7 +158,7 @@
"alternative_steuerung": "Icon-Navigationsstil",
"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_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_hint": "Vermeiden Sie das automatische Abspielen von Videos und Tondateien",
"hide_item_ratings": "Item-Bewertungen ausblenden",
diff --git a/src/index.mjs b/src/index.mjs
index f910f67..9f79ea4 100644
--- a/src/index.mjs
+++ b/src/index.mjs
@@ -1486,7 +1486,7 @@ process.on('uncaughtException', err => {
get cleanup_include_engaged() { return getCleanupIncludeEngaged(); },
matrix_enabled: cfg.clients.find(c => c.type === 'matrix')?.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(); },
show_koepfe: !!cfg.websrv.show_koepfe,
hide_sidebar_default: !!cfg.websrv.hide_sidebar_default,
diff --git a/views/settings.html b/views/settings.html
index f05a89f..8d308ba 100644
--- a/views/settings.html
+++ b/views/settings.html
@@ -334,7 +334,7 @@
{{ t('settings.f_for_fullscreen') || 'F is for Fullscreen' }}
- {{ t('settings.f_for_fullscreen_hint') || 'Pressing F will toggle fullscreen in the video player instead of favoriting the post.' }}
+ {{ t('settings.f_for_fullscreen_hint') || 'Pressing F will toggle fullscreen in the video player instead of favoriting the post (Shift+F to favorite).' }}
diff --git a/views/snippets/header.html b/views/snippets/header.html
index c7e280e..37c61b1 100644
--- a/views/snippets/header.html
+++ b/views/snippets/header.html
@@ -66,7 +66,7 @@
@endif
@endif
-
+
@if(!private_society || session)