Files
homepage/static/other/jellyfin.js
T
Alexander ccca1a7959 fix(jellyfin): scope logo bocken.org link to home header only
The previous wrap-in-anchor approach leaked the bocken.org link onto
unrelated page titles because Jellyfin reuses the same <h3> across
navigations and only swaps its class between .pageTitleWithDefaultLogo
and .pageTitle. Switch to click delegation so the redirect fires only
when the clicked element currently carries the logo class. Also unwrap
any legacy anchor wrappers on first mutation, and bump cursor/filter
hover styles to !important so they survive Jellyfin's own h3 rules.
2026-05-02 13:10:24 +02:00

268 lines
8.7 KiB
JavaScript

/*
* Custom Jellyfin UI enhancements
* Import via custom JS plugin
*/
/* ═══════════════════════════════════════════
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. Make Bocken logo link to bocken.org
Uses click delegation so we don't wrap the
DOM — Jellyfin reuses the same <h3> across
navigations and swaps its class between
.pageTitleWithDefaultLogo (home) and
.pageTitle (collection pages), which would
leak the wrapper onto unrelated titles.
═══════════════════════════════════════════ */
(function () {
var isMobileApp = /wv\)|Jellyfin Mobile/.test(navigator.userAgent);
/* One-time cleanup of legacy <a.bocken-logo-link> wrappers
left behind by older versions of this script. */
function unwrapLegacy() {
document.querySelectorAll('a.bocken-logo-link').forEach(function (a) {
while (a.firstChild) a.parentNode.insertBefore(a.firstChild, a);
a.remove();
});
document.querySelectorAll('[data-bocken-linked]').forEach(function (el) {
delete el.dataset.bockenLinked;
});
}
function openBocken() {
if (isMobileApp) {
navigator.clipboard.writeText('https://bocken.org').then(function () {
var toast = document.createElement('div');
toast.className = 'bocken-toast';
toast.textContent = 'Link copied — open in browser';
document.body.appendChild(toast);
setTimeout(function () { toast.classList.add('bocken-toast-hide'); }, 2000);
setTimeout(function () { toast.remove(); }, 2500);
});
} else {
window.location.href = 'https://bocken.org';
}
}
document.addEventListener('click', function (e) {
var logo = e.target.closest(
'.skinHeader:not(.osdHeader) .pageTitleWithDefaultLogo'
);
if (!logo) return;
e.preventDefault();
e.stopPropagation();
openBocken();
}, true);
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', unwrapLegacy);
} else {
unwrapLegacy();
}
/* Catch wrappers that re-appear from cached SPA state */
new MutationObserver(unwrapLegacy).observe(document.body, {
childList: true,
subtree: true,
});
})();
/* ═══════════════════════════════════════════
3. 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,
});
})();