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)