Fix cases where Home Screen wouldn't correctly render

This commit is contained in:
HighImKevin committed 2026-09-22 17:41:48 -04:00
1 parent 8f987dae0e
commit a9012dd9b0
16 files changed
+721 -296

No files matched your search

+93 -23
View File
@@ -734,7 +734,7 @@
font-style: normal;
opacity: 0.9;
line-height: 1.45;
max-width: 75%;
max-width: min(700px, 75%);
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
@@ -853,10 +853,8 @@
opacity: 1;
}
.spotlight-nav-container {
.spotlight-section:is([data-section-id]):not(.hide) .spotlight-nav-container {
position: absolute !important;
top: 0px;
right: 0px;
display: flex;
gap: 8px;
z-index: 2;
@@ -1184,8 +1182,17 @@
min-width: 12rem;
}
.section-controls-more-popover .section-controls-more-item {
align-items: start;
/* Modal-hosted more menu: normal flow so the dialog body does not collapse */
.section-controls-more-menu {
position: static;
display: block;
min-width: 12rem;
background: transparent;
}
.section-controls-more-popover .section-controls-more-item,
.section-controls-more-menu .section-controls-more-item {
align-items: center;
}
.section-controls-more-item {
@@ -1209,12 +1216,56 @@
display: none !important;
}
@media (max-width: 899px) {
:not(.layout-mobile) .section-controls-more {
display: inline-flex !important;
opacity: 1;
visibility: visible;
pointer-events: auto;
}
:not(.layout-mobile) .section-controls-inline .show-all-button,
:not(.layout-mobile) .section-controls-inline .section-refresh-button,
:not(.layout-mobile) .section-controls-inline .section-configure-button {
display: none !important;
}
.spotlight-section {
position: relative;
}
.spotlight-section .section-controls {
position: absolute;
right: 0.5rem;
bottom: 0.5rem;
z-index: 5;
margin: 0;
}
}
.layout-mobile .spotlight-section {
position: relative;
}
.layout-mobile .spotlight-section .section-controls {
position: absolute;
right: -0.5rem;
bottom: unset;
top: -4px;
z-index: 5;
margin: 0;
}
.layout-mobile .section-controls-inline .show-all-button,
.layout-mobile .section-controls-inline .section-refresh-button,
.layout-mobile .section-controls-inline .section-configure-button {
display: none !important;
}
.layout-mobile .spotlight-section .section-controls-more {
opacity: 0;
}
.layout-mobile .section-controls-more {
display: inline-flex !important;
opacity: 1;
@@ -1222,6 +1273,18 @@
pointer-events: auto;
}
.layout-mobile .spotlight-section:hover .section-controls-more {
opacity: 1;
}
.kefinTweaks-popover.section-controls-more-menu > .kefinTweaks-popover-item {
align-items: center;
justify-content: space-between;
margin: 0 !important;
padding: 0.5em 0.9em;
max-width: unset !important;
}
/* Grid (non-scrolling) item layouts */
.itemsContainer[data-layout="grid"] {
display: flex !important;
@@ -1416,7 +1479,7 @@
}
.layout-mobile .spotlight-section[data-layout="Borderless"] {
margin-top: -6rem;
margin-top: -4rem;
margin-bottom: -6rem;
}
@@ -1453,7 +1516,7 @@
left: 0;
right: 0;
width: 100%;
height: 22%;
height: 15%;
pointer-events: none;
z-index: 2;
}
@@ -1660,7 +1723,7 @@
.layout-mobile .spotlight-overlay {
width: calc(100% - 2rem) !important;
background: linear-gradient(to bottom, transparent 0%, rgba(0, 0, 0, 0.4) 40%, rgba(0, 0, 0, 0.85) 100%) !important;
padding: 4rem 1rem 2rem 1rem !important;
padding: 4rem 1rem 1rem 1rem !important;
align-items: start;
align-content: start;
grid-template-rows: auto auto 1fr minmax(48px, auto);
@@ -1677,7 +1740,7 @@
}
.layout-mobile .spotlight-section[data-layout="Borderless"] .spotlight-section-title-container {
top: 6rem;
top: 4rem;
}
.layout-mobile .spotlight-section .spotlight-section-title-container {
@@ -1686,6 +1749,7 @@
left: unset !important;
justify-content: center;
margin: 0 auto !important;
display: grid;
}
.layout-mobile .metadata-container {
@@ -1716,9 +1780,20 @@
gap: 0px !important;
}
.layout-mobile .spotlight-buttons-container .starRatingContainer {
display: none !important;
}
.layout-mobile .spotlight-section .spotlight-buttons-container.mainDetailButtons .btnPlay {
margin: 0.3em !important;
}
.spotlight-section .spotlight-buttons-container.mainDetailButtons .btnPlay {
right: 0px !important;
margin: 0.3em !important;
margin-top: 0 !important;
min-width: unset;
flex: unset !important;
margin-right: 0 !important;
}
.spotlight-section .spotlight-buttons-container.mainDetailButtons {
@@ -1736,8 +1811,6 @@
margin-right: 0px !important;
width: auto !important;
position: relative !important;
top: initial !important;
left: initial !important;
transform: unset !important;
}
@@ -1788,7 +1861,7 @@
}
.spotlight-section[data-layout="Borderless"][data-size="normal"] {
margin-top: calc(-1 * calc(var(--spotlight-fullscreen-vertical) * 2));
margin-top: calc(-1 * calc(var(--spotlight-fullscreen-vertical) * 1.5));
margin-bottom: calc(-1 * calc(var(--spotlight-fullscreen-vertical) * 2));
}
.spotlight-section[data-layout="Borderless"][data-size="normal"] .spotlight-banner-container {
@@ -1859,7 +1932,7 @@
display: none;
}
.spotlight-section[data-size="large"] .spotlight-overlay {
grid-template-rows: 2fr auto minmax(auto, 6rem) 1fr;
grid-template-rows: 2fr auto minmax(auto, 6rem) auto;
}
.spotlight-section[data-size="large"] .spotlight-overlay .spotlight-item-logo {
background-position: left bottom;
@@ -1894,8 +1967,8 @@
}
.spotlight-section[data-layout="Border"][data-size="large"] .spotlight-nav-container,
.spotlight-section[data-layout="Border"][data-size="full"] .spotlight-nav-container {
top: 2rem;
right: 2rem;
top: 2rem !important;
right: 2rem !important;
}
.spotlight-section[data-layout="Border"][data-size="normal"] .spotlight-section-title a {
font-size: 1rem !important;
@@ -1929,8 +2002,7 @@
}
.layout-mobile .section-controls-inline .show-all-button,
.layout-mobile .section-controls-inline .section-refresh-button,
.layout-mobile .section-controls-inline .section-configure-button {
.layout-mobile .section-controls-inline .section-refresh-button {
opacity: 1;
visibility: visible;
}
@@ -1963,6 +2035,7 @@
.layout-mobile .spotlight-dots {
bottom: 1rem !important;
display: none;
}
.layout-mobile .spotlight-section[data-layout="Borderless"] .spotlight-dots {
@@ -2220,10 +2293,6 @@ html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels"
display: none !important;
}
.emby-scroller-container:has(.itemsContainer[data-layout="grid"]) .show-all-button {
display: initial !important;
}
:has(.spotlight-section[data-layout="Borderless"]) .homeSectionsContainer > *:not(.spotlight-section) {
z-index: 1;
}
@@ -2264,6 +2333,7 @@ html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels"
.layout-mobile .spotlight-section .spotlight-section-title.sectionTabs:has(.emby-tab-button:not(.hide)) > * {
display: block !important;
padding: 0.25em 0.5em !important;
}
.layout-mobile .spotlight-section[data-hide-section-name="true"] .spotlight-section-title,
+165 -30
View File
@@ -14,7 +14,8 @@
const SPOTLIGHT_WINDOW_NEXT = 4;
const state = {
defaultCardBackgroundNumber: 1
defaultCardBackgroundNumber: 1,
useBlurhash: false
}
function getSpotlightWindowIndices(currentIndex, totalLength) {
@@ -2223,13 +2224,14 @@
// Append one section per frame so scroll/compositor get frames between paints.
// Build already happened in the fragment; only the container append is paced.
const sectionNodes = Array.from(fragment.childNodes);
/* const sectionNodes = Array.from(fragment.childNodes);
for (let i = 0; i < sectionNodes.length; i++) {
container.appendChild(sectionNodes[i]);
if (i + 1 < sectionNodes.length) {
await new Promise((resolve) => requestAnimationFrame(resolve));
}
}
} */
container.appendChild(fragment);
syncAttachedGridTilesLayouts(container);
// Update the scroll buttons for all scrollable containers
@@ -4454,7 +4456,7 @@
// Watchlist button (material icon span button with bookmark icon)
const watchlistButton = document.createElement('button');
watchlistButton.className = 'emby-button button-flat';
watchlistButton.className = 'emby-button button-flat btnWatchlist';
// Check watchlist status - try to get from cache or API
let isInWatchlist = false;
@@ -5152,8 +5154,8 @@
grid: 'grid_view'
};
const ITEMS_LAYOUT_NEXT_TITLE = {
row: 'Show as grid',
grid: 'Show as scrollable row'
row: 'Grid layout',
grid: 'Row layout'
};
function resolveItemsLayout(sectionConfig) {
@@ -5526,7 +5528,7 @@
definitions.push({
id: 'refresh',
icon: 'refresh',
label: 'Refresh Section',
label: 'Refresh',
className: 'section-refresh-button',
isVisible: () => true,
onClick: async (e) => {
@@ -5541,7 +5543,7 @@
id: 'showAll',
icon: 'grid_view',
getLabel: () => getShowAllControlLabel(sectionElement),
label: 'Show as grid',
label: 'Grid layout',
className: 'show-all-button',
isVisible: () => true,
onClick: (e) => {
@@ -5591,26 +5593,23 @@
const newMoreButton = moreButton.cloneNode(true);
moreButton.replaceWith(newMoreButton);
const MORE_CONTROLS_MODAL_ID = 'kefin-section-controls-more';
let activeMorePopover = null;
const closeMorePopover = () => {
if (window.ModalSystem?.isOpen?.(MORE_CONTROLS_MODAL_ID)) {
window.ModalSystem.close(MORE_CONTROLS_MODAL_ID);
}
if (activeMorePopover) {
activeMorePopover.remove();
activeMorePopover = null;
}
};
newMoreButton.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
if (activeMorePopover) {
closeMorePopover();
return;
}
const buildMorePopoverContent = (onActivate, { forModal = false } = {}) => {
const popover = document.createElement('div');
popover.className = 'kefinTweaks-popover section-controls-more-popover itemDetailsGroup';
popover.className = forModal
? 'kefinTweaks-popover section-controls-more-menu'
: 'kefinTweaks-popover section-controls-more-popover';
visibleDefinitions.forEach((def) => {
const label = typeof def.getLabel === 'function' ? def.getLabel() : def.label;
const item = document.createElement('div');
@@ -5632,7 +5631,7 @@
const activate = (ev) => {
ev.preventDefault();
ev.stopPropagation();
closeMorePopover();
onActivate();
def.onClick(ev, newMoreButton);
};
item.addEventListener('click', activate);
@@ -5643,7 +5642,82 @@
});
popover.appendChild(item);
});
return popover;
};
const positionMoreModal = (modal, anchorBtn) => {
if (!modal?.dialog || !anchorBtn) return;
modal.dialog.classList.remove('centeredDialog', 'formDialog', 'smoothScrollY', 'dialog-fixedSize');
modal.dialog.style.position = 'fixed';
modal.dialog.style.margin = '0';
modal.dialog.style.maxHeight = 'none';
modal.dialog.style.height = 'auto';
modal.dialog.style.minHeight = 'auto';
modal.dialog.style.minWidth = '12rem';
modal.dialog.style.width = 'auto';
modal.dialog.style.padding = '0.25em 0';
modal.dialog.style.animation = '160ms ease-out 0s 1 normal both running scaleup';
if (modal.dialogContent) {
modal.dialogContent.style.padding = '0';
modal.dialogContent.style.overflow = 'visible';
modal.dialogContent.style.minHeight = 'auto';
modal.dialogContent.style.height = 'auto';
modal.dialogContent.style.flex = '0 0 auto';
}
if (modal.backdrop) {
modal.backdrop.style.background = 'transparent';
}
if (modal.dialogContainer) {
modal.dialogContainer.style.pointerEvents = 'none';
}
modal.dialog.style.pointerEvents = 'auto';
const btnRect = anchorBtn.getBoundingClientRect();
const margin = 8;
const gap = 4;
requestAnimationFrame(() => {
const menuHeight = modal.dialog.offsetHeight || 0;
const menuWidth = Math.max(modal.dialog.offsetWidth || 0, 180);
let top = btnRect.bottom + gap;
if (top + menuHeight > window.innerHeight - margin) {
top = Math.max(margin, btnRect.top - menuHeight - gap);
}
let left = btnRect.right - menuWidth;
left = Math.min(Math.max(left, margin), window.innerWidth - menuWidth - margin);
modal.dialog.style.top = `${top}px`;
modal.dialog.style.left = `${left}px`;
});
};
newMoreButton.addEventListener('click', (e) => {
e.stopPropagation();
e.preventDefault();
if (activeMorePopover || window.ModalSystem?.isOpen?.(MORE_CONTROLS_MODAL_ID)) {
closeMorePopover();
return;
}
if (isMobileLayout() && window.ModalSystem?.create) {
const content = buildMorePopoverContent(() => closeMorePopover(), { forModal: true });
window.ModalSystem.create({
id: MORE_CONTROLS_MODAL_ID,
title: '',
content,
showCloseButton: false,
closeOnBackdrop: true,
closeOnEscape: true,
fixedSize: false,
onOpen: (modal) => {
if (modal?.dialogHeader) modal.dialogHeader.style.display = 'none';
if (modal?.dialogFooter) modal.dialogFooter.style.display = 'none';
positionMoreModal(modal, newMoreButton);
}
});
return;
}
const popover = buildMorePopoverContent(() => closeMorePopover());
titleContainer.style.position = 'relative';
newMoreButton.parentNode.insertBefore(popover, newMoreButton.nextSibling);
@@ -5816,13 +5890,68 @@
}
}
leftButton.addEventListener('click', () => {
function getScrollCards() {
const itemsContainer = scroller.querySelector('.itemsContainer');
if (!itemsContainer) return;
const scrollAmount = itemsContainer.clientWidth - itemsContainer.offsetLeft;
if (!itemsContainer) return [];
return Array.from(itemsContainer.querySelectorAll(':scope > .card:not(.card-layout-dummy):not(.skeleton-card)'));
}
const scrollerPadding = (() => {
const style = window.getComputedStyle(scroller);
return {
left: parseFloat(style.paddingLeft) || 0,
right: parseFloat(style.paddingRight) || 0
};
})();
function getCardScrollLeft(card) {
// Scroller + cards share the same translateX, so this delta is layout offset
// within the border box. Subtract paddingLeft so snap aligns to the padded
// content gutter (same inset as card 0 at scroll 0), not the page edge.
const scrollerRect = scroller.getBoundingClientRect();
const cardRect = card.getBoundingClientRect();
return cardRect.left - scrollerRect.left - scrollerPadding.left;
}
/**
* Cards fully inside the padded content clip (not the browser viewport / border box).
* Undo translateX, then inset by scroller padding so visibility matches snap targets.
*/
function getFullyVisibleCardIndices(cards) {
const fullyVisible = [];
const epsilon = 1;
const scrollerRect = scroller.getBoundingClientRect();
const currentPosition = getCurrentPosition();
const newPosition = scrollAmount > currentPosition ? 0 : currentPosition - scrollAmount;
setPosition(newPosition, { animate: true });
const contentWidth = Math.max(
0,
scroller.clientWidth - scrollerPadding.left - scrollerPadding.right
);
const visibleLeft = scrollerRect.left + currentPosition + scrollerPadding.left;
const visibleRight = visibleLeft + contentWidth;
cards.forEach((card, index) => {
const cardRect = card.getBoundingClientRect();
if (cardRect.width <= 0) return;
if (cardRect.left >= visibleLeft - epsilon && cardRect.right <= visibleRight + epsilon) {
fullyVisible.push(index);
}
});
return fullyVisible;
}
function snapScrollToCardIndex(cards, targetIndex) {
if (!cards.length) return;
const clampedIndex = Math.max(0, Math.min(targetIndex, cards.length - 1));
const targetLeft = getCardScrollLeft(cards[clampedIndex]);
setPosition(targetLeft, { animate: true });
}
leftButton.addEventListener('click', () => {
const cards = getScrollCards();
if (!cards.length) return;
const fullyVisible = getFullyVisibleCardIndices(cards);
const firstVisible = fullyVisible.length ? fullyVisible[0] : 0;
const step = Math.max(1, fullyVisible.length);
snapScrollToCardIndex(cards, firstVisible - step);
});
scrollButtons.appendChild(leftButton);
@@ -5839,11 +5968,16 @@
rightButton.appendChild(rightSpan);
rightButton.addEventListener('click', () => {
const itemsContainer = scroller.querySelector('.itemsContainer');
if (!itemsContainer) return;
const scrollAmount = itemsContainer.clientWidth - itemsContainer.offsetLeft;
const currentPosition = getCurrentPosition();
setPosition(currentPosition + scrollAmount, { animate: true });
const cards = getScrollCards();
if (!cards.length) return;
const fullyVisible = getFullyVisibleCardIndices(cards);
const lastVisible = fullyVisible.length ? fullyVisible[fullyVisible.length - 1] : -1;
const nextIndex = lastVisible + 1;
if (nextIndex >= cards.length) {
setPosition(getMaxPosition(), { animate: true });
return;
}
snapScrollToCardIndex(cards, nextIndex);
});
scrollButtons.appendChild(rightButton);
@@ -7631,6 +7765,7 @@
await window.userHelper.waitForLogin();
}
startCardUserDataListener();
state.useBlurhash = localStorage.getItem(`${ApiClient.getCurrentUserId()}-blurhash`) === 'true' || false;
}
initialize();
+66 -38
View File
@@ -164,22 +164,6 @@ div#discovery-loading-indicator:not(.show)::after {
}
}
/* Hide next up section if next up if KefinTweaks next up section is present */
/* .homeSectionsContainer:has([data-section-id="nextUp"]) > div:nth-child(-n+10):has(a[href*="nextup"]) {
display: none;
} */
/* Hide continue watching section if continue watching if KefinTweaks continue watching section is present */
/* .homeSectionsContainer:has([data-section-id="continueWatching"]) > div:nth-child(-n+10):has([data-positionticks][data-mediatype="Video"]) {
display: none;
} */
/* Hide recently added section if recently added if KefinTweaks recently added section is present */
/* .homeSectionsContainer:has([data-section-id*="recently-added"]) > div:nth-child(-n+10):has(a[href*="movies&tab=1"]),
.homeSectionsContainer:has([data-section-id*="recently-added"]) > div:nth-child(-n+10):has(a[href*="tv&tab=1"]),
.homeSectionsContainer:has([data-section-id*="recently-added"]) > div:nth-child(-n+10):has(a[href*="music&tab=1"])
{
display: none;
} */
/* Hide scroll buttons on mobile using Jellyfin's layout classes */
.layout-mobile .emby-scrollbuttons,
.layout-tv .emby-scrollbuttons {
@@ -427,8 +411,11 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
pointer-events: auto;
}
.kefin-home-create-section {
left: max(0.75rem, env(safe-area-inset-left));
bottom: max(0.75rem, env(safe-area-inset-bottom));
left: 0;
bottom: 0.25em;
}
.kefin-home-create-section-btn {
padding: 0.25em;
}
.kefin-home-create-section-btn .material-icons {
font-size: 1.75rem;
@@ -441,27 +428,22 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
}
/* Inner visual bar — keep our layout over skinHeader / MuiAppBar positioning */
#kefin-home-category-rail .kefin-home-category-rail {
position: fixed;
z-index: 12;
top: max(4.5rem, calc(env(safe-area-inset-top) + 4.5rem));
left: max(0.5rem, env(safe-area-inset-left));
right: auto;
bottom: auto;
width: auto;
height: auto;
max-width: none;
display: flex;
flex-direction: column;
.kefin-home-category-rail {
--AppBar-background: var(--jf-palette-AppBar-defaultBg);
--AppBar-color: var(--jf-palette-text-primary);
background-color: var(--jf-palette-AppBar-darkBg, var(--AppBar-background));
color: var(--jf-palette-AppBar-darkColor, var(--AppBar-color));
z-index: 10;
gap: 0.25rem;
border-radius: 999px;
box-shadow: 1px 1px 2px 0 #fff !important;
transform: none;
pointer-events: none;
pointer-events: auto;
display: flex;
}
#kefin-home-category-rail .kefin-home-category-rail:hover,
#kefin-home-category-rail .kefin-home-category-rail:focus-within {
opacity: 1;
background: #0f0f0f !important;
border-radius: 0 25px 25px 0;
box-shadow: 1px 0px 5px -1px black;
}
.kefin-home-category-btn {
opacity: 0.75;
@@ -473,6 +455,7 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
}
.kefin-home-category-btn .material-icons {
font-size: 1.35rem;
top: unset !important;
}
@media (max-width: 899px) {
@@ -486,15 +469,18 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
align-items: center;
gap: 0.15rem;
padding: 0.4rem 0.5rem calc(0.4rem + env(safe-area-inset-bottom, 0px)) !important;
box-shadow: 1px 1px 1px 1px #fff !important;
border-radius: 0;
opacity: 1;
pointer-events: auto;
margin: 0 !important;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
border: none !important;
}
.kefin-home-create-section {
bottom: calc(3.25rem + env(safe-area-inset-bottom, 0px));
.kefin-home-create-section,
#kefin-home-create-section-btn {
display: none !important;
}
.homePage:not(.hide) {
padding-bottom: calc(3.5rem + env(safe-area-inset-bottom, 0px));
@@ -502,9 +488,51 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
}
.kefin-section-more-toggle {
margin-left: auto;
margin-left: auto !important;
}
body:not(:has(.homePage:not(.hide))) div#kefin-home-category-rail {
body:not(:has(.homePage:not(.hide) #homeTab.is-active)) div#kefin-home-category-rail,
body:not(:has(.homePage:not(.hide) #homeTab.is-active)) div#kefin-home-create-section-btn {
display: none;
}
@media (min-width: 900px) {
#kefin-home-category-rail .kefin-home-category-rail,
#kefin-home-category-rail .kefin-home-category-rail::before {
background: none !important;
background-color: transparent !important;
}
#kefin-home-category-rail .kefin-home-category-rail {
position: fixed !important;
z-index: 10;
top: 50vh !important;
left: 0 !important;
right: auto !important;
bottom: auto !important;
width: auto !important;
height: auto !important;
max-width: none !important;
flex-direction: column !important;
transform: translateY(-50%);
overflow: visible !important;
padding: 5px 0 !important;
margin: 0 !important;
backdrop-filter: none;
border: none !important;
display: grid !important;
grid-auto-rows: 1fr;
}
}
body:not(:has(#slides-container)):has(.MuiBox-root) .homeSectionsContainer:has([data-section-id]) {
margin-top: 3rem;
}
#kefin-home-category-rail .kefin-home-category-rail {
height: auto !important;
}
body:has(.kefin-home-category-btn.is-active[data-category-id="discovery"]) .sections.homeSectionsContainer::before {
content: "Discover Something To Watch";
padding-left: max(env(safe-area-inset-left), 3.3%);
font-size: 1.5rem;
}
+118 -84
View File
@@ -113,20 +113,26 @@
recoveryRenderEnd: null
};
// Create loading indicator element
// Create loading indicator element (always last child of the sections container)
function createDiscoveryLoadingIndicator(container) {
let loadingDiv = document.querySelector('.homePage:not(.hide) #discovery-loading-indicator')
|| document.querySelector('.libraryPage:not(.hide) #discovery-loading-indicator');
if (loadingDiv) {
return loadingDiv;
}
// Prefer the provided container, but fall back to the standard sections container
if (!container) {
container = document.querySelector('.homePage:not(.hide) #homeTab .sections')
|| document.querySelector('.libraryPage:not(.hide) .homeSectionsContainer');
}
let loadingDiv = document.querySelector('.homePage:not(.hide) #discovery-loading-indicator')
|| document.querySelector('.libraryPage:not(.hide) #discovery-loading-indicator');
if (loadingDiv) {
if (container && loadingDiv.parentElement !== container) {
container.appendChild(loadingDiv);
} else if (container && container.lastElementChild !== loadingDiv) {
container.appendChild(loadingDiv);
}
return loadingDiv;
}
if (!container) {
WARN('Discovery loading indicator: sections container not found');
return null;
@@ -146,6 +152,10 @@
return loadingDiv;
}
function removeDiscoveryLoadingIndicator() {
document.querySelectorAll('#discovery-loading-indicator').forEach((el) => el.remove());
}
function getDiscoveryHomePageEl() {
return document.querySelector('#indexPage')
|| document.querySelector('.homePage:not(.hide)')
@@ -260,7 +270,7 @@
return homeSectionsContainer;
}
const HOME_SECTIONS_SELECTOR = '.homePage:not(.hide) #homeTab .sections';
const HOME_SECTIONS_SELECTOR = '.homePage:not(.hide) #homeTab.is-active .sections';
let homeSectionsReadyObserver = null;
function disconnectHomeSectionsReadyObserver() {
@@ -433,7 +443,7 @@
performanceMetrics.initialRenderStart = performance.now();
mountCreateSectionButton();
mountCategoryRail();
// Defer mountCategoryRail until pinned availability is known (avoids first-load tab jump)
syncHomeScreenChromeVisibility();
// Reset cache/metrics for a new initialization cycle
@@ -476,20 +486,18 @@
const kefinTweaksHomeSections = document.querySelectorAll('.homePage:not(.hide) #homeTab .homeSectionsContainer [data-section-id]');
if (kefinTweaksHomeSections.length > 0) {
LOG('Home screen already initialized and Jellyfin has rendered');
return;
}
// Check if another page container exists with an already rendered KefinTweaks home screen
const otherPageContainer = document.querySelector('.pageContainer:not(.hide) .homeSectionsContainer[data-sections-rendered="true"]');
if (otherPageContainer) {
// Copy the existing home sections with [data-section-id] from that container into our target container instead of re-rendering it
const otherPageSections = otherPageContainer.querySelectorAll('[data-section-id]');
const homeScreenFragment = document.createDocumentFragment();
otherPageSections.forEach(section => {
homeScreenFragment.appendChild(section.cloneNode(true));
});
container.appendChild(homeScreenFragment);
LOG('Cloned existing home sections from a previous container');
try {
updatePinnedCategoryAvailability(
Array.from(kefinTweaksHomeSections).map((el) => ({
id: el.dataset.sectionId,
category: el.dataset.category
})),
null
);
refreshHomeCategoryChrome();
} catch (err) {
ERR('Failed to sync home category chrome for existing home:', err);
}
return;
}
@@ -535,6 +543,8 @@
}
updatePinnedCategoryAvailability(filteredHomeSections, homeScreen);
mountCategoryRail();
syncHomeScreenChromeVisibility();
performanceEndTime = performance.now();
performanceDuration = performanceEndTime - performanceStartTime;
@@ -1485,12 +1495,18 @@
const config = await window.KefinHomeScreen.getConfig();
const revealSectionsSequentially = config.DISCOVERY_SETTINGS?.fadeInSections === true;
// Park the indicator so newly appended sections are not inserted after it in the DOM
removeDiscoveryLoadingIndicator();
await window.cardBuilder.renderProgressiveSections(container, bufferedSections, {
revealSectionsSequentially,
enhanceOnVisible: true,
showStaleDataBeforeRefresh: config.HOME_SETTINGS?.showStaleDataBeforeRefresh === true
});
createDiscoveryLoadingIndicator(container);
container.dataset.loadingDiscovery = 'false';
container.classList.remove('loading-discovery');
@@ -1504,6 +1520,7 @@
} catch (e) {
ERR('Error rendering discovery group:', e);
state.discoveryBuffer = null;
createDiscoveryLoadingIndicator(container);
} finally {
state.isRenderingDiscovery = false;
if (state.discoverySectionsRemain !== false) {
@@ -1844,13 +1861,14 @@
const HOME_CHROME_CREATE_ID = 'kefin-home-create-section-btn';
const HOME_CHROME_CATEGORY_RAIL_ID = 'kefin-home-category-rail';
const HOME_CHROME_CATEGORY_STYLE_ID = 'kefin-home-category-filter-styles';
const HOME_CHROME_CLASSIC_RAIL_CLASSES = 'skinHeader skinHeader-withBackground emby-tab-button';
const HOME_CHROME_CLASSIC_RAIL_CLASSES = 'skinHeader skinHeader-withBackground emby-tabs-slider emby-tab-button';
const HOME_CHROME_MODERN_RAIL_CLASSES = 'MuiPaper-root MuiPaper-elevation MuiPaper-elevation1 MuiAppBar-root MuiAppBar-colorDefault MuiAppBar-positionFixed mui-fixed';
let homeChromeActiveCategory = 'none';
let homeChromeCreateMounted = false;
let homeChromeCreateMounting = false;
let homeChromeCategoryMounted = false;
let homeChromeHasPinnedContent = false;
let homeChromeVisibilityObserver = null;
let homeChromeMuiAppBarObserver = null;
function getHomeSettings() {
return window.KefinTweaksConfig?.homeScreenConfig?.HOME_SETTINGS
@@ -1873,10 +1891,8 @@
}
function updatePinnedCategoryAvailability(sections, homeScreen) {
const userHomeConfig = window.KefinUserHomeScreenConfig;
const fromUserPins = (userHomeConfig?.buildPinnedSectionConfigs?.(homeScreen) || []).length > 0;
const fromSections = (sections || []).some(sectionIsPinnedCategory);
homeChromeHasPinnedContent = fromUserPins || fromSections;
const fromSections = (sections || []).some(s => sectionIsPinnedCategory(s) && s.enabled !== false);
homeChromeHasPinnedContent = fromSections;
}
function getRailCategories() {
@@ -1887,34 +1903,17 @@
}
function isModernUIChrome() {
return document.querySelector('.MuiBox-root') !== null || localStorage.getItem('layout')?.length === 0;
return !!document.querySelector('main.MuiBox-root') || localStorage.getItem('layout')?.length === 0;
}
function getCategoryRailNativeClasses() {
const base = 'kefin-home-chrome kefin-home-category-rail';
if (isModernUIChrome()) {
let appBar = document.querySelector('.MuiAppBar-root');
if (appBar?.className) {
return `${base} ${appBar.className}`;
}
return `${base} ${HOME_CHROME_MODERN_RAIL_CLASSES}`;
}
return `${base} ${HOME_CHROME_CLASSIC_RAIL_CLASSES}`;
}
function ensureMuiAppBarClassObserver() {
if (!isModernUIChrome() || homeChromeMuiAppBarObserver) return;
if (document.querySelector('.MuiAppBar-root')) return;
homeChromeMuiAppBarObserver = new MutationObserver(() => {
if (!document.querySelector('.MuiAppBar-root')) return;
try { homeChromeMuiAppBarObserver?.disconnect(); } catch (_) { /* ignore */ }
homeChromeMuiAppBarObserver = null;
const inner = document.querySelector(`#${HOME_CHROME_CATEGORY_RAIL_ID} .kefin-home-category-rail`);
if (inner) inner.className = getCategoryRailNativeClasses();
});
homeChromeMuiAppBarObserver.observe(document.body, { childList: true, subtree: true });
}
function isHomePageVisible() {
return !!document.querySelector('.homePage:not(.hide)');
}
@@ -1950,38 +1949,57 @@
}
async function mountCreateSectionButton() {
if (homeChromeCreateMounted || document.getElementById(HOME_CHROME_CREATE_ID)) {
homeChromeCreateMounted = true;
const existingNodes = document.querySelectorAll(`#${HOME_CHROME_CREATE_ID}`);
if (existingNodes.length > 1) {
existingNodes.forEach((el, idx) => {
if (idx > 0) el.remove();
});
}
if (homeChromeCreateMounted || homeChromeCreateMounting || document.getElementById(HOME_CHROME_CREATE_ID)) {
homeChromeCreateMounted = !!document.getElementById(HOME_CHROME_CREATE_ID);
return;
}
if (getHomeSettings().showCreateSectionButtonOnHome !== true) return;
try {
const isAdmin = !!(await window.apiHelper?.isAdmin?.());
if (!isAdmin) return;
} catch {
return;
}
const wrap = document.createElement('div');
wrap.id = HOME_CHROME_CREATE_ID;
wrap.className = 'kefin-home-chrome kefin-home-create-section';
wrap.hidden = !isHomePageVisible();
wrap.innerHTML = `
<button type="button" class="paper-icon-button-light emby-button kefin-home-create-section-btn" title="Create Section" aria-label="Create Section">
<span class="material-icons" aria-hidden="true">add_circle</span>
</button>
`;
wrap.querySelector('button')?.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
homeChromeCreateMounting = true;
try {
let isAdmin = false;
try {
window.KefinHomeScreen?.openNewCustomSection?.({ refreshHomeOnSave: true });
} catch (err) {
ERR('Failed to open create section from home:', err);
isAdmin = !!(await window.apiHelper?.isAdmin?.());
} catch {
return;
}
});
document.body.appendChild(wrap);
homeChromeCreateMounted = true;
if (!isAdmin) return;
// Re-check after await — another caller may have mounted meanwhile
if (document.getElementById(HOME_CHROME_CREATE_ID)) {
homeChromeCreateMounted = true;
return;
}
const wrap = document.createElement('div');
wrap.id = HOME_CHROME_CREATE_ID;
wrap.className = 'kefin-home-chrome kefin-home-create-section';
wrap.hidden = !isHomePageVisible();
wrap.innerHTML = `
<button type="button" class="paper-icon-button-light emby-button kefin-home-create-section-btn" title="Create Section" aria-label="Create Section">
<span class="material-icons" aria-hidden="true">add_circle</span>
</button>
`;
wrap.querySelector('button')?.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
try {
window.KefinHomeScreen?.openNewCustomSection?.({ refreshHomeOnSave: true });
} catch (err) {
ERR('Failed to open create section from home:', err);
}
});
document.body.appendChild(wrap);
homeChromeCreateMounted = true;
} finally {
homeChromeCreateMounting = false;
}
}
function buildCategoryFilterStyles(categories) {
@@ -2017,7 +2035,6 @@
function applyActiveHomeCategory(categoryId) {
const categories = getRailCategories();
const next = categories.some((c) => c.id === categoryId) ? categoryId : 'none';
const prev = homeChromeActiveCategory || 'none';
homeChromeActiveCategory = next;
const separate = getHomeSettings().renderCategoriesSeparately === true;
@@ -2031,11 +2048,12 @@
rail.querySelectorAll('.kefin-home-category-btn').forEach((btn) => {
const active = btn.dataset.categoryId === next;
btn.classList.toggle('is-active', active);
btn.classList.toggle('emby-tab-button-active', active);
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
});
}
if (next === 'discovery' && prev !== 'discovery' && isDiscoveryAllowedForActiveCategory()) {
if (next === 'discovery' && isDiscoveryAllowedForActiveCategory()) {
const container = document.querySelector('.libraryPage:not(.hide) .homeSectionsContainer, .homePage:not(.hide) .homeSectionsContainer');
if (container) {
const hasDiscovery = !!(
@@ -2089,7 +2107,7 @@
outer.hidden = !isHomePageVisible();
outer.setAttribute('role', 'toolbar');
outer.setAttribute('aria-label', 'Home categories');
outer.setAttribute('aria-hidden', outer.hidden ? 'true' : 'false');
//outer.setAttribute('aria-hidden', outer.hidden ? 'true' : 'false');
const inner = document.createElement('div');
inner.className = getCategoryRailNativeClasses();
@@ -2100,12 +2118,12 @@
const active = desiredCategory === cat.id;
return `
<button type="button"
class="paper-icon-button-light emby-button kefin-home-category-btn${active ? ' is-active' : ''}"
class="paper-icon-button-light emby-button kefin-home-category-btn${active ? ' is-active emby-tab-button-active' : ''}"
data-category-id="${id}"
title="${name}"
aria-label="${name}"
aria-pressed="${active ? 'true' : 'false'}">
<span class="material-icons" aria-hidden="true">${icon}</span>
<span class="material-icons emby-button-foreground ${icon}" aria-hidden="true"></span>
</button>
`;
}).join('');
@@ -2121,16 +2139,32 @@
document.body.appendChild(outer);
homeChromeCategoryMounted = true;
ensureMuiAppBarClassObserver();
applyActiveHomeCategory(desiredCategory);
}
function getMountedRailCategoryIds() {
const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
if (!rail) return null;
return Array.from(rail.querySelectorAll('.kefin-home-category-btn'))
.map((btn) => btn.dataset.categoryId);
}
function refreshHomeCategoryChrome() {
ensureCategoryFilterStyles();
const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
if (rail) rail.remove();
homeChromeCategoryMounted = false;
mountCategoryRail();
const desired = getRailCategories().map((c) => String(c?.id || ''));
const mounted = getMountedRailCategoryIds();
const same = Array.isArray(mounted)
&& mounted.length === desired.length
&& mounted.every((id, i) => id === desired[i]);
if (!same) {
const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
if (rail) rail.remove();
homeChromeCategoryMounted = false;
mountCategoryRail();
} else {
applyActiveHomeCategory(homeChromeActiveCategory || 'none');
}
syncHomeScreenChromeVisibility();
}
+8 -7
View File
@@ -349,7 +349,7 @@
order: cfg.order,
ttl: cfg.ttl,
cardFormat: cfg.cardFormat,
animationEnabled: spotlight.panAnimation,
animationEnabled: spotlight.panAnimation !== false,
hideName: cfg.hideName === true,
hideCardTitles: cfg.cardTitleVisibility === 'hidden' || cfg.hideCardTitles === true,
cardTitlePosition: cfg.cardTitlePosition,
@@ -450,6 +450,7 @@
function syncRestoreDefaultsButton(btn, dirty) {
if (!btn) return;
btn.hidden = !dirty;
btn.disabled = !dirty;
btn.classList.toggle('is-disabled', !dirty);
btn.setAttribute('aria-disabled', String(!dirty));
@@ -2309,7 +2310,7 @@
<div class="kefin-section-configure-title-container">
<div class="kefin-section-configure-title-row">
<h3>${escapeHtml(sectionConfig.name)}</h3>
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-restore-defaults-btn${sectionDirty ? '' : ' is-disabled'}" title="Restore defaults" aria-label="Restore defaults" ${sectionDirty ? '' : 'disabled'} aria-disabled="${sectionDirty ? 'false' : 'true'}">
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-restore-defaults-btn${sectionDirty ? '' : ' is-disabled'}" title="Restore defaults" aria-label="Restore defaults" ${sectionDirty ? '' : 'hidden disabled'} aria-disabled="${sectionDirty ? 'false' : 'true'}">
<span class="material-icons" aria-hidden="true">settings_backup_restore</span>
</button>
${isPinnedParent ? `
@@ -2714,8 +2715,8 @@
const button = document.createElement('button');
button.type = 'button';
button.className = 'section-configure-button material-icons settings';
button.title = 'Configure Section';
button.setAttribute('aria-label', 'Configure Section');
button.title = 'Configure';
button.setAttribute('aria-label', 'Configure');
button.addEventListener('click', (e) => {
e.preventDefault();
e.stopPropagation();
@@ -2743,7 +2744,7 @@
return {
id: 'configure',
icon: 'settings',
label: 'Configure Section',
label: 'Configure',
className: 'section-configure-button',
isVisible: () => true,
onClick: (e, anchorButton) => {
@@ -2892,8 +2893,7 @@
}
.kefin-section-restore-defaults-btn.is-disabled,
.kefin-section-restore-defaults-btn:disabled {
opacity: 0.4;
pointer-events: none;
display: none !important;
}
.kefin-section-more-toggle[aria-expanded="true"] {
opacity: 1;
@@ -3175,6 +3175,7 @@
attachSectionControls,
createConfigureButton,
getConfigureSectionControl,
openConfigurePopover,
updateRuntimeItemsLayout,
updateRuntimeGridExpanded
};
+11 -3
View File
@@ -163,7 +163,7 @@
name: 'homeScreen-configuration',
script: '../configuration/homeScreen-configuration.js',
css: '../configuration/homeScreen-configuration.css',
dependencies: ['modal', 'toaster', 'utils', 'cardBuilder', 'ui', 'homeScreenCustomItemsEditor', 'homeScreenSectionEditor', 'homescreenBenchmark'],
dependencies: ['modal', 'toaster', 'utils', 'cardBuilder', 'ui', 'homeScreenCustomItemsEditor', 'homeScreenSectionEditor', 'homescreenBenchmark', 'sectionHelper'],
description: 'Home screen configuration for KefinTweaks'
},
{
@@ -377,11 +377,18 @@
dependencies: [],
description: 'Watchlist statistics charts and activity analytics'
},
{
name: 'watchlist-configuration',
script: '../configuration/watchlist-configuration.js',
css: null,
dependencies: ['modal', 'ui'],
description: 'Configuration UI for Watchlist menu placements'
},
{
name: 'watchlist',
script: 'watchlist.js',
css: 'watchlist.css',
dependencies: ['cardBuilder', 'localStorageCache', 'modal', 'utils', 'statistics'],
dependencies: ['cardBuilder', 'localStorageCache', 'modal', 'utils', 'statistics', 'watchlist-configuration'],
thirdParty: ['chartjs'],
description: 'Adds watchlist functionality throughout Jellyfin interface'
},
@@ -544,7 +551,8 @@
'homeScreen-configuration', 'search-configuration', 'seriesEpisodes-configuration',
'seriesInfo-configuration', 'versionPreferences-configuration', 'skinManager-configuration',
'customMenuLinks-configuration', 'thumbnailScrubber-configuration',
'watchTogether-configuration', 'userManager-configuration', 'apiHelper'
'watchTogether-configuration', 'watchlist-configuration', 'userManager-configuration', 'apiHelper',
'sectionHelper'
];
const CONFIGURATION_FILES = { script: 'configuration.js', css: 'configuration.css' };