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 =
+ 'home';
+
+ /* 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 =
+ 'favorite_border';
+
+ /* 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 = 'play_arrow';
+ 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,
+ });
+})();