Compare commits
8
Commits
36df765000
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a43e009b92 | ||
|
|
ee090e7a4b | ||
|
|
bde94dde1c | ||
|
|
80c6587f3b | ||
|
|
2672f8d86d | ||
|
|
8da0d884ed | ||
|
|
69f113891f | ||
|
|
3674f07716 |
No files matched your search
@@ -4,6 +4,7 @@
|
|||||||
// incrementversion DISMISSED_KEY
|
// incrementversion DISMISSED_KEY
|
||||||
const DISMISSED_KEY = 'jf-banner-dismissed-v1';
|
const DISMISSED_KEY = 'jf-banner-dismissed-v1';
|
||||||
const BANNER_ID = 'custom-announcement-banner';
|
const BANNER_ID = 'custom-announcement-banner';
|
||||||
|
const SCRIPT_START = Date.now();
|
||||||
|
|
||||||
// Edit these to change banner content:
|
// Edit these to change banner content:
|
||||||
const BANNER_HTML = `
|
const BANNER_HTML = `
|
||||||
@@ -100,7 +101,10 @@
|
|||||||
offsetElements.add(el);
|
offsetElements.add(el);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (offsetPx > 0 && candidates.size === 0) {
|
// Jellyfin's SPA mounts its header asynchronously after DOMContentLoaded,
|
||||||
|
// so an empty result in the first few seconds is expected, not a bug.
|
||||||
|
// Only warn once things have had time to settle.
|
||||||
|
if (offsetPx > 0 && candidates.size === 0 && Date.now() - SCRIPT_START > 4000) {
|
||||||
console.warn('[custom-banner] No fixed top navigation elements detected to push down; banner may overlap the menu.');
|
console.warn('[custom-banner] No fixed top navigation elements detected to push down; banner may overlap the menu.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,6 +115,72 @@
|
|||||||
pushDownTopBars(offset);
|
pushDownTopBars(offset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hide the default server-name text next to the top-bar logo icon, leaving
|
||||||
|
// just the (now custom) logo image in place. "icon-transparent" matches the
|
||||||
|
// filename inject-banner.sh replaces for the top-bar logo; keep these two
|
||||||
|
// in sync if that ever changes again.
|
||||||
|
const LOGO_IMG_MATCH = 'icon-transparent';
|
||||||
|
const LOGO_MAX_SIZE = '44px'; // grow the icon now that the text next to it is gone; fixed
|
||||||
|
// px instead of em, since em resolves against whatever
|
||||||
|
// (possibly tiny/zeroed) font-size the icon wrapper uses
|
||||||
|
|
||||||
|
function hideLogoText() {
|
||||||
|
const logoImg = document.querySelector('img[src*="' + LOGO_IMG_MATCH + '"]');
|
||||||
|
if (!logoImg) return;
|
||||||
|
|
||||||
|
logoImg.style.setProperty('height', LOGO_MAX_SIZE, 'important');
|
||||||
|
logoImg.style.setProperty('max-height', LOGO_MAX_SIZE, 'important');
|
||||||
|
logoImg.style.setProperty('width', 'auto', 'important');
|
||||||
|
logoImg.style.setProperty('max-width', 'none', 'important');
|
||||||
|
|
||||||
|
// The image is usually wrapped in its own icon span, so the text label
|
||||||
|
// is a sibling of that wrapper, not of the <img> itself. Walk up to the
|
||||||
|
// whole clickable logo button/link and hide every child that isn't part
|
||||||
|
// of the branch containing the image.
|
||||||
|
const container = logoImg.closest('button, a, [role="button"]') || logoImg.parentElement;
|
||||||
|
if (!container) return;
|
||||||
|
|
||||||
|
// Strip any fixed size/overflow-clipping the icon wrapper itself might
|
||||||
|
// impose (MUI's startIcon span is normally sized for a small icon font),
|
||||||
|
// which would otherwise clip the enlarged image regardless of its own size.
|
||||||
|
const iconWrap = logoImg.parentElement;
|
||||||
|
if (iconWrap && iconWrap !== container) {
|
||||||
|
iconWrap.style.setProperty('overflow', 'visible', 'important');
|
||||||
|
iconWrap.style.setProperty('height', 'auto', 'important');
|
||||||
|
iconWrap.style.setProperty('width', 'auto', 'important');
|
||||||
|
iconWrap.style.setProperty('max-height', 'none', 'important');
|
||||||
|
iconWrap.style.setProperty('max-width', 'none', 'important');
|
||||||
|
// MUI's startIcon wrapper carries baked-in negative-left/positive-right
|
||||||
|
// margin (compensating for icon-font glyphs with their own internal
|
||||||
|
// padding); left over, it shifts the image into the container's left
|
||||||
|
// padding and stacks extra space on the right. Zero it so the
|
||||||
|
// container's own symmetric padding is the only thing spacing it.
|
||||||
|
iconWrap.style.setProperty('margin', '0', 'important');
|
||||||
|
}
|
||||||
|
Array.prototype.forEach.call(container.childNodes, function (node) {
|
||||||
|
if (node === logoImg) return;
|
||||||
|
if (node.nodeType === Node.ELEMENT_NODE && node.contains(logoImg)) return;
|
||||||
|
if (node.nodeType === Node.TEXT_NODE) {
|
||||||
|
if (node.textContent.trim()) node.textContent = '';
|
||||||
|
} else if (node.nodeType === Node.ELEMENT_NODE && node.style.display !== 'none') {
|
||||||
|
node.style.display = 'none';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The button was sized (min-width, padding) to fit the icon + label
|
||||||
|
// together; with the label gone, shrink it to fit just the icon instead
|
||||||
|
// of leaving the label's dead space behind. It's a flex item in the
|
||||||
|
// toolbar, so flex-grow/flex-basis would otherwise override the explicit
|
||||||
|
// width below and keep reserving the old footprint.
|
||||||
|
container.style.setProperty('flex', '0 0 auto', 'important');
|
||||||
|
container.style.setProperty('min-width', '0', 'important');
|
||||||
|
container.style.setProperty('width', 'fit-content', 'important');
|
||||||
|
// MUI buttons carry their own horizontal padding around the content
|
||||||
|
// (icon + label); the icon span itself now fits tightly (confirmed via
|
||||||
|
// devtools), so any remaining right-side gap is this padding.
|
||||||
|
container.style.setProperty('padding', '0 8px', 'important');
|
||||||
|
}
|
||||||
|
|
||||||
function isHomePage() {
|
function isHomePage() {
|
||||||
const h = window.location.hash;
|
const h = window.location.hash;
|
||||||
return h === '' || h === '#' || h === '#/home' || h === '#!'
|
return h === '' || h === '#' || h === '#/home' || h === '#!'
|
||||||
@@ -160,22 +230,24 @@
|
|||||||
debounceTimer = setTimeout(function () {
|
debounceTimer = setTimeout(function () {
|
||||||
update();
|
update();
|
||||||
applyOffset();
|
applyOffset();
|
||||||
|
hideLogoText();
|
||||||
}, delay || 150);
|
}, delay || 150);
|
||||||
}
|
}
|
||||||
|
|
||||||
// React to SPA navigation
|
// React to SPA navigation
|
||||||
window.addEventListener('hashchange', update);
|
window.addEventListener('hashchange', function () { update(); hideLogoText(); });
|
||||||
window.addEventListener('resize', function () { scheduleUpdate(); });
|
window.addEventListener('resize', function () { scheduleUpdate(); });
|
||||||
|
|
||||||
// Watch for Jellyfin's deferred body rendering (SPA bootstraps after
|
// Watch for Jellyfin's deferred body rendering (SPA bootstraps after
|
||||||
// DOMContentLoaded) and re-run on DOM churn, since the nav bar can be
|
// DOMContentLoaded) and re-run on DOM churn, since the nav bar (and the
|
||||||
// unmounted/remounted by the SPA router.
|
// logo it contains) can be unmounted/remounted by the SPA router.
|
||||||
const observer = new MutationObserver(function () { scheduleUpdate(); });
|
const observer = new MutationObserver(function () { scheduleUpdate(); });
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
observer.observe(document.body, { childList: true, subtree: true });
|
observer.observe(document.body, { childList: true, subtree: true });
|
||||||
update();
|
update();
|
||||||
|
hideLogoText();
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fallback: run immediately if DOMContentLoaded already fired
|
// Fallback: run immediately if DOMContentLoaded already fired
|
||||||
if (document.readyState !== 'loading') update();
|
if (document.readyState !== 'loading') { update(); hideLogoText(); }
|
||||||
})();
|
})();
|
||||||
+13
-8
@@ -36,16 +36,21 @@ with open(index_path, 'w') as f:
|
|||||||
print("jellyfin-inject-banner: inlined banner script into " + index_path)
|
print("jellyfin-inject-banner: inlined banner script into " + index_path)
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
# Replace the logo image (filename contains a content hash that changes on updates)
|
# Replace the logo image(s) (filenames contain a content hash that changes on
|
||||||
|
# updates). "icon-transparent" is the top-bar logo as of the 12.x header
|
||||||
|
# redesign; "banner-light" is kept too since it's still used for the splash
|
||||||
|
# screen / PWA icons.
|
||||||
LOGO_SRC="${CUSTOM_DIR}/banner-light.png"
|
LOGO_SRC="${CUSTOM_DIR}/banner-light.png"
|
||||||
if [[ -f "${LOGO_SRC}" ]]; then
|
if [[ -f "${LOGO_SRC}" ]]; then
|
||||||
LOGO_DEST=$(ls "${WEB_DIR}"/banner-light.*.png 2>/dev/null | head -1)
|
for LOGO_BASENAME in banner-light icon-transparent; do
|
||||||
if [[ -n "${LOGO_DEST}" ]]; then
|
LOGO_DEST=$(ls "${WEB_DIR}/${LOGO_BASENAME}".*.png 2>/dev/null | head -1)
|
||||||
cp "${LOGO_SRC}" "${LOGO_DEST}"
|
if [[ -n "${LOGO_DEST}" ]]; then
|
||||||
echo "jellyfin-inject-banner: replaced logo at ${LOGO_DEST}"
|
cp "${LOGO_SRC}" "${LOGO_DEST}"
|
||||||
else
|
echo "jellyfin-inject-banner: replaced logo at ${LOGO_DEST}"
|
||||||
echo "jellyfin-inject-banner: no banner-light.*.png found in ${WEB_DIR}, skipping logo." >&2
|
else
|
||||||
fi
|
echo "jellyfin-inject-banner: no ${LOGO_BASENAME}.*.png found in ${WEB_DIR}, skipping." >&2
|
||||||
|
fi
|
||||||
|
done
|
||||||
else
|
else
|
||||||
echo "jellyfin-inject-banner: ${LOGO_SRC} not found, skipping logo." >&2
|
echo "jellyfin-inject-banner: ${LOGO_SRC} not found, skipping logo." >&2
|
||||||
fi
|
fi
|
||||||
Reference in new issue
Block a user