From 5883a664d014ce9805e6dbad0960ee684015cde4 Mon Sep 17 00:00:00 2001 From: Kevin D Date: Thu, 2 Apr 2026 18:33:07 -0700 Subject: [PATCH] initial commit 2 --- banner.js | 120 +++++++++++++++++++++++++++++++++++++++++++ inject-banner.sh | 51 ++++++++++++++++++ jellyfin-banner.hook | 10 ++++ 3 files changed, 181 insertions(+) create mode 100644 banner.js create mode 100755 inject-banner.sh create mode 100644 jellyfin-banner.hook diff --git a/banner.js b/banner.js new file mode 100644 index 0000000..739f9fa --- /dev/null +++ b/banner.js @@ -0,0 +1,120 @@ +(function () { + // =========== README ========== + // Update the message below + // incrementversion DISMISSED_KEY + const DISMISSED_KEY = 'jf-banner-dismissed-v1'; + const BANNER_ID = 'custom-announcement-banner'; + + // Edit these to change banner content: + const BANNER_HTML = ` + + + `; + + const BANNER_CSS = ` + #custom-announcement-banner { + position: fixed; + top: 0; + left: 0; + display: flex; + align-items: center; + justify-content: space-between; + background: #1c3a5c; + color: #e0e0e0; + font-size: 0.9em; + padding: 8px 16px; + box-sizing: border-box; + width: 100%; + z-index: 9999; + border-bottom: 1px solid #2d5a8e; + } + #custom-announcement-banner a { + color: #7cb8f0; + } + #custom-banner-close { + background: none; + border: none; + color: #e0e0e0; + cursor: pointer; + font-size: 1.1em; + padding: 0 4px; + flex-shrink: 0; + } + `; + + 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; + } + + function isHomePage() { + const h = window.location.hash; + return h === '' || h === '#' || h === '#/home' || h === '#!' + || h === '#!/' || h.startsWith('#/home') || h.startsWith('#!/home'); + } + + function injectBanner() { + if (document.getElementById(BANNER_ID)) return; + + const banner = document.createElement('div'); + banner.id = BANNER_ID; + banner.innerHTML = BANNER_HTML; + document.body.insertBefore(banner, document.body.firstChild); + + document.getElementById('custom-banner-close').addEventListener('click', function () { + sessionStorage.setItem(DISMISSED_KEY, '1'); + document.getElementById(BANNER_ID).remove(); + applyOffset(); + }); + + applyOffset(); + } + + function removeBanner() { + const el = document.getElementById(BANNER_ID); + if (el) el.remove(); + applyOffset(); + } + + function update() { + if (sessionStorage.getItem(DISMISSED_KEY)) { removeBanner(); return; } + if (isHomePage()) { injectBanner(); } else { removeBanner(); } + } + + // Inject CSS once + const style = document.createElement('style'); + style.textContent = BANNER_CSS; + document.head.appendChild(style); + + // React to SPA navigation + window.addEventListener('hashchange', update); + + // 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(); + }); + document.addEventListener('DOMContentLoaded', function () { + observer.observe(document.body, { childList: true, subtree: true }); + update(); + }); + + // Fallback: run immediately if DOMContentLoaded already fired + if (document.readyState !== 'loading') update(); +})(); diff --git a/inject-banner.sh b/inject-banner.sh new file mode 100755 index 0000000..f24b25d --- /dev/null +++ b/inject-banner.sh @@ -0,0 +1,51 @@ +#!/bin/bash +set -euo pipefail + +CUSTOM_DIR="/srv/jellyfin/custom" +WEB_DIR="/usr/share/jellyfin/web" +SRC_JS="${CUSTOM_DIR}/banner.js" +INDEX="${WEB_DIR}/index.html" + +if [[ ! -f "${SRC_JS}" ]]; then + echo "jellyfin-inject-banner: ${SRC_JS} not found, skipping." >&2 + exit 0 +fi + +python3 - "${INDEX}" "${SRC_JS}" <<'EOF' +import sys + +index_path, js_path = sys.argv[1], sys.argv[2] + +with open(index_path, 'r') as f: + html = f.read() + +with open(js_path, 'r') as f: + js = f.read() + +# Remove any previous injection +import re +html = re.sub(r'', '', html, flags=re.DOTALL) + +# Inject before +tag = f'' +html = html.replace('', tag + '', 1) + +with open(index_path, 'w') as f: + f.write(html) + +print("jellyfin-inject-banner: inlined banner script into " + index_path) +EOF + +# Replace the logo image (filename contains a content hash that changes on updates) +LOGO_SRC="${CUSTOM_DIR}/banner-light.png" +if [[ -f "${LOGO_SRC}" ]]; then + LOGO_DEST=$(ls "${WEB_DIR}"/banner-light.*.png 2>/dev/null | head -1) + if [[ -n "${LOGO_DEST}" ]]; then + cp "${LOGO_SRC}" "${LOGO_DEST}" + echo "jellyfin-inject-banner: replaced logo at ${LOGO_DEST}" + else + echo "jellyfin-inject-banner: no banner-light.*.png found in ${WEB_DIR}, skipping logo." >&2 + fi +else + echo "jellyfin-inject-banner: ${LOGO_SRC} not found, skipping logo." >&2 +fi diff --git a/jellyfin-banner.hook b/jellyfin-banner.hook new file mode 100644 index 0000000..f0c85de --- /dev/null +++ b/jellyfin-banner.hook @@ -0,0 +1,10 @@ +[Trigger] +Operation = Install +Operation = Upgrade +Type = Package +Target = jellyfin-web + +[Action] +Description = Injecting custom banner into Jellyfin web UI... +When = PostTransaction +Exec = /srv/jellyfin/custom/inject-banner.sh