This commit is contained in:
2026-07-12 21:41:42 +02:00
parent 763ef32275
commit 9c6db72d5b
2 changed files with 117 additions and 91 deletions

View File

@@ -7166,25 +7166,44 @@ button#togglebg {
animation: none; animation: none;
} }
/* ─── Posts Grid Skeleton: shimmer on real items while thumbnail loads ─────── */ /* ─── Posts Grid Skeleton: GPU-composited shimmer while thumbnail loads ───── */
/* Uses ::before + transform:translateX instead of background-position. */
/* background-position forces a CPU repaint every frame for every element; */
/* transform runs on the compositor thread — no main-thread blocking. */
div.posts>a.lazy-thumb:not(.loaded) { div.posts>a.lazy-thumb:not(.loaded) {
background: rgba(255, 255, 255, 0.04);
overflow: hidden;
position: relative;
}
div.posts>a.lazy-thumb:not(.loaded)::before {
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, background: linear-gradient(90deg,
rgba(255, 255, 255, 0.04) 0%, transparent 0%,
rgba(255, 255, 255, 0.10) 40%, rgba(255, 255, 255, 0.10) 50%,
rgba(255, 255, 255, 0.04) 80%); transparent 100%);
background-size: 600px 100%; transform: translateX(-100%);
animation: skeleton-shimmer 1.6s ease-in-out infinite; animation: shimmer-slide 1.6s ease-in-out infinite;
will-change: transform;
pointer-events: none;
} }
/* Stagger shimmer phases so items feel alive individually */ /* Stagger shimmer phases so items feel alive individually */
div.posts>a.lazy-thumb:not(.loaded):nth-child(3n+2) { div.posts>a.lazy-thumb:not(.loaded):nth-child(3n+2)::before {
animation-delay: 0.2s; animation-delay: 0.2s;
} }
div.posts>a.lazy-thumb:not(.loaded):nth-child(3n+3) { div.posts>a.lazy-thumb:not(.loaded):nth-child(3n+3)::before {
animation-delay: 0.4s; animation-delay: 0.4s;
} }
@keyframes shimmer-slide {
0% { transform: translateX(-100%); }
100% { transform: translateX(100%); }
}
/* Make individual item entry subtler and faster for infinite scroll */ /* Make individual item entry subtler and faster for infinite scroll */
@keyframes fadeInFX { @keyframes fadeInFX {

View File

@@ -2167,12 +2167,6 @@ window.cancelAnimFrame = (function () {
const currentScroll = window.scrollY; const currentScroll = window.scrollY;
// Immediately fade out the current main content to provide a clean slate for the next page
if (main) {
main.classList.add('grid-transition');
main.classList.remove('show');
}
// Save scroll position for the current page before we leave it (only for new navigations) // Save scroll position for the current page before we leave it (only for new navigations)
if (!options.skipPush) { if (!options.skipPush) {
const currentState = history.state || {}; const currentState = history.state || {};
@@ -2216,84 +2210,79 @@ window.cancelAnimFrame = (function () {
} }
// Check for cached grid (Index/Grid Only) - RESTORE EARLY to avoid pagination/layout destruction // Check for cached grid — fires on ANY navigation to a grid URL (not just browser back/forward).
if (isGrid && options.skipPush) { // The fade-out of main must NOT happen before this check: doing so causes the
// IntersectionObserver to fire for every cached thumbnail at once when they re-enter
// the viewport, loading them all simultaneously instead of progressively.
if (isGrid) {
const targetCacheKey = urlObj.pathname + urlObj.search; const targetCacheKey = urlObj.pathname + urlObj.search;
if (gridCacheMap.has(targetCacheKey)) { if (gridCacheMap.has(targetCacheKey)) {
const cache = gridCacheMap.get(targetCacheKey); const cache = gridCacheMap.get(targetCacheKey);
gridCacheMap.delete(targetCacheKey); // Consume cache // Keep cache alive — cleared on mode/filter changes via gridCacheMap.clear()
if (navbar) navbar.classList.add('pbwork');
stopMedia(); stopMedia();
// Instant fade out background canvas (from item view) // Fade out background canvas (item view blur)
const canvas = document.getElementById('bg'); const canvas = document.getElementById('bg');
if (canvas) { if (canvas) {
canvas.style.transition = 'none'; // Disable transition canvas.style.transition = 'none';
canvas.style.opacity = '0'; canvas.style.opacity = '0';
canvas.classList.remove('fader-in'); canvas.classList.remove('fader-in');
canvas.classList.add('fader-out'); canvas.classList.add('fader-out');
// Clean up overrides after render
requestAnimationFrame(() => { requestAnimationFrame(() => {
setTimeout(() => { setTimeout(() => { canvas.style.transition = ''; canvas.style.opacity = ''; }, 50);
canvas.style.transition = '';
canvas.style.opacity = '';
}, 50);
}); });
} }
// Restore DOM // Restore DOM
if (main) { if (main) {
main.innerHTML = ''; // Clear item view main.innerHTML = '';
main.appendChild(cache.node); main.appendChild(cache.node);
cache.node.style.display = ''; // Ensure visible cache.node.style.display = '';
cache.node.classList.remove('cached-grid'); // Cleanup cache.node.classList.remove('cached-grid');
main.className = ''; main.className = '';
// Clear layout-lock state now that we are restoring the grid
document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy'); document.body.classList.remove('legacy-view', 'layout-modern', 'layout-legacy');
document.body.style.overflow = ''; document.body.style.overflow = '';
document.body.style.height = ''; document.body.style.height = '';
// Restore Pagination HTML
const paginationWrapper = document.querySelector('.pagination-wrapper'); const paginationWrapper = document.querySelector('.pagination-wrapper');
if (paginationWrapper && cache.pagination) { if (paginationWrapper && cache.pagination) paginationWrapper.innerHTML = cache.pagination;
paginationWrapper.innerHTML = cache.pagination;
}
// Re-attach Infinite Scroll Handler
const restoredPosts = cache.node.querySelector('.posts'); const restoredPosts = cache.node.querySelector('.posts');
if (restoredPosts && restoredPosts._scrollHandler) { if (restoredPosts && restoredPosts._scrollHandler) {
window.addEventListener('scroll', restoredPosts._scrollHandler); window.addEventListener('scroll', restoredPosts._scrollHandler, { passive: true });
// Ensure loading state is reset just in case
if (restoredPosts._infiniteState) restoredPosts._infiniteState.loading = false; if (restoredPosts._infiniteState) restoredPosts._infiniteState.loading = false;
} }
} }
// Restore Scroll Position // Update URL bar (only for explicit clicks, not browser back/forward which already did it)
if (!options.skipPush) history.pushState({}, '', url);
// Restore scroll position
requestAnimationFrame(() => window.scrollTo(0, cache.scroll)); requestAnimationFrame(() => window.scrollTo(0, cache.scroll));
// Update Document Title // Update title
let titleSuffix = ''; const urlParamsC = new URLSearchParams(urlObj.search);
const urlParams = new URLSearchParams(window.location.search); const pageC = urlParamsC.get('p') || urlParamsC.get('page');
const page = urlParams.get('p') || urlParams.get('page'); document.title = `${window.f0ckDomain}${pageC ? ` - page ${pageC}` : ''}`;
if (page) titleSuffix = ` - page ${page}`;
document.title = `${window.f0ckDomain}${titleSuffix}`;
// Reset navigation state document.querySelectorAll('.pagination-container-fluid').forEach(el => el.style.display = '');
document.querySelectorAll('.pagination-container-fluid').forEach(el => el.style.display = ''); // Restore pagination visibility if (navbar) navbar.classList.remove('pbwork');
if (navbar) navbar.classList.remove("pbwork"); window.updateVisitIndicators();
window.updateVisitIndicators(); window.initLazyLoading();
window.initLazyLoading(); if (window.initSidebarRightToggle) window.initSidebarRightToggle();
if (window.initSidebarRightToggle) window.initSidebarRightToggle(); if (window.syncNavbarHeight) window.syncNavbarHeight();
if (window.syncNavbarHeight) window.syncNavbarHeight(); window.NotificationSystemInstance?.pollDebounced?.();
// Sync has-notif highlights — on PWA there's no visibilitychange, so poll here isNavigating = false;
window.NotificationSystemInstance?.pollDebounced?.(); return; // SKIP FETCH
isNavigating = false;
return; // SKIP FETCH
} }
} }
// Cache miss — full fetch path. Fade out now (safe: no cached thumbnails in DOM).
if (main) {
main.classList.add('grid-transition');
main.classList.remove('show');
}
// Handle transition from Item View or User Profile back to Grid View // Handle transition from Item View or User Profile back to Grid View
const isOnProfilePage = main && main.querySelector('.profile_head'); const isOnProfilePage = main && main.querySelector('.profile_head');
@@ -3695,6 +3684,12 @@ window.cancelAnimFrame = (function () {
targetUrl = targetUrl.replace(/[?&]strict=1/, '').replace(/[?&]$/, ''); targetUrl = targetUrl.replace(/[?&]strict=1/, '').replace(/[?&]$/, '');
} }
// Already on this exact grid page — just scroll to top, no reload
if (targetUrl.split('#')[0] === window.location.href.split('#')[0] && !anyLink.hash && document.querySelector('.posts')) {
window.scrollTo({ top: 0, behavior: 'smooth' });
return false;
}
const parts = pathname.split('/').filter(Boolean); const parts = pathname.split('/').filter(Boolean);
const isItemLink = !pathname.match(/\/p\//) && ( const isItemLink = !pathname.match(/\/p\//) && (
pathname.match(/^\/\d+/) || pathname.match(/^\/\d+/) ||
@@ -4689,39 +4684,48 @@ window.cancelAnimFrame = (function () {
// Scroll detection - preload before reaching bottom // Scroll detection - preload before reaching bottom
const PRELOAD_OFFSET = 500; // pixels before bottom to trigger load const PRELOAD_OFFSET = 500; // pixels before bottom to trigger load
// RAF throttle: getBoundingClientRect and offsetHeight force synchronous layout
// reflows. Without throttling, Chromium recalculates layout on every scroll
// event (potentially hundreds/sec), blocking the compositor and causing freezes.
let _scrollTicking = false;
const onScroll = () => { const onScroll = () => {
const currentContainer = postsContainer; if (_scrollTicking) return;
// Only run if THIS container is the active one and still in DOM _scrollTicking = true;
if (!currentContainer || !document.body.contains(currentContainer)) { requestAnimationFrame(() => {
window.removeEventListener('scroll', onScroll); _scrollTicking = false;
return;
}
if (currentContainer.classList.contains('no-infinite-scroll')) return;
if (!document.querySelector('#main')) return;
updateUrlAndPagination(); 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);
return;
}
if (currentContainer.classList.contains('no-infinite-scroll')) return;
if (!main) return;
const scrollPosition = window.innerHeight + window.scrollY; updateUrlAndPagination();
const pageHeight = document.querySelector('#main').offsetHeight;
const distanceFromBottom = pageHeight - scrollPosition;
// Load more when within PRELOAD_OFFSET pixels of bottom const scrollPosition = window.innerHeight + window.scrollY;
if (distanceFromBottom < PRELOAD_OFFSET && infiniteState.hasMore && !infiniteState.loading) { const pageHeight = main.offsetHeight; // use cached ref, not querySelector every scroll
loadMoreItems(); const distanceFromBottom = pageHeight - scrollPosition;
}
// Load previous when within PRELOAD_OFFSET pixels of top // Load more when within PRELOAD_OFFSET pixels of bottom
// Also ensure we aren't already at page 1 if (distanceFromBottom < PRELOAD_OFFSET && infiniteState.hasMore && !infiniteState.loading) {
if (window.scrollY < PRELOAD_OFFSET && infiniteState.hasPrev && !infiniteState.loadingUp) { loadMoreItems();
loadPreviousItems(); }
}
infiniteState.lastScrollY = window.scrollY; // Load previous when within PRELOAD_OFFSET pixels of top
if (window.scrollY < PRELOAD_OFFSET && infiniteState.hasPrev && !infiniteState.loadingUp) {
loadPreviousItems();
}
infiniteState.lastScrollY = window.scrollY;
});
}; };
// Store scroll handler reference for cleanup // Store scroll handler reference for cleanup
postsContainer._scrollHandler = onScroll; postsContainer._scrollHandler = onScroll;
window.addEventListener("scroll", onScroll); window.addEventListener('scroll', onScroll, { passive: true });
// Initial check (in case we loaded at top/bottom) // Initial check (in case we loaded at top/bottom)
setTimeout(onScroll, 100); setTimeout(onScroll, 100);
@@ -8275,13 +8279,14 @@ document.addEventListener('DOMContentLoaded', () => {
const file = thumb.dataset.file; const file = thumb.dataset.file;
const mime = thumb.dataset.mime; const mime = thumb.dataset.mime;
// Only preview videos/gifs // Only preview videos (not gifs — fetching a GIF causes main-thread freeze on click)
if (!file || !mime || (!mime.startsWith('video/') && mime !== 'image/gif') || mime === 'video/youtube') return; if (!file || !mime || !mime.startsWith('video/') || mime === 'video/youtube') return;
// Helper to actually start video // Helper to actually start video — only called AFTER the hover delay
// so we don't start a network fetch during the 150ms grace period.
const run = () => { const run = () => {
if (!document.body.contains(thumb)) return; // Thumb removed if (!document.body.contains(thumb)) return;
if (thumb !== activeThumb) return; // Switched away if (thumb !== activeThumb) return;
activeThumb.classList.add('previewing'); activeThumb.classList.add('previewing');
@@ -8291,10 +8296,12 @@ document.addEventListener('DOMContentLoaded', () => {
video.loop = true; video.loop = true;
video.className = 'preview-video'; video.className = 'preview-video';
video.playsInline = true; video.playsInline = true;
video.preload = 'auto'; // 'metadata' fetches only the file header (a few KB for duration/dimensions).
// 'auto' fetches the entire file immediately — catastrophic for large GIFs.
video.preload = 'metadata';
video.oncanplay = () => { video.oncanplay = () => {
video.play().catch(err => { /* Autoplay blocked */ }); video.play().catch(() => {});
video.classList.add('playing'); video.classList.add('playing');
}; };