diff --git a/banner.js b/banner.js index 739f9fa..b5f5111 100644 --- a/banner.js +++ b/banner.js @@ -12,7 +12,7 @@ Get the official app for your device - | + | Submit requests @@ -51,15 +51,64 @@ } `; + // Jellyfin's own nav chrome has moved before (left drawer -> top app bar in + // 12.x) and will likely move again. Rather than hardcoding selectors that + // break on every redesign, find whatever is fixed/sticky and anchored near + // the top of the viewport at runtime, and push it down (Y) by the banner's + // height so the banner never just sits in front of (Z) live nav chrome. + const originalMarginTop = new WeakMap(); + const offsetElements = new Set(); + + function isTopBarCandidate(el) { + if (el.id === BANNER_ID || el.closest('#' + BANNER_ID)) return false; + const style = getComputedStyle(el); + if (style.position !== 'fixed' && style.position !== 'sticky') return false; + if (style.display === 'none' || style.visibility === 'hidden' || parseFloat(style.opacity) === 0) return false; + const rect = el.getBoundingClientRect(); + if (rect.width < window.innerWidth * 0.5) return false; // must span most of the viewport width + if (rect.height <= 0 || rect.height > window.innerHeight * 0.6) return false; // skip full-page overlays/modals + if (rect.top > 400) return false; // must be anchored near the top of the page + return true; + } + + function findTopBars() { + return Array.prototype.filter.call(document.body.querySelectorAll('*'), isTopBarCandidate); + } + + function pushDownTopBars(offsetPx) { + const candidates = new Set(findTopBars()); + + // Restore anything we previously offset that's no longer a candidate + // (e.g. replaced by the SPA router on navigation). + offsetElements.forEach(function (el) { + if (!candidates.has(el) || !document.contains(el)) { + if (originalMarginTop.has(el)) { + el.style.marginTop = originalMarginTop.get(el); + } + offsetElements.delete(el); + } + }); + + candidates.forEach(function (el) { + if (!originalMarginTop.has(el)) { + originalMarginTop.set(el, el.style.marginTop || ''); + } + const base = originalMarginTop.get(el); + el.style.marginTop = offsetPx > 0 + ? 'calc(' + (base || '0px') + ' + ' + offsetPx + 'px)' + : base; + offsetElements.add(el); + }); + + if (offsetPx > 0 && candidates.size === 0) { + console.warn('[custom-banner] No fixed top navigation elements detected to push down; banner may overlap the menu.'); + } + } + function applyOffset() { const banner = document.getElementById(BANNER_ID); - const offset = banner ? (banner.offsetHeight + 'px') : ''; - const header = document.querySelector('.headerTop'); - const drawer = document.querySelector('.mainDrawer'); - const sections = document.querySelector('.homeSectionsContainer'); - if (header) header.style.marginTop = offset; - if (drawer) drawer.style.marginTop = offset; - if (sections) sections.style.marginTop = offset; + const offset = banner ? banner.offsetHeight : 0; + pushDownTopBars(offset); } function isHomePage() { @@ -101,15 +150,27 @@ style.textContent = BANNER_CSS; document.head.appendChild(style); + // Debounce: the SPA's own DOM churn (list virtualization, image loads, etc.) + // can fire many mutations per second. We don't need per-frame precision for + // nav-bar layout, so coalesce bursts instead of rescanning the whole DOM + // on every single mutation. + let debounceTimer = null; + function scheduleUpdate(delay) { + clearTimeout(debounceTimer); + debounceTimer = setTimeout(function () { + update(); + applyOffset(); + }, delay || 150); + } + // React to SPA navigation window.addEventListener('hashchange', update); + window.addEventListener('resize', function () { scheduleUpdate(); }); - // Watch for Jellyfin's deferred body rendering (SPA bootstraps after DOMContentLoaded) - // subtree: true catches .headerTop wherever it appears in the DOM - const observer = new MutationObserver(function () { - update(); - applyOffset(); - }); + // Watch for Jellyfin's deferred body rendering (SPA bootstraps after + // DOMContentLoaded) and re-run on DOM churn, since the nav bar can be + // unmounted/remounted by the SPA router. + const observer = new MutationObserver(function () { scheduleUpdate(); }); document.addEventListener('DOMContentLoaded', function () { observer.observe(document.body, { childList: true, subtree: true }); update();