jellyfin: floating glass pill header, nav icons, click-to-play cards
All checks were successful
CI / update (push) Successful in 1m50s
All checks were successful
CI / update (push) Successful in 1m50s
- 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
This commit is contained in:
@@ -2,6 +2,9 @@
|
|||||||
This file styles jellyfin and can be imported by adding:
|
This file styles jellyfin and can be imported by adding:
|
||||||
@import url("https://bocken.org/other/jellyfin.css");
|
@import url("https://bocken.org/other/jellyfin.css");
|
||||||
under Server -> General -> Custom CSS Code
|
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{
|
:root{
|
||||||
--nord0: #2E3440;
|
--nord0: #2E3440;
|
||||||
@@ -104,21 +107,117 @@ progress[value]::-moz-progress-bar {
|
|||||||
background-color: var(--blue);
|
background-color: var(--blue);
|
||||||
}
|
}
|
||||||
|
|
||||||
.skinHeader,
|
/* ═══════════════════════════════════════════
|
||||||
.mainDrawer{
|
FLOATING GLASS PILL HEADER
|
||||||
background: var(--nord0);
|
Matches bocken.org homepage header style
|
||||||
}
|
═══════════════════════════════════════════ */
|
||||||
.card{
|
|
||||||
transition: 200ms;
|
/* Base glass effect for all header states */
|
||||||
}
|
.skinHeader {
|
||||||
.card:hover{
|
background: rgba(20, 20, 20, 0.78) !important;
|
||||||
scale: 1.05;
|
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;
|
transition: 200ms;
|
||||||
}
|
}
|
||||||
.listItme
|
|
||||||
.listItem:hover {
|
.listItem:hover {
|
||||||
scale: 1.02;
|
scale: 1.02;
|
||||||
}
|
}
|
||||||
@@ -127,9 +226,36 @@ scale: 1.05;
|
|||||||
font-size: initial !important;
|
font-size: initial !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.backgroundContainer{
|
.backgroundContainer {
|
||||||
background-color: #000;
|
background-color: #000;
|
||||||
}
|
}
|
||||||
.pageTitleWithDefaultLogo {
|
.pageTitleWithDefaultLogo {
|
||||||
background-image: url(https://bocken.org/static/css/logos/logo_text_light.png);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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 =
|
||||||
|
'<span class="material-icons" aria-hidden="true">home</span>';
|
||||||
|
|
||||||
|
/* 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 =
|
||||||
|
'<span class="material-icons" aria-hidden="true">favorite_border</span>';
|
||||||
|
|
||||||
|
/* 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 = '<span class="material-icons" aria-hidden="true">play_arrow</span>';
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|||||||
Reference in New Issue
Block a user