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
+232 -88
View File
@@ -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);
});
+1 -1
View File
@@ -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