diff --git a/configuration/homeScreen-configuration.css b/configuration/homeScreen-configuration.css index 653ebc3..ed5ea91 100644 --- a/configuration/homeScreen-configuration.css +++ b/configuration/homeScreen-configuration.css @@ -77,6 +77,10 @@ [data-modal-id="kefin-homescreen-config-main"] div#section-content { max-height: calc(90vh - 19em); overflow: auto; + scrollbar-width: none; +} +[data-modal-id="kefin-homescreen-config-main"] #section-content > div > div:last-child { + margin-bottom: 6em; } div#order-sections-container .editable-group-name { @@ -342,3 +346,21 @@ span.listItemBodyText.secondary.seasonal-tag { [data-modal-id="kefin-homescreen-config-main"] .hsc-settings-grid .hsc-settings-span-2 .kefin-toggle-card { flex: 1; } + +[data-modal-id="kefin-homescreen-config-main"] .kefin-settings-input-card { + align-items: center; +} + +[data-modal-id="kefin-homescreen-config-main"] .kefin-settings-input-card-control { + flex-shrink: 0; + width: 7.5em; +} + +[data-modal-id="kefin-homescreen-config-main"] .kefin-settings-input-card-control .emby-input { + width: 100%; + box-sizing: border-box; +} + +.section-row-actions .btnSectionConfigure { + display: none; +} diff --git a/configuration/homeScreen-configuration.js b/configuration/homeScreen-configuration.js index 430218b..b0443ae 100644 --- a/configuration/homeScreen-configuration.js +++ b/configuration/homeScreen-configuration.js @@ -72,7 +72,7 @@ off: 'Person appearance data is limited to only Movies and Series.' }, 'home-showCreateSectionButtonOnHome': { - on: 'Adds a hidden button to the bottom left corner of the home screen so that you can quickly create a new section directly from the Home Screen. Only admins will see this button.', + on: 'Adds a button to the bottom left corner of the home screen so that you can quickly create a new section directly from the Home Screen. Only admins will see this button. The button is hidden on mobile.', off: 'You must use the KefinTweaks Configuration > Home Screen > New Section flow to create new Home Screen sections.' }, 'home-showCategoryFilters': { @@ -2247,7 +2247,7 @@ const D = GENERAL_TOGGLE_DESCRIPTIONS; const maxPeopleCount = homeSettings.maxPeopleCount ?? 100; const maxPeopleOptions = MAX_PEOPLE_COUNT_OPTIONS.map((n) => ({ value: String(n), label: String(n) })); - const settingsGroupStyle = 'margin-bottom: 1.5em; border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 1em;'; + const settingsGroupStyle = 'margin-top: 0.75em; margin-bottom: 1.5em; border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 1em;'; return `
0 removes it immediately. Any other value shows “No items found” for that many milliseconds, then fades the section out.
+ ${err.message || 'Preview refresh failed'}
`; @@ -5363,7 +5472,8 @@ Dashboard.showLoadingMsg(); try { - const { items } = await fetchSectionPreviewItems(section); + const { items, section: resolvedSection } = await fetchSectionPreviewItems(section); + const previewSection = mergeDraftWithResolvedPreview(section, resolvedSection); if (items.length === 0) { showToast('No items found for preview'); @@ -5388,7 +5498,7 @@ content.style.cssText = 'padding: 1em; max-width: 1400px; width: 100%;'; content.innerHTML = `Restoring the default Home Screen Settings will override your existing settings. Are you sure you want to proceed?
'; const footer = ` - + `; window.ModalSystem.create({ diff --git a/configuration/homeScreenAdvancedEditor.css b/configuration/homeScreenAdvancedEditor.css index cfe7272..b8780a1 100644 --- a/configuration/homeScreenAdvancedEditor.css +++ b/configuration/homeScreenAdvancedEditor.css @@ -1596,7 +1596,7 @@ .hsae-root[data-editor-profile="normal"] .hsae-toggle-card-grid > [data-hsae-profiles~="normal"], .hsae-root[data-editor-profile="dateCutoffs"] .hsae-toggle-card-grid > [data-hsae-profiles~="dateCutoffs"], .hsae-root[data-editor-profile="postProcessing"] .hsae-toggle-card-grid > [data-hsae-profiles~="postProcessing"] { - display: block !important; + display: grid !important; } /* Item source — show only the panel matching the active source (profile rules still gate custom items on non-full profiles) */ @@ -1624,7 +1624,7 @@ .hsae-root[data-editor-profile] .hsae-query-row-sort .hsae-query-sort-by, .hsae-root[data-editor-profile] .hsae-query-row-sort .hsae-query-sort-order, .hsae-root[data-editor-profile] .hsae-query-row-sort .hsae-query-limit { - display: block !important; + display: grid !important; } .hsae-root[data-editor-profile="minimal"] .hsae-query-row-sort .hsae-query-sort-by, diff --git a/configuration/homeScreenAdvancedEditor.js b/configuration/homeScreenAdvancedEditor.js index 21fdff7..14d5f6b 100644 --- a/configuration/homeScreenAdvancedEditor.js +++ b/configuration/homeScreenAdvancedEditor.js @@ -391,8 +391,14 @@ }; const ADVANCED_TOGGLE_DESCRIPTIONS = { - 'section-flattenSeries': 'Multiple episodes from the same series will be grouped into one item in the section.', - 'hsae-limit-before-sort': 'When enabled, the item limit is applied before section-level sort.', + 'section-flattenSeries': { + on: 'Multiple episodes from the same series will be grouped into one item in the section.', + off: 'Episodes from the same series appear as separate items in the section.' + }, + 'hsae-limit-before-sort': { + on: 'When enabled, the item limit is applied before section-level sort.', + off: 'The item limit is applied after section-level sort.' + }, 'hsae-hide-card-titles': { on: 'Card titles for individual items in the section will not be shown.', off: 'Card titles for individual items in the section will be shown.' @@ -1915,7 +1921,7 @@ let appearancePreviewDebounceTimer = null; let appearancePreviewRequestId = 0; - let appearancePreviewCache = { fetchKey: null, renderKey: null, items: null }; + let appearancePreviewCache = { fetchKey: null, renderKey: null, items: null, resolvedSection: null }; function clearAppearancePreviewScheduling() { if (appearancePreviewDebounceTimer) { @@ -1935,6 +1941,7 @@ const fetchPreview = window.KefinHomeScreen?.fetchSectionPreviewItems; const renderPreview = window.KefinHomeScreen?.renderSectionPreviewInto; + const mergeResolved = window.KefinHomeScreen?.mergeDraftWithResolvedPreview; if (typeof fetchPreview !== 'function' || typeof renderPreview !== 'function') { container.innerHTML = 'Preview unavailable.
'; return; @@ -1967,9 +1974,9 @@ try { if (!cacheHit) { - const { items } = await fetchPreview(draft); + const { items, section: resolvedSection } = await fetchPreview(draft); if (requestId !== appearancePreviewRequestId) return; - appearancePreviewCache = { fetchKey, renderKey, items }; + appearancePreviewCache = { fetchKey, renderKey, items, resolvedSection: resolvedSection || null }; } else { appearancePreviewCache.renderKey = renderKey; } @@ -1982,10 +1989,14 @@ return; } - await renderPreview(getDraftSection(), previewItems, container, { getDraftSection }); + const liveDraft = getDraftSection(); + const previewSection = typeof mergeResolved === 'function' + ? mergeResolved(liveDraft, appearancePreviewCache.resolvedSection) + : liveDraft; + await renderPreview(previewSection, previewItems, container, { getDraftSection }); } catch (err) { if (requestId !== appearancePreviewRequestId) return; - appearancePreviewCache = { fetchKey: null, renderKey: null, items: null }; + appearancePreviewCache = { fetchKey: null, renderKey: null, items: null, resolvedSection: null }; container.innerHTML = `${escapeHtml(err.message || 'Preview failed')}
`; } } @@ -2066,13 +2077,6 @@ ADVANCED_TOGGLE_DESCRIPTIONS['hsae-hide-card-titles'][hideCardTitles ? 'on' : 'off'], { hintKey: 'hsae-hide-card-titles' } )} - ${buildToggleCard( - 'hsae-use-parent-card', - section.useParentCard === true, - 'Link Parent Item', - ADVANCED_TOGGLE_DESCRIPTIONS['hsae-use-parent-card'][section.useParentCard === true ? 'on' : 'off'], - { hintKey: 'hsae-use-parent-card' } - )}Preview unavailable.
'; return; @@ -4092,16 +4073,19 @@ try { if (!cacheHit) { - const { items } = await fetchPreview(draft, { useSpotlightFields: true }); + const { items, section: resolvedSection } = await fetchPreview(draft, { useSpotlightFields: true }); if (requestId !== appearancePreviewRequestId) return; - appearancePreviewCache = { queryKey, items }; + appearancePreviewCache = { queryKey, items, resolvedSection: resolvedSection || null }; } if (requestId !== appearancePreviewRequestId) return; - renderPreview(draft, appearancePreviewCache.items, container); + const previewSection = typeof mergeResolved === 'function' + ? mergeResolved(draft, appearancePreviewCache.resolvedSection) + : draft; + renderPreview(previewSection, appearancePreviewCache.items, container); } catch (err) { if (requestId !== appearancePreviewRequestId) return; - appearancePreviewCache = { queryKey: null, items: null }; + appearancePreviewCache = { queryKey: null, items: null, resolvedSection: null }; container.innerHTML = `${escapeHtml(err.message || 'Preview failed')}
`; } }, WIZARD_PARENT_DATA.APPEARANCE_PREVIEW_DEBOUNCE_MS); @@ -4659,7 +4643,7 @@ e.target.id === 'hsse-wizard-spotlightLayout' || e.target.id === 'hsse-wizard-spotlightSize' || e.target.id === 'hsse-wizard-tileCount' || - e.target.id === 'hsse-wizard-animated-checkbox' + e.target.id === 'hsse-wizard-animated' )) { collectAppearanceStepFromForm(root, wizardState); scheduleAppearancePreview(root); diff --git a/configuration/homescreenBenchmark.js b/configuration/homescreenBenchmark.js index cf2370a..c1d3bda 100644 --- a/configuration/homescreenBenchmark.js +++ b/configuration/homescreenBenchmark.js @@ -369,7 +369,6 @@ display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 0.4em; - max-height: 460px; overflow-y: auto; padding: 0.5em; border: 1px solid rgba(255,255,255,0.12); diff --git a/injector.js b/injector.js index ba74c98..c433c8c 100644 --- a/injector.js +++ b/injector.js @@ -306,7 +306,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' }, { @@ -1157,7 +1157,7 @@ } async function loadConfigurationJS() { - const configDependencyNames = ['modal', 'toaster', 'utils', 'homeScreenConfig2', 'ui', 'homeScreen-migration', 'homeScreen-configuration', 'search-configuration', 'seriesEpisodes-configuration', 'seriesInfo-configuration', 'versionPreferences-configuration', 'skinManager-configuration', 'customMenuLinks-configuration', 'thumbnailScrubber-configuration', 'watchTogether-configuration', 'userManager-configuration', 'apiHelper']; + const configDependencyNames = ['modal', 'toaster', 'utils', 'homeScreenConfig2', 'ui', 'homeScreen-migration', 'homeScreen-configuration', 'search-configuration', 'seriesEpisodes-configuration', 'seriesInfo-configuration', 'versionPreferences-configuration', 'skinManager-configuration', 'customMenuLinks-configuration', 'thumbnailScrubber-configuration', 'watchTogether-configuration', 'userManager-configuration', 'apiHelper', 'sectionHelper']; for (const depName of configDependencyNames) { const depScript = SCRIPT_DEFINITIONS.find(script => script.name === depName); if (depScript) { diff --git a/kefinTweaks-plugin.js b/kefinTweaks-plugin.js index 6e18861..93473d9 100644 --- a/kefinTweaks-plugin.js +++ b/kefinTweaks-plugin.js @@ -1719,7 +1719,7 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; top: 1em; } -#reactRoot:has(.MuiBox-root) .libraryPage:not(.noSecondaryNavPage).customPage { +#reactRoot:has(main.MuiBox-root) .libraryPage:not(.noSecondaryNavPage).customPage { padding-top: 0 !important; } `; diff --git a/scripts/cardBuilder.css b/scripts/cardBuilder.css index a031e94..15faa9c 100644 --- a/scripts/cardBuilder.css +++ b/scripts/cardBuilder.css @@ -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, diff --git a/scripts/cardBuilder.js b/scripts/cardBuilder.js index a5743b8..38b21df 100644 --- a/scripts/cardBuilder.js +++ b/scripts/cardBuilder.js @@ -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(); diff --git a/scripts/homeScreen.css b/scripts/homeScreen.css index ae3c474..dff0c77 100644 --- a/scripts/homeScreen.css +++ b/scripts/homeScreen.css @@ -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; +} diff --git a/scripts/homeScreen3.js b/scripts/homeScreen3.js index f9d497e..9cbb49b 100644 --- a/scripts/homeScreen3.js +++ b/scripts/homeScreen3.js @@ -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 = ` - - `; - 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 = ` + + `; + 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 ` `; }).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(); } diff --git a/scripts/homeScreenSectionConfigure.js b/scripts/homeScreenSectionConfigure.js index 3252d99..6047e3e 100644 --- a/scripts/homeScreenSectionConfigure.js +++ b/scripts/homeScreenSectionConfigure.js @@ -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 @@