Add Home Screen Category filters and fix bugs related to Home Screen not rendering when navigating back to it

This commit is contained in:
HighImKevin committed 2026-09-21 03:01:43 -04:00
1 parent ea0483ff92
commit eda0a94420
12 files changed
+1069 -152

No files matched your search

+5 -1
View File
@@ -5,6 +5,7 @@ interface HomeScreenSection {
caption?: string;
enabled: boolean;
order: number;
category?: string; // Stable category id (custom sections); runtime may force discovery/pinned
// Display Options
cardFormat?: "Poster" | "Thumb" | "Backdrop" | "Random";
@@ -31,6 +32,7 @@ interface HomeScreenSection {
cardTitleFontSize?: "small" | "normal" | "large";
userConfigurable?: boolean; // Whether users can toggle/reorder this section
useGaplessCards?: boolean; // When true, remove gaps between cards (row or grid)
useParentCard?: boolean; // When true, use the parent item will be rendered as the card image and link
// Loads items from an external URL and loads matches from that list from your Jellyfin library
// Currently supports: mdblist.com only
@@ -145,7 +147,7 @@ interface SpotlightConfig {
// Used for the customPrefs.kefinTweaks.homeScreen property
// All values are arrays of semi-colon separated strings
interface KefinHomeScreen {
sections: Array<string>; // id;enabled;order;ttl;cardFormat;animationEnabled;hideName;hideCardTitles;cardTitlePosition;borderStyle;spotlightLayout;spotlightSize;spotlightTileCount;itemsLayout(row|grid; legacy true→grid);cardTitleCapitalization;cardTitleFontFamily;cardTitleFontSize;borderColor;cardTitleColor;useGaplessCards
sections: Array<string>; // id;enabled;order;ttl;cardFormat;animationEnabled;hideName;hideCardTitles;cardTitlePosition;borderStyle;spotlightLayout;spotlightSize;spotlightTileCount;itemsLayout(row|grid; legacy true→grid);cardTitleCapitalization;cardTitleFontFamily;cardTitleFontSize;borderColor;cardTitleColor;useGaplessCards;renderMode(Normal|Spotlight|Random)
pinnedLists: Array<string>; // name;id1;id2;id3;...
pinnedParents: Array<string>; // name;id;type
}
@@ -191,4 +193,6 @@ interface KefinHomeScreenSectionUserConfig {
cardTitleCapitalization?: string;
cardTitleFontFamily?: string;
cardTitleFontSize?: string;
/** User override of the section's render mode. */
renderMode?: "Normal" | "Spotlight" | "Random";
}
+41 -32
View File
@@ -2049,16 +2049,24 @@
.emby-scroller-container[data-card-format="Logo"]:not(.visualCardBox) .cardScalable,
.emby-scroller-container[data-card-format="Logo"]:not(.visualCardBox) .cardPadder,
.emby-scroller-container[data-card-format="Logo"]:not(.visualCardBox) .cardBox,
.emby-scroller-container[data-card-format="Clear Art"]:not(.visualCardBox) .cardScalable,
.emby-scroller-container[data-card-format="Clear Art"]:not(.visualCardBox) .cardPadder,
.emby-scroller-container[data-card-format="Clear Art"]:not(.visualCardBox) .cardBox,
.emby-scroller-container[data-card-format="Disc"]:not(.visualCardBox) .cardScalable,
.emby-scroller-container[data-card-format="Disc"]:not(.visualCardBox) .cardPadder,
.emby-scroller-container[data-card-format="Disc"]:not(.visualCardBox) .cardBox,
.emby-scroller-container[data-section-id="liveTv-channels"]:not(.visualCardBox) .cardScalable,
.emby-scroller-container[data-section-id="liveTv-channels"]:not(.visualCardBox) .cardPadder,
.emby-scroller-container[data-section-id="liveTv-channels"]:not(.visualCardBox) .cardBox,
html.sleekfin-main-ui .emby-scroller-container[data-card-format="Logo"]:not(.visualCardBox) .cardPadder,
html.sleekfin-main-ui .emby-scroller-container[data-card-format="Clear Art"]:not(.visualCardBox) .cardPadder,
html.sleekfin-main-ui .emby-scroller-container[data-card-format="Disc"]:not(.visualCardBox) .cardPadder,
html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels"]:not(.visualCardBox) .cardPadder {
html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels"]:not(.visualCardBox) .cardPadder,
html.sleekfin-main-ui .emby-scroller-container[data-card-format="Logo"]:not(.visualCardBox) .cardBox,
html.sleekfin-main-ui .emby-scroller-container[data-card-format="Clear Art"]:not(.visualCardBox) .cardBox,
html.sleekfin-main-ui .emby-scroller-container[data-card-format="Disc"]:not(.visualCardBox) .cardBox,
html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels"]:not(.visualCardBox) .cardBox {
background: transparent !important;
box-shadow: none !important;
border: none !important;
@@ -2192,88 +2200,89 @@ html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels"
display: inline-block;
}
/* .spotlight-section .spotlight-item:has(.spotlight-background-layer-center) .spotlight-background-layer {
width: 37.5%;
.spotlight-section .spotlight-item:has(.spotlight-background-layer-center) .spotlight-background-layer {
width: 37.5%;
}
.emby-scroller-container:has([data-direction="right"]:disabled):has([data-direction="left"]:disabled) button.show-all-button {
display: none;
display: none;
}
.emby-scroller-container .emby-scrollbuttons:has([data-direction="right"]:disabled):has([data-direction="left"]:disabled) {
display: none;
display: none;
}
.spotlight-truncated-list:has(.spotlight-truncated-list-tooltip) {
overflow: visible;
overflow: visible;
}
.emby-scroller-container:has(.itemsContainer[data-layout="grid"]) .emby-scrollbuttons {
display: none !important;
display: none !important;
}
.emby-scroller-container:has(.itemsContainer[data-layout="grid"]) .show-all-button {
display: initial !important;
display: initial !important;
}
:has(.spotlight-section[data-layout="Borderless"]) .homeSectionsContainer > *:not(.spotlight-section) {
z-index: 1;
z-index: 1;
}
.spotlight-section[data-size="full"] .spotlight-truncated-list.itemDetailsGroup:has(>.detailsGroupItem),
.spotlight-section[data-size="full"] .spotlight-truncated-list.sectionTabs:has(>.emby-tab-button) {
font-size: large;
display: flex;
align-items: center;
align-self: start;
.spotlight-section[data-size="full"] .spotlight-truncated-list.itemDetailsGroup:has(> .detailsGroupItem),
.spotlight-section[data-size="full"] .spotlight-truncated-list.sectionTabs:has(> .emby-tab-button) {
font-size: large;
display: flex;
align-items: center;
align-self: start;
}
.spotlight-section .itemDetailsGroup:has(.detailsGroupItem:not(.hide)), .spotlight-section .sectionTabs:not(.spotlight-section-title):has(.emby-tab-button:not(.hide)) {
margin: 0px !important;
padding: 0px !important;
width: initial !important;
min-width: initial !important;
.spotlight-section .itemDetailsGroup:has(.detailsGroupItem:not(.hide)),
.spotlight-section .sectionTabs:not(.spotlight-section-title):has(.emby-tab-button:not(.hide)) {
margin: 0px !important;
padding: 0px !important;
width: initial !important;
min-width: initial !important;
}
.spotlight-section:not(.hide) span.emby-tab-button.emby-tab-button-active:not(:has(a)) {
cursor: default;
cursor: default;
}
.emby-scroller-container:has(.itemsContainer[data-layout="grid"]) button.show-all-button::after {
content: 'Collapse';
content: "Collapse";
}
.layout-mobile .emby-scroller-container:has(.itemsContainer[data-layout="grid"]) button.show-all-button::after {
content: 'keyboard_arrow_up';
content: "keyboard_arrow_up";
}
.layout-mobile .spotlight-section .spotlight-section-title.sectionTabs:has(.emby-tab-button:not(.hide)) {
justify-self: center;
display: flex !important;
justify-content: center;
justify-self: center;
display: flex !important;
justify-content: center;
}
.layout-mobile .spotlight-section .spotlight-section-title.sectionTabs:has(.emby-tab-button:not(.hide)) > * {
display: block !important;
display: block !important;
}
.layout-mobile .spotlight-section[data-hide-section-name="true"] .spotlight-section-title,
.layout-mobile .spotlight-section[data-hide-section-name="true"] .spotlight-section-title.sectionTabs,
.spotlight-section[data-hide-section-name="true"] .spotlight-section-title.sectionTabs {
display: none !important;
display: none !important;
}
.emby-scroller-container:has(.emby-scrollbuttons-button:not(.hide)) button.show-all-button {
display: flex;
display: flex;
}
.layout-mobile .emby-scroller-container:has(.overflowPortraitCard:nth-child(4)) button.show-all-button,
.layout-mobile .emby-scroller-container:has(.overflowBackdropCard:nth-child(3)) button.show-all-button {
display: flex;
display: flex;
}
.emby-scroller-container:has([data-custom-card="true"]) .section-refresh-button {
display: none;
} */
display: none;
}
/* Section-level card title presentation */
.verticalSection .cardText,
+45 -22
View File
@@ -551,6 +551,13 @@
element.removeAttribute('data-section-type');
}
const resolvedCategory = resolveSectionCategory(sectionConfig);
if (resolvedCategory) {
element.setAttribute('data-category', resolvedCategory);
} else {
element.removeAttribute('data-category');
}
if (isCustomSection) element.dataset.customSection = 'true';
else delete element.dataset.customSection;
@@ -558,6 +565,20 @@
else delete element.dataset.discoverySection;
}
function resolveSectionCategory(sectionConfig) {
if (!sectionConfig) return 'none';
const id = String(sectionConfig.id || '');
if (id.startsWith('pinned-list-') || id.startsWith('pinned-parent-')) return 'pinned';
const sectionType = String(sectionConfig.type || '').toLowerCase();
const isDiscovery = sectionConfig.discoveryEnabled === true
|| sectionConfig.discoverySection === true
|| sectionType === 'discovery'
|| sectionType === 'custom-discovery'
|| !!sectionConfig.discoveryType;
if (isDiscovery) return 'discovery';
return sectionConfig.category || 'none';
}
/**
* Updates the scroll buttons for a scrollable container
* @param {HTMLElement} scroller - The scroller element
@@ -4691,31 +4712,33 @@
bannerContainer.appendChild(navContainer);
}
// Pause button (bottom right)
// Pause button (bottom right) — only when nav chrome exists (showNavButtons)
if (autoPlay && items.length > 1) {
const navButtonsContainer = bannerContainer.querySelector('.spotlight-nav-container .spotlight-nav-buttons-container');
const pauseButton = document.createElement('button');
pauseButton.className = 'spotlight-pause-button spotlight-nav-button emby-button';
const pauseIcon = document.createElement('span');
pauseIcon.className = 'material-icons';
pauseIcon.textContent = 'pause';
pauseButton.appendChild(pauseIcon);
pauseButton.addEventListener('click', (e) => {
e.stopPropagation();
isPaused = !isPaused;
if (isPaused) {
if (autoPlayTimer) {
clearTimeout(autoPlayTimer);
autoPlayTimer = null;
if (navButtonsContainer) {
const pauseButton = document.createElement('button');
pauseButton.className = 'spotlight-pause-button spotlight-nav-button emby-button';
const pauseIcon = document.createElement('span');
pauseIcon.className = 'material-icons';
pauseIcon.textContent = 'pause';
pauseButton.appendChild(pauseIcon);
pauseButton.addEventListener('click', (e) => {
e.stopPropagation();
isPaused = !isPaused;
if (isPaused) {
if (autoPlayTimer) {
clearTimeout(autoPlayTimer);
autoPlayTimer = null;
}
pauseIcon.textContent = 'play_arrow';
} else {
startAutoPlay();
pauseIcon.textContent = 'pause';
}
pauseIcon.textContent = 'play_arrow';
} else {
startAutoPlay();
pauseIcon.textContent = 'pause';
}
});
navButtonsContainer.insertBefore(pauseButton, navButtonsContainer.firstChild);
});
navButtonsContainer.insertBefore(pauseButton, navButtonsContainer.firstChild);
}
}
// Slide state container (dots or numeric "N / X")
+125 -1
View File
@@ -67,6 +67,27 @@ div#discovery-loading-indicator:not(.show)::after {
opacity: 1 !important;
}
/* Hide Discover More / loading affordance outside Home + Discover categories */
#indexPage .homeSectionsContainer[data-category]:not([data-category="none"]):not([data-category="discovery"]) #discovery-loading-indicator,
.homePage .homeSectionsContainer[data-category]:not([data-category="none"]):not([data-category="discovery"]) #discovery-loading-indicator,
.libraryPage
.homeSectionsContainer[data-category]:not([data-category="none"]):not([data-category="discovery"])
#discovery-loading-indicator,
#indexPage .homeSectionsContainer[data-category]:not([data-category="none"]):not([data-category="discovery"]) .discovery-loading-indicator,
.homePage .homeSectionsContainer[data-category]:not([data-category="none"]):not([data-category="discovery"]) .discovery-loading-indicator,
.libraryPage
.homeSectionsContainer[data-category]:not([data-category="none"]):not([data-category="discovery"])
.discovery-loading-indicator {
display: none !important;
visibility: hidden !important;
min-height: 0 !important;
height: 0 !important;
overflow: hidden !important;
padding: 0 !important;
margin: 0 !important;
opacity: 0 !important;
}
#indexPage[data-discovery-ready] #discovery-loading-indicator:not(.show) .spinner {
visibility: hidden;
opacity: 0;
@@ -358,7 +379,7 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
text-align: center;
margin: 3em auto;
display: block;
animation: fadeInDelayed 0.1s linear 1s forwards;
animation: fadeInDelayed 0.1s linear 3s forwards;
opacity: 0;
visibility: hidden;
}
@@ -384,3 +405,106 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
width: 30vw !important;
}
}
/* Home screen chrome: create section + category rail */
.kefin-home-chrome {
position: fixed;
z-index: 12;
pointer-events: none;
opacity: 0.18;
transition: opacity 0.2s ease;
}
.kefin-home-chrome:hover,
.kefin-home-chrome:focus-within {
opacity: 1;
}
.kefin-home-chrome[hidden],
#kefin-home-category-rail[hidden] {
display: none !important;
}
.kefin-home-chrome button,
#kefin-home-category-rail button {
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));
}
.kefin-home-create-section-btn .material-icons {
font-size: 1.75rem;
}
/* Outer sectionTabs wrapper — no native header layout */
#kefin-home-category-rail.sectionTabs {
position: static;
display: contents;
}
/* 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;
gap: 0.25rem;
border-radius: 999px;
box-shadow: 1px 1px 2px 0 #fff !important;
transform: none;
pointer-events: none;
}
#kefin-home-category-rail .kefin-home-category-rail:hover,
#kefin-home-category-rail .kefin-home-category-rail:focus-within {
opacity: 1;
}
.kefin-home-category-btn {
opacity: 0.75;
}
.kefin-home-category-btn.is-active {
opacity: 1;
color: var(--theme-primary-color, #00a4dc);
background: rgba(0, 164, 220, 0.18);
}
.kefin-home-category-btn .material-icons {
font-size: 1.35rem;
}
@media (max-width: 899px) {
#kefin-home-category-rail .kefin-home-category-rail {
top: auto;
left: 0;
right: 0;
bottom: 0;
flex-direction: row;
justify-content: space-evenly;
align-items: center;
gap: 0.15rem;
padding: 0.4rem 0.5rem calc(0.4rem + env(safe-area-inset-bottom, 0px)) !important;
border-radius: 0;
opacity: 1;
pointer-events: auto;
margin: 0 !important;
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
}
.kefin-home-create-section {
bottom: calc(3.25rem + env(safe-area-inset-bottom, 0px));
}
.homePage:not(.hide) {
padding-bottom: calc(3.5rem + env(safe-area-inset-bottom, 0px));
}
}
.kefin-section-more-toggle {
margin-left: auto;
}
body:not(:has(.homePage:not(.hide))) div#kefin-home-category-rail {
display: none;
}
+378 -32
View File
@@ -75,7 +75,8 @@
kefinContinueWatching: false,
kefinLatestMedia: false,
jellyfinOrders: {},
homePaintedEventFired: false
homePaintedEventFired: false,
isRenderingHome: false
};
async function fetchDisplayPreferences() {
@@ -427,11 +428,14 @@
* Main Entry Point
*/
async function enhanceHomeScreen() {
LOG('Login detected, continuing with home screen initialization');
LOG('Initializing Home Screen v3...');
performanceTimer.loadTimeStart = performance.now();
performanceMetrics.initialRenderStart = performance.now();
mountCreateSectionButton();
mountCategoryRail();
syncHomeScreenChromeVisibility();
// Reset cache/metrics for a new initialization cycle
sectionCache.renderedSections = [];
sectionCache.fragmentCache = null;
@@ -476,7 +480,7 @@
}
// 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"]');
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]');
@@ -487,7 +491,7 @@
container.appendChild(homeScreenFragment);
LOG('Cloned existing home sections from a previous container');
return;
} */
}
let performanceStartTime = performance.now();
@@ -530,6 +534,8 @@
filteredHomeSections = filteredHomeSections.filter(section => section.enabled !== false);
}
updatePinnedCategoryAvailability(filteredHomeSections, homeScreen);
performanceEndTime = performance.now();
performanceDuration = performanceEndTime - performanceStartTime;
LOG(`Home Screen v3 Load user home screen sections initialization time: ${performanceDuration.toFixed(2)}ms`);
@@ -606,6 +612,12 @@
LOG('Initializing Discovery Sections...');
setupDiscoveryInteraction(container);
try {
refreshHomeCategoryChrome();
} catch (err) {
ERR('Failed to refresh home category chrome after home render:', err);
}
/* // If Jellyfin hasn't rendered after 5 seconds, assume it won't and clean up the observer
setTimeout(() => {
if (!sectionCache.jellyfinHasRendered && observer) {
@@ -1147,9 +1159,6 @@
return;
}
// Skip if we've already rendered into this container for this cycle and we're not explicitly re-using the cache
if (container.dataset.sectionsRendered && !useCache) return;
// Check if children with [data-section-id] are present
const children = container.children;
const hasSections = Array.from(children).some(child => child.dataset && child.dataset.sectionId);
@@ -1159,6 +1168,8 @@
}
container.dataset.sectionsRendered = 'true';
applyActiveHomeCategory(homeChromeActiveCategory || 'none');
syncHomeScreenChromeVisibility();
const sectionsToRender = [];
@@ -1233,34 +1244,47 @@
const homeConfig = await window.KefinHomeScreen.getConfig();
const showStaleDataBeforeRefresh = homeConfig?.HOME_SETTINGS?.showStaleDataBeforeRefresh === true;
const targetContainer = container;
const targetContainer = container;
const renderStartTime = performance.now();
await window.cardBuilder.renderProgressiveSections(targetContainer, sectionsToRender, {
waitForContainerClass: 'homeSectionsContainer',
enhanceOnVisible: true,
showStaleDataBeforeRefresh
});
const renderEndTime = performance.now();
const renderDuration = renderEndTime - renderStartTime;
LOG(`Home Screen v3 Render progressive sections initialization time: ${renderDuration.toFixed(2)}ms`);
// If MediaBar plugin is in use, call LayoutSync.update() to update the layout
if (typeof LayoutSync !== 'undefined' && LayoutSync && typeof LayoutSync.update === 'function') {
LayoutSync.update();
const kefinSections = Array.from(children).some(child => child.dataset && child.dataset.sectionId);
if (kefinSections) {
LOG('Sections already rendered, skipping');
return;
}
// Allow deferred library-cache crawls to start (movies/series/people scheduleBootstrap)
if (!state.homePaintedEventFired) {
state.homePaintedEventFired = true;
try {
document.dispatchEvent(new CustomEvent('kefinTweaksHomePainted'));
} catch (_) { /* ignore */ }
}
if (!state.isRenderingHome && !kefinSections) {
const renderStartTime = performance.now();
state.isRenderingHome = true;
// Check if target container is homeSectionsContainer
if (!targetContainer.classList.contains('homeSectionsContainer')) {
targetContainer.dataset.sectionsPrerendered = 'true';
await window.cardBuilder.renderProgressiveSections(targetContainer, sectionsToRender, {
waitForContainerClass: 'homeSectionsContainer',
enhanceOnVisible: true,
showStaleDataBeforeRefresh
});
state.isRenderingHome = false;
const renderEndTime = performance.now();
const renderDuration = renderEndTime - renderStartTime;
LOG(`Home Screen v3 Render progressive sections initialization time: ${renderDuration.toFixed(2)}ms`);
// If MediaBar plugin is in use, call LayoutSync.update() to update the layout
if (typeof LayoutSync !== 'undefined' && LayoutSync && typeof LayoutSync.update === 'function') {
LayoutSync.update();
}
// Allow deferred library-cache crawls to start (movies/series/people scheduleBootstrap)
if (!state.homePaintedEventFired) {
state.homePaintedEventFired = true;
try {
document.dispatchEvent(new CustomEvent('kefinTweaksHomePainted'));
} catch (_) { /* ignore */ }
}
// Check if target container is homeSectionsContainer
if (!targetContainer.classList.contains('homeSectionsContainer')) {
targetContainer.dataset.sectionsPrerendered = 'true';
}
}
}
@@ -1301,6 +1325,10 @@
* Ensures discovery buffer is populated (pre-fetches next group)
*/
async function ensureDiscoveryBuffer() {
if (!isDiscoveryAllowedForActiveCategory()) {
LOG('Discovery buffer skipped: active category does not allow discovery');
return state.discoveryBuffer;
}
if (state.ensuringDiscoveryBuffer) {
LOG('Already ensuring discovery buffer...');
@@ -1411,6 +1439,11 @@
const container = document.querySelector('.libraryPage:not(.hide) .homeSectionsContainer');
if (!container) return;
if (!isDiscoveryAllowedForActiveCategory()) {
LOG('Discovery render skipped: active category does not allow discovery');
return;
}
// Single-flight: set synchronously before any await
if (state.isRenderingDiscovery) return;
if (state.discoverySectionsRemain === false) return;
@@ -1808,15 +1841,327 @@
await enhanceHomeScreen();
}
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';
let homeChromeActiveCategory = 'none';
let homeChromeCreateMounted = false;
let homeChromeCategoryMounted = false;
let homeChromeHasPinnedContent = false;
let homeChromeVisibilityObserver = null;
let homeChromeMuiAppBarObserver = null;
function getHomeSettings() {
return window.KefinTweaksConfig?.homeScreenConfig?.HOME_SETTINGS
|| window.KefinHomeConfig2?.HOME_SETTINGS
|| {};
}
function getConfiguredCategories() {
const defaults = window.KefinHomeConfig2?.HOME_SETTINGS?.categories || [];
const configured = getHomeSettings().categories;
if (Array.isArray(configured) && configured.length) return configured;
return defaults;
}
function sectionIsPinnedCategory(section) {
const id = String(section?.id || '');
return id.startsWith('pinned-list-')
|| id.startsWith('pinned-parent-')
|| section?.category === 'pinned';
}
function updatePinnedCategoryAvailability(sections, homeScreen) {
const userHomeConfig = window.KefinUserHomeScreenConfig;
const fromUserPins = (userHomeConfig?.buildPinnedSectionConfigs?.(homeScreen) || []).length > 0;
const fromSections = (sections || []).some(sectionIsPinnedCategory);
homeChromeHasPinnedContent = fromUserPins || fromSections;
}
function getRailCategories() {
return getConfiguredCategories().filter((cat) => {
if (cat?.id === 'pinned') return homeChromeHasPinnedContent;
return true;
});
}
function isModernUIChrome() {
return document.querySelector('.MuiBox-root') !== null || 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_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)');
}
function syncHomeScreenChromeVisibility() {
const onHome = isHomePageVisible();
const createBtn = document.getElementById(HOME_CHROME_CREATE_ID);
if (createBtn) {
createBtn.hidden = !onHome;
createBtn.setAttribute('aria-hidden', onHome ? 'false' : 'true');
}
const showFilters = getHomeSettings().showCategoryFilters !== false;
const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
if (rail) {
const visible = onHome && showFilters;
rail.hidden = !visible;
rail.setAttribute('aria-hidden', visible ? 'false' : 'true');
}
if (onHome) {
applyActiveHomeCategory(homeChromeActiveCategory || 'none');
}
}
function ensureHomeChromeVisibilityObserver() {
if (homeChromeVisibilityObserver) return;
homeChromeVisibilityObserver = new MutationObserver(() => syncHomeScreenChromeVisibility());
homeChromeVisibilityObserver.observe(document.body, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ['class']
});
}
async function mountCreateSectionButton() {
if (homeChromeCreateMounted || document.getElementById(HOME_CHROME_CREATE_ID)) {
homeChromeCreateMounted = true;
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();
try {
window.KefinHomeScreen?.openNewCustomSection?.({ refreshHomeOnSave: true });
} catch (err) {
ERR('Failed to open create section from home:', err);
}
});
document.body.appendChild(wrap);
homeChromeCreateMounted = true;
}
function buildCategoryFilterStyles(categories) {
const rules = [];
rules.push(`
.homeSectionsContainer[data-render-categories-separately="true"][data-category="none"] > [data-section-id]:not([data-category="none"]) {
display: none !important;
}
`);
(categories || []).forEach((cat) => {
const id = String(cat?.id || '').trim();
if (!id || id === 'none') return;
const safe = CSS.escape(id);
rules.push(`
.homeSectionsContainer[data-category="${safe}"] > [data-section-id]:not([data-category="${safe}"]) {
display: none !important;
}
`);
});
return rules.join('\n');
}
function ensureCategoryFilterStyles() {
let styleEl = document.getElementById(HOME_CHROME_CATEGORY_STYLE_ID);
if (!styleEl) {
styleEl = document.createElement('style');
styleEl.id = HOME_CHROME_CATEGORY_STYLE_ID;
document.head.appendChild(styleEl);
}
styleEl.textContent = buildCategoryFilterStyles(getConfiguredCategories());
}
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;
document.querySelectorAll('.homeSectionsContainer').forEach((container) => {
container.dataset.category = next;
container.dataset.renderCategoriesSeparately = separate ? 'true' : 'false';
});
const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
if (rail) {
rail.querySelectorAll('.kefin-home-category-btn').forEach((btn) => {
const active = btn.dataset.categoryId === next;
btn.classList.toggle('is-active', active);
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
});
}
if (next === 'discovery' && prev !== 'discovery' && isDiscoveryAllowedForActiveCategory()) {
const container = document.querySelector('.libraryPage:not(.hide) .homeSectionsContainer, .homePage:not(.hide) .homeSectionsContainer');
if (container) {
const hasDiscovery = !!(
container.querySelector('[data-discovery-section="true"], [data-category="discovery"]')
);
if (!hasDiscovery) {
try {
renderNextDiscoveryGroup();
} catch (err) {
ERR('Failed to bootstrap discovery group for Discover category:', err);
}
}
}
}
}
function isDiscoveryAllowedForActiveCategory() {
const separate = getHomeSettings().renderCategoriesSeparately === true;
const active = homeChromeActiveCategory || 'none';
if (separate) return active === 'discovery';
return active === 'none' || active === 'discovery';
}
function mountCategoryRail() {
if (getHomeSettings().showCategoryFilters === false) {
const existing = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
if (existing) existing.remove();
homeChromeCategoryMounted = false;
ensureCategoryFilterStyles();
homeChromeActiveCategory = 'none';
applyActiveHomeCategory('none');
return;
}
if (homeChromeCategoryMounted && document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID)) {
ensureCategoryFilterStyles();
applyActiveHomeCategory(homeChromeActiveCategory || 'none');
return;
}
const categories = getRailCategories();
ensureCategoryFilterStyles();
const desiredCategory = categories.some((c) => c.id === homeChromeActiveCategory)
? homeChromeActiveCategory
: 'none';
const outer = document.createElement('div');
outer.id = HOME_CHROME_CATEGORY_RAIL_ID;
outer.className = 'sectionTabs';
outer.hidden = !isHomePageVisible();
outer.setAttribute('role', 'toolbar');
outer.setAttribute('aria-label', 'Home categories');
outer.setAttribute('aria-hidden', outer.hidden ? 'true' : 'false');
const inner = document.createElement('div');
inner.className = getCategoryRailNativeClasses();
inner.innerHTML = categories.map((cat) => {
const id = escapeHtmlChrome(cat.id);
const name = escapeHtmlChrome(cat.name || cat.id);
const icon = escapeHtmlChrome(cat.icon || 'label');
const active = desiredCategory === cat.id;
return `
<button type="button"
class="paper-icon-button-light emby-button kefin-home-category-btn${active ? ' is-active' : ''}"
data-category-id="${id}"
title="${name}"
aria-label="${name}"
aria-pressed="${active ? 'true' : 'false'}">
<span class="material-icons" aria-hidden="true">${icon}</span>
</button>
`;
}).join('');
outer.appendChild(inner);
outer.addEventListener('click', (e) => {
const btn = e.target.closest('.kefin-home-category-btn');
if (!btn) return;
e.preventDefault();
e.stopPropagation();
applyActiveHomeCategory(btn.dataset.categoryId || 'none');
});
document.body.appendChild(outer);
homeChromeCategoryMounted = true;
ensureMuiAppBarClassObserver();
applyActiveHomeCategory(desiredCategory);
}
function refreshHomeCategoryChrome() {
ensureCategoryFilterStyles();
const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
if (rail) rail.remove();
homeChromeCategoryMounted = false;
mountCategoryRail();
syncHomeScreenChromeVisibility();
}
function escapeHtmlChrome(value) {
return String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
}
async function initHomeScreenChrome() {
ensureHomeChromeVisibilityObserver();
await mountCreateSectionButton();
mountCategoryRail();
syncHomeScreenChromeVisibility();
}
window.homeScreen3 = {
init: enhanceHomeScreen,
refreshHomeSections,
resolveDiscoverySection: buildDiscoverySectionInstance,
sectionHelper: () => window.sectionHelper
sectionHelper: () => window.sectionHelper,
getActiveHomeCategory: () => homeChromeActiveCategory || 'none',
isDiscoveryAllowedForActiveCategory,
refreshHomeCategoryChrome
};
//enhanceHomeScreen();
connectHomeSectionsReadyObserver();
//initHomeScreenChrome();
if (window.KefinTweaksUtils && typeof window.KefinTweaksUtils.onViewPage === 'function') {
window.KefinTweaksUtils.onViewPage((view, element, hash) => {
@@ -1834,6 +2179,7 @@
try {
manageBodyClasses();
enhanceHomeScreen();
syncHomeScreenChromeVisibility();
} catch (err) { ERR('Home screen page change handler failed:', err); }
}, { pages: ['home', 'home.html'] });
}
+6 -3
View File
@@ -2227,6 +2227,9 @@
ensureThumbsForPopularTVNetworks: false,
showStaleDataBeforeRefresh: true,
dismissEmptySectionTimer: 0,
showCreateSectionButtonOnHome: false,
showCategoryFilters: true,
renderCategoriesSeparately: false,
minPeopleAppearancesTotal: 10,
minPeopleAppearancesMovies: 10,
minPeopleAppearancesSeries: 10,
@@ -2236,18 +2239,18 @@
categories: [
{
id: 'none',
name: 'None',
name: 'Home',
icon: 'home',
},
{
id: 'discovery',
id: 'discovery',
name: 'Discover',
icon: 'explore',
},
{
id: 'pinned',
name: 'Pinned',
icon: 'pin',
icon: 'push_pin',
}
]
}
+127 -46
View File
@@ -345,6 +345,7 @@
function buildPrefOverrides(cfg) {
const spotlight = cfg.spotlightConfig || {};
return {
renderMode: resolveRenderMode(cfg),
order: cfg.order,
ttl: cfg.ttl,
cardFormat: cfg.cardFormat,
@@ -394,6 +395,9 @@
function applyServerDefaultsToConfig(cfg, defaults) {
if (!cfg || !defaults) return;
const renderMode = defaults.renderMode || 'Normal';
cfg.renderMode = renderMode;
cfg.spotlight = renderMode === 'Spotlight';
cfg.order = defaults.order;
cfg.ttl = defaults.ttl;
cfg.cardFormat = defaults.cardFormat;
@@ -751,10 +755,38 @@
];
}
function resolveRenderMode(cfg) {
const api = getConfigApi();
if (api?.resolveRenderMode) return api.resolveRenderMode(cfg);
return cfg?.renderMode || (cfg?.spotlight === true ? 'Spotlight' : 'Normal');
}
function isSpotlightSection(cfg) {
return cfg?.renderMode === 'Spotlight' || cfg?.spotlight === true;
}
// 'Random' picks a mode at render time, so a re-render must keep whatever is on screen.
function shouldRenderAsSpotlight(entry) {
if (resolveRenderMode(entry?.effectiveConfig) === 'Random') {
return entry?.element?.classList?.contains('spotlight-section') === true;
}
return isSpotlightSection(entry?.effectiveConfig);
}
// A section that was never spotlight has no spotlightConfig, so the popover controls and
// the renderer would otherwise fall back to different defaults.
function buildSpotlightConfigSeed(existingConfig, serverDefaults = {}) {
const existing = existingConfig || {};
const admin = window.KefinHomeScreen?.getConfig?.()?.SPOTLIGHT_SETTINGS || {};
return {
...admin,
...existing,
spotlightLayout: existing.spotlightLayout ?? admin.spotlightLayout ?? serverDefaults.spotlightLayout ?? 'Border',
spotlightSize: existing.spotlightSize ?? admin.spotlightSize ?? serverDefaults.spotlightSize ?? 'normal',
tileCount: existing.tileCount ?? admin.tileCount ?? serverDefaults.spotlightTileCount ?? 1
};
}
function applyPresentationToElement(entry) {
const cfg = entry.effectiveConfig;
const el = entry.element;
@@ -827,7 +859,7 @@
delete el.dataset.fontSize;
}
if (isSpotlightSection(cfg)) {
if (shouldRenderAsSpotlight(entry)) {
const spotlight = cfg.spotlightConfig || {};
if (spotlight.spotlightLayout) {
el.dataset.layout = spotlight.spotlightLayout;
@@ -846,9 +878,16 @@
}
}
// Structural classes/attributes owned by each render mode's builder. When a section
// switches modes the freshly built root must keep its own set instead of inheriting
// the previous mode's.
const SPOTLIGHT_MODE_CLASSES = ['spotlight-section', 'padded-left'];
const NORMAL_MODE_CLASSES = ['verticalSection', 'emby-scroller-container', 'custom-scroller-container'];
const SPOTLIGHT_MODE_ATTRS = ['data-layout', 'data-size', 'data-tile-count'];
function replaceSectionElement(entry, newContent) {
const cfg = entry.effectiveConfig;
const isSpotlight = isSpotlightSection(cfg);
const isSpotlight = newContent.classList.contains('spotlight-section');
let preservedLayout = null;
let preservedGapless = null;
if (!isSpotlight) {
@@ -862,9 +901,21 @@
|| window.cardBuilder?.resolveUseGaplessCards?.(cfg) === true;
}
const wasSpotlight = entry.element.classList.contains('spotlight-section');
const modeChanged = wasSpotlight !== isSpotlight;
newContent.style.cssText = entry.element.style.cssText;
newContent.className = entry.element.className;
if (modeChanged) {
const staleClasses = wasSpotlight ? SPOTLIGHT_MODE_CLASSES : NORMAL_MODE_CLASSES;
entry.element.classList.forEach(cls => {
if (!staleClasses.includes(cls)) newContent.classList.add(cls);
});
} else {
newContent.className = entry.element.className;
}
Array.from(entry.element.attributes).forEach(attr => {
if (attr.name === 'class') return;
if (modeChanged && !isSpotlight && SPOTLIGHT_MODE_ATTRS.includes(attr.name)) return;
newContent.setAttribute(attr.name, attr.value);
});
if (cfg.order != null) {
@@ -910,7 +961,7 @@
}
let newContent = null;
if (isSpotlightSection(cfg) && window.cardBuilder?.renderSpotlightSection) {
if (shouldRenderAsSpotlight(entry) && window.cardBuilder?.renderSpotlightSection) {
const activeSlide = entry.element?.querySelector('.spotlight-item[data-active]');
const initialIndex = parseInt(activeSlide?.getAttribute('data-index'), 10);
newContent = window.cardBuilder.renderSpotlightSection(items, cfg.name, {
@@ -1129,6 +1180,7 @@
showCloseButton: false,
closeOnBackdrop: true,
closeOnEscape: true,
fixedSize: false,
onOpen: (modalInstance) => {
styleAnchoredPopoverDialog(modalInstance, anchorBtn);
const root = modalInstance.dialogContent;
@@ -1314,6 +1366,7 @@
showCloseButton: false,
closeOnBackdrop: true,
closeOnEscape: true,
fixedSize: false,
onOpen: (modalInstance) => {
styleAnchoredPopoverDialog(modalInstance, anchorBtn);
const root = modalInstance.dialogContent;
@@ -1959,20 +2012,16 @@
}
function resolvePopoverMount(anchorButton, sectionElement) {
return anchorButton?.closest(
'.sectionTitleContainer, .spotlight-section-title-container'
) || sectionElement;
return document.body;
}
function preparePopoverMount(anchorButton, sectionElement) {
const mountElement = resolvePopoverMount(anchorButton, sectionElement);
mountElement.classList.add('kefin-section-has-configure-popover');
sectionElement?.classList.add('kefin-section-configure-popover-open');
return mountElement;
}
function cleanupPopoverMount(mountElement, sectionElement) {
mountElement?.classList.remove('kefin-section-has-configure-popover');
sectionElement?.classList.remove('kefin-section-configure-popover-open');
}
@@ -2139,54 +2188,51 @@
}
function positionConfigurePopover(popover, anchorButton, mountElement) {
const sectionElement = mountElement.closest('[data-section-id]')
|| activePopover?.entry?.element
const sectionElement = activePopover?.entry?.element
|| anchorButton?.closest?.('[data-section-id]')
|| mountElement;
const mountRect = mountElement.getBoundingClientRect();
const sectionRect = sectionElement.getBoundingClientRect();
const sectionRect = sectionElement?.getBoundingClientRect?.() || anchorButton.getBoundingClientRect();
const buttonRect = anchorButton.getBoundingClientRect();
const popoverWidth = popover.offsetWidth || popover.getBoundingClientRect().width;
const popoverWidth = popover.offsetWidth || popover.getBoundingClientRect().width || 280;
const margin = 8;
const gap = 8;
// Center on the configure button in viewport space first.
let leftViewport = buttonRect.left + (buttonRect.width / 2) - (popoverWidth / 2);
// Keep inside the section when possible; always keep inside the viewport.
const minLeft = Math.max(sectionRect.left + margin, margin);
const maxLeft = Math.min(sectionRect.right - popoverWidth - margin, window.innerWidth - popoverWidth - margin);
if (maxLeft >= minLeft) {
leftViewport = Math.min(Math.max(leftViewport, minLeft), maxLeft);
} else {
// Section narrower than popover (common when section name is hidden):
// pin to the section's left edge, then clamp to the viewport.
leftViewport = Math.min(
Math.max(sectionRect.left + margin, margin),
Math.max(margin, window.innerWidth - popoverWidth - margin)
);
}
popover.style.left = `${leftViewport - mountRect.left}px`;
// Spotlights: always open below so the popover stays inside the banner
// (overflow/mask on .spotlight-banner-container would clip anything above).
// Normal sections: decide once from expanded height; never flip after open.
let placeBelow;
if (activePopover?.anchorMode === 'above' || activePopover?.anchorMode === 'below') {
placeBelow = activePopover.anchorMode === 'below';
} else {
const isSpotlight = !!mountElement.closest('.spotlight-section')
|| activePopover?.entry?.element?.classList.contains('spotlight-section');
const isSpotlight = !!sectionElement?.classList?.contains('spotlight-section')
|| !!anchorButton?.closest?.('.spotlight-section');
const expandedHeight = getPopoverPlacementHeight(popover, true);
const spaceAbove = mountRect.top - margin;
const spaceAbove = buttonRect.top - margin;
placeBelow = isSpotlight || spaceAbove < expandedHeight + gap;
if (activePopover) activePopover.anchorMode = placeBelow ? 'below' : 'above';
}
popover.classList.toggle('is-below', placeBelow);
popover.style.top = '';
popover.style.bottom = '';
popover.style.position = 'fixed';
popover.style.left = `${leftViewport}px`;
if (placeBelow) {
popover.style.top = `${buttonRect.bottom + gap}px`;
popover.style.bottom = 'auto';
} else {
const height = getPopoverPlacementHeight(popover, popover.classList.contains('is-expanded'));
popover.style.top = `${Math.max(margin, buttonRect.top - gap - height)}px`;
popover.style.bottom = 'auto';
}
}
function openConfigurePopover(sectionConfig, sectionElement, anchorButton) {
@@ -2263,6 +2309,13 @@
<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'}">
<span class="material-icons" aria-hidden="true">settings_backup_restore</span>
</button>
${isPinnedParent ? `
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-unpin-button" title="Unpin" aria-label="Unpin from Home Screen">
<span class="material-icons" aria-hidden="true">push_pin</span>
</button>` : ''}
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-publish-btn" title="Apply to all users" aria-label="Apply to all users" hidden>
<span class="material-icons" aria-hidden="true">publish</span>
</button>
@@ -2287,6 +2340,10 @@
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-visibility-btn${cfg.enabled !== false ? '' : ' is-hidden-visibility'}" title="${cfg.enabled !== false ? 'Hide section' : 'Show section'}" aria-label="${cfg.enabled !== false ? 'Hide section' : 'Show section'}" aria-pressed="${cfg.enabled !== false}">
<span class="material-icons" aria-hidden="true">${cfg.enabled !== false ? 'visibility' : 'visibility_off'}</span>
</button>
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-view-carousel-btn${isSpotlight ? ' is-active' : ''}" title="${isSpotlight ? 'Spotlight mode' : 'Normal mode'}" aria-label="Toggle Spotlight" aria-pressed="${isSpotlight}">
<span class="material-icons" aria-hidden="true">view_carousel</span>
</button>
<span class="kefin-section-configure-divider" aria-hidden="true"></span>
${!isSpotlight ? `
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-format-btn" title="Card Format" aria-label="Card Format">
<span class="material-icons" aria-hidden="true">image</span>
@@ -2306,13 +2363,6 @@
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-spotlight-tiles-btn" title="${escapeHtml(spotlightTilesLabel)}" aria-label="${escapeHtml(spotlightTilesLabel)}" data-tile-count="${spotlightTileCount}">
<span class="material-icons" aria-hidden="true">${spotlightTilesIcon}</span>
</button>`}
${isPinnedParent ? `
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-unpin-button" title="Unpin" aria-label="Unpin from Home Screen">
<span class="material-icons" aria-hidden="true">push_pin</span>
</button>` : ''}
<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'}">
<span class="material-icons" aria-hidden="true">settings_backup_restore</span>
</button>
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-more-toggle" title="More options" aria-label="More options" aria-expanded="false">
<span class="material-icons" aria-hidden="true">more_horiz</span>
</button>
@@ -2589,6 +2639,25 @@
openRestoreSectionDefaultsConfirm(() => restoreSectionToServerDefaults(entry, serverDefaults));
});
popover.querySelector('.kefin-section-view-carousel-btn')?.addEventListener('click', (e) => {
e.stopPropagation();
const nextSpotlight = !isSpotlightSection(entry.effectiveConfig);
entry.effectiveConfig.renderMode = nextSpotlight ? 'Spotlight' : 'Normal';
entry.effectiveConfig.spotlight = nextSpotlight;
if (nextSpotlight) {
entry.effectiveConfig.spotlightConfig = buildSpotlightConfigSeed(
entry.effectiveConfig.spotlightConfig,
serverDefaults
);
}
persistAndApply(entry, serverDefaults, () => {
closePopover();
rerenderSection(entry);
const anchor = getConfigureAnchorButton(entry);
if (anchor) openConfigurePopover(entry.effectiveConfig, entry.element, anchor);
});
});
popover.querySelector('.kefin-section-more-toggle')?.addEventListener('click', (e) => {
e.stopPropagation();
const willExpand = !popover.classList.contains('is-expanded');
@@ -2724,9 +2793,7 @@
overflow: visible;
}
.kefin-section-configure-popover {
position: absolute;
bottom: calc(100% - 15px);
top: auto;
position: fixed;
z-index: 12000;
box-sizing: border-box;
display: flex;
@@ -2738,8 +2805,6 @@
overflow: hidden;
}
.kefin-section-configure-popover.is-below {
top: 55px;
bottom: auto;
flex-direction: column-reverse;
}
.kefin-section-configure-popover.is-below .kefin-section-configure-title-container {
@@ -2809,12 +2874,22 @@
.kefin-section-configure-primary-row .paper-icon-button-light .material-icons {
font-size: 1.25rem;
}
.kefin-section-configure-primary-row .kefin-section-restore-defaults-btn {
margin-left: auto;
.kefin-section-configure-divider {
width: 1px;
align-self: stretch;
margin: 0.35em 0.25em;
background: rgba(255, 255, 255, 0.2);
flex: 0 0 1px;
}
.kefin-section-visibility-btn.is-hidden-visibility {
opacity: 0.45;
}
.kefin-section-view-carousel-btn:not(.is-active) {
opacity: 0.4;
}
.kefin-section-view-carousel-btn.is-active {
opacity: 1;
}
.kefin-section-restore-defaults-btn.is-disabled,
.kefin-section-restore-defaults-btn:disabled {
opacity: 0.4;
@@ -3066,7 +3141,9 @@
margin: 0 0 0.5rem 0.5rem;
}
.kefin-section-configure-title-row .kefin-section-open-editor-btn,
.kefin-section-configure-title-row .kefin-section-publish-btn {
.kefin-section-configure-title-row .kefin-section-publish-btn,
.kefin-section-configure-title-row .kefin-section-restore-defaults-btn,
.kefin-section-configure-title-row .kefin-section-unpin-button {
flex: 0 0 auto;
opacity: 0.85;
padding: 0;
@@ -3075,7 +3152,9 @@
height: 2.25rem;
}
.kefin-section-configure-title-row .kefin-section-open-editor-btn .material-icons,
.kefin-section-configure-title-row .kefin-section-publish-btn .material-icons {
.kefin-section-configure-title-row .kefin-section-publish-btn .material-icons,
.kefin-section-configure-title-row .kefin-section-restore-defaults-btn .material-icons,
.kefin-section-configure-title-row .kefin-section-unpin-button .material-icons {
font-size: 1.25rem;
}
.kefin-section-configure-title-row .kefin-section-open-editor-btn[hidden],
@@ -3083,7 +3162,9 @@
display: none !important;
}
.kefin-section-configure-title-row .kefin-section-open-editor-btn:hover,
.kefin-section-configure-title-row .kefin-section-publish-btn:hover {
.kefin-section-configure-title-row .kefin-section-publish-btn:hover,
.kefin-section-configure-title-row .kefin-section-restore-defaults-btn:hover,
.kefin-section-configure-title-row .kefin-section-unpin-button:hover {
opacity: 1;
}
`;