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();