From 74c43b11814e360d4e3e63a8d3465d1fc5bba67e Mon Sep 17 00:00:00 2001 From: Alexander Bocken Date: Mon, 2 Mar 2026 20:58:19 +0100 Subject: [PATCH] jellyfin: floating glass pill header, nav icons, click-to-play cards - Restyle header as floating glassmorphism pill matching bocken.org - Replace Home/Favorites tab bar with icon buttons (house + heart) in header right - Add play triangle overlay on card thumbnails with click-to-play - Black backgrounds for detail page containers - Always show detail logo regardless of screen width - Mobile adjustments for pill header --- static/other/jellyfin.css | 150 +++++++++++++++++++++++++--- static/other/jellyfin.js | 205 +++++++++++++++++++++++++++++++++++++- 2 files changed, 342 insertions(+), 13 deletions(-) diff --git a/static/other/jellyfin.css b/static/other/jellyfin.css index 14a838f..aadd9ed 100644 --- a/static/other/jellyfin.css +++ b/static/other/jellyfin.css @@ -2,6 +2,9 @@ This file styles jellyfin and can be imported by adding: @import url("https://bocken.org/other/jellyfin.css"); under Server -> General -> Custom CSS Code + +Also requires jellyfin.js loaded via the Custom CSS/JS Injector plugin: +https://bocken.org/other/jellyfin.js */ :root{ --nord0: #2E3440; @@ -104,21 +107,117 @@ progress[value]::-moz-progress-bar { background-color: var(--blue); } -.skinHeader, -.mainDrawer{ - background: var(--nord0); -} -.card{ -transition: 200ms; -} -.card:hover{ -scale: 1.05; +/* ═══════════════════════════════════════════ + FLOATING GLASS PILL HEADER + Matches bocken.org homepage header style + ═══════════════════════════════════════════ */ + +/* Base glass effect for all header states */ +.skinHeader { + background: rgba(20, 20, 20, 0.78) !important; + backdrop-filter: blur(16px) !important; + -webkit-backdrop-filter: blur(16px) !important; + box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25) !important; } -.listItem{ +/* Float as a pill (non-OSD) */ +.skinHeader:not(.osdHeader) { + position: fixed !important; + top: 12px !important; + left: 50% !important; + transform: translateX(-50%) !important; + width: fit-content !important; + min-width: min(600px, calc(100% - 1.5rem)); + max-width: calc(100% - 1.5rem); + border-radius: 100px !important; + border: 1px solid rgba(255, 255, 255, 0.06) !important; + z-index: 999; + padding: 0 0.4rem !important; + overflow: visible !important; +} + +/* Push page content below the floating header */ +.skinBody { + padding-top: 5rem !important; +} + +/* Compact the top bar row */ +.skinHeader:not(.osdHeader) .headerTop { + padding: 0 0.2rem !important; + min-height: 3rem !important; +} + +/* ── Header buttons ─────────────────────── */ +.skinHeader:not(.osdHeader) .headerButton { + color: #999 !important; + transition: all 150ms; + border-radius: 100px; +} +.skinHeader:not(.osdHeader) .headerButton:hover { + color: white !important; + background: rgba(255, 255, 255, 0.1); +} + +/* ── Hide the tab bar — replaced by icon buttons via JS ── */ +.skinHeader:not(.osdHeader) .headerTabs { + display: none !important; +} + +/* ── Active state for injected nav buttons (home/favorites) ── */ +.bocken-nav-active { + color: white !important; + background: rgba(136, 192, 208, 0.25) !important; +} + +/* Hide the original home button — replaced by our injected one */ +.headerHomeButton { + display: none !important; +} + +/* ═══════════════════════════════════════════ + SIDEBAR DRAWER + ═══════════════════════════════════════════ */ +.mainDrawer { + background: rgba(20, 20, 20, 0.95) !important; + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); +} + +/* ═══════════════════════════════════════════ + CARDS & LIST ITEMS + ═══════════════════════════════════════════ */ +.card { + transition: 200ms; +} +.card:hover { + scale: 1.05; +} + +/* Play overlay on card thumbnails */ +.bocken-play-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: rgba(0, 0, 0, 0.3); + opacity: 0; + transition: opacity 200ms; + pointer-events: none; + border-radius: inherit; +} +.bocken-play-overlay .material-icons { + font-size: 3rem; + color: white; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.5)); +} +.cardImageContainer:hover .bocken-play-overlay { + opacity: 1; +} + +.listItem { transition: 200ms; } -.listItme .listItem:hover { scale: 1.02; } @@ -127,9 +226,36 @@ scale: 1.05; font-size: initial !important; } } -.backgroundContainer{ +.backgroundContainer { background-color: #000; } .pageTitleWithDefaultLogo { background-image: url(https://bocken.org/static/css/logos/logo_text_light.png); } + +/* ═══════════════════════════════════════════ + VIDEO DETAIL PAGE + ═══════════════════════════════════════════ */ +.detailPagePrimaryContainer, +.detailPageSecondaryContainer, +.detailRibbon { + background-color: #000 !important; +} + +/* Always show the detail logo, not just on large screens */ +.detailLogo { + display: block !important; +} + +/* ═══════════════════════════════════════════ + MOBILE ADJUSTMENTS + ═══════════════════════════════════════════ */ +@media (max-width: 800px) { + .skinHeader:not(.osdHeader) { + min-width: unset; + width: calc(100% - 1rem); + top: 8px !important; + border-radius: 20px !important; + padding: 0 0.2rem !important; + } +} diff --git a/static/other/jellyfin.js b/static/other/jellyfin.js index 73bce19..20e63eb 100644 --- a/static/other/jellyfin.js +++ b/static/other/jellyfin.js @@ -1,2 +1,205 @@ +/* + * Custom Jellyfin UI enhancements + * Import via custom JS plugin + */ -document.addEventListener('load', function() { alert(1);document.querySelector('.detailImageContainer').addEventListener('click',function(){document.querySelector(".btnPlay[title='Play'],.btnPlay[title='Resume']").click()});}); +/* ═══════════════════════════════════════════ + 1. Home/Favorites icon buttons in header + ═══════════════════════════════════════════ */ +(function () { + var homeBtn = null; + var favBtn = null; + + function ensureButtons() { + var header = document.querySelector('.skinHeader:not(.osdHeader)'); + if (!header) return; + + var headerRight = header.querySelector('.headerRight'); + if (!headerRight) return; + + /* Already injected and still in the DOM — just update state */ + if (homeBtn && homeBtn.parentNode && favBtn && favBtn.parentNode) { + updateActiveStates(header); + return; + } + + /* Clean up any orphans */ + document.querySelectorAll('.bocken-home-btn, .bocken-fav-btn').forEach( + function (el) { el.remove(); } + ); + + /* Create home button */ + homeBtn = document.createElement('button'); + homeBtn.type = 'button'; + homeBtn.className = + 'headerButton headerButtonRight bocken-home-btn paper-icon-button-light'; + homeBtn.title = 'Home'; + homeBtn.innerHTML = + ''; + + /* Create favorites button */ + favBtn = document.createElement('button'); + favBtn.type = 'button'; + favBtn.className = + 'headerButton headerButtonRight bocken-fav-btn paper-icon-button-light'; + favBtn.title = 'Favorites'; + favBtn.innerHTML = + ''; + + /* Insert at the beginning of headerRight (before dice/cast/etc) */ + headerRight.prepend(favBtn); + headerRight.prepend(homeBtn); + + function isHomePage() { + return window.location.hash === '#/home' + || window.location.hash.startsWith('#/home?'); + } + + /* Click handlers */ + homeBtn.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + if (isHomePage()) { + var tab = header.querySelector('.emby-tab-button[data-index="0"]'); + if (tab) { tab.click(); return; } + } + window.location.hash = '/home'; + }); + + favBtn.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + if (isHomePage()) { + var tab = header.querySelector('.emby-tab-button[data-index="1"]'); + if (tab) { tab.click(); return; } + } + /* Navigate to home first, then activate favorites tab */ + window.location.hash = '/home'; + setTimeout(function () { + var t = document.querySelector('.emby-tab-button[data-index="1"]'); + if (t) t.click(); + }, 500); + }); + + updateActiveStates(header); + + /* Watch for tab changes */ + var tabs = header.querySelector('.headerTabs'); + if (tabs) { + new MutationObserver(function () { + updateActiveStates(header); + }).observe(tabs, { + subtree: true, + attributes: true, + attributeFilter: ['class'], + }); + } + } + + function updateActiveStates(header) { + if (!homeBtn || !favBtn) return; + var activeTab = header.querySelector('.emby-tab-button-active'); + var activeText = activeTab + ? activeTab.textContent.trim().toLowerCase() + : ''; + + homeBtn.classList.toggle('bocken-nav-active', activeText === 'home'); + favBtn.classList.toggle('bocken-nav-active', activeText === 'favorites'); + + var icon = favBtn.querySelector('.material-icons'); + if (icon) { + icon.textContent = + activeText === 'favorites' ? 'favorite' : 'favorite_border'; + } + } + + var pending = null; + function scheduleCheck() { + if (pending) return; + pending = setTimeout(function () { + pending = null; + ensureButtons(); + }, 300); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', scheduleCheck); + } else { + scheduleCheck(); + } + + new MutationObserver(scheduleCheck).observe(document.body, { + childList: true, + subtree: true, + }); +})(); + +/* ═══════════════════════════════════════════ + 2. Click card thumbnail to play video + + play triangle overlay on cards + ═══════════════════════════════════════════ */ +(function () { + /* Add play overlay and click-to-play on card thumbnails */ + function processCards() { + var cards = document.querySelectorAll('.cardImageContainer.coveredImage'); + cards.forEach(function (card) { + if (card.dataset.bockenPlay) return; + card.dataset.bockenPlay = '1'; + card.style.cursor = 'pointer'; + + /* Add play triangle overlay */ + var overlay = document.createElement('div'); + overlay.className = 'bocken-play-overlay'; + overlay.innerHTML = ''; + card.appendChild(overlay); + + /* Click to play */ + card.addEventListener('click', function (e) { + e.preventDefault(); + e.stopPropagation(); + + /* Find the detail page's play/resume button */ + var playBtn = document.querySelector('.btnPlay[title="Resume"], .btnPlay[title="Play"]'); + if (playBtn) { + playBtn.click(); + return; + } + + /* If we're on the homepage, navigate to the item first */ + var itemCard = card.closest('.card'); + if (!itemCard) return; + var link = itemCard.querySelector('a[data-id], button[data-id]'); + var id = link ? link.dataset.id : null; + var serverId = link ? link.dataset.serverid : null; + if (id && serverId) { + window.location.hash = '/details?id=' + id + '&serverId=' + serverId; + /* Wait for page to load, then click play */ + setTimeout(function () { + var btn = document.querySelector('.btnPlay[title="Resume"], .btnPlay[title="Play"]'); + if (btn) btn.click(); + }, 1000); + } + }); + }); + } + + var pending = null; + function scheduleProcess() { + if (pending) return; + pending = setTimeout(function () { + pending = null; + processCards(); + }, 300); + } + + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', scheduleProcess); + } else { + scheduleProcess(); + } + + new MutationObserver(scheduleProcess).observe(document.body, { + childList: true, + subtree: true, + }); +})();