From 281514aae3f76dffaaac7b12e22a3aa999c21a18 Mon Sep 17 00:00:00 2001 From: HighImKevin Date: Fri, 25 Sep 2026 05:09:33 -0400 Subject: [PATCH] Improve Home Screen configuration and add functionality to hide the mobile Category Home rail on scroll --- configuration/homeScreen-configuration.css | 26 +- configuration/homeScreen-configuration.js | 281 ++++++++++++++++++--- configuration/homeScreenAdvancedEditor.css | 4 +- configuration/homeScreenSectionEditor.js | 10 +- scripts/homeScreen.css | 118 +++------ scripts/homeScreen3.js | 185 +++++++------- 6 files changed, 413 insertions(+), 211 deletions(-) diff --git a/configuration/homeScreen-configuration.css b/configuration/homeScreen-configuration.css index ed5ea91..22a4a87 100644 --- a/configuration/homeScreen-configuration.css +++ b/configuration/homeScreen-configuration.css @@ -74,13 +74,18 @@ padding-right: 1em; } -[data-modal-id="kefin-homescreen-config-main"] div#section-content { +[data-modal-id="kefin-homescreen-config-main"] div#section-content, +[data-modal-id="kefin-homescreen-config-main"] div#global-settings-content, +[data-modal-id="kefin-homescreen-config-main"] #home-screen-benchmark-root { 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; + +[data-modal-id="kefin-homescreen-config-main"] div#global-settings-content > *:last-child, +[data-modal-id="kefin-homescreen-config-main"] #home-screen-benchmark-root > *:last-child, +[data-modal-id="kefin-homescreen-config-main"] #section-content > div > *:last-child { + margin-bottom: 4em !important; } div#order-sections-container .editable-group-name { @@ -364,3 +369,18 @@ span.listItemBodyText.secondary.seasonal-tag { .section-row-actions .btnSectionConfigure { display: none; } + +.dialogContainer[data-modal-id="kefin-homescreen-config-main"] .formDialogHeader + div > div { + padding: 1.25em 1.5em; + height: calc(100% - 2.5em); +} +.dialogContainer[data-modal-id="kefin-homescreen-config-main"] .formDialogHeader + div { + padding: 0 !important; + overflow-y: hidden !important; +} +.dialogContainer[data-modal-id="kefin-homescreen-config-main"] .formDialogHeader + div > div > div #section-content > div { + padding-bottom: 4em; +} +.config-tab-nav button[data-tab="order"] { + display: none; +} diff --git a/configuration/homeScreen-configuration.js b/configuration/homeScreen-configuration.js index b07ec54..4a44091 100644 --- a/configuration/homeScreen-configuration.js +++ b/configuration/homeScreen-configuration.js @@ -68,8 +68,8 @@ off: 'Studios without any image may be rendered in Popular Studios sections. You can enable the Optional CSS Module "Studio Thumbnails" to overlay a background with the Studio Name on items without an image.' }, 'home-loadPeopleEpisodeData': { - on: 'Loads all Person appearances from all episodes in your library. This data takes much longer to cache than just the Movies/Series level data.', - off: 'Person appearance data is limited to only Movies and Series.' + on: 'Not Recommended for larger libraries. Loads all Person appearances from all episodes in your library. This data takes much longer to cache than just the Movies/Series level data.', + off: 'This is recommended. Person appearance data is limited to only Movies and Series.' }, 'home-showCreateSectionButtonOnHome': { 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.', @@ -680,7 +680,7 @@ return BROWSE_BY_STUDIO_CATALOG.every((entry) => resolvedNames.has(entry.name)); } - async function fetchAllStudiosForBrowseSection() { + function fetchAllStudiosForBrowseSection() { if (browseStudiosFetchPromise) { return browseStudiosFetchPromise; } @@ -960,7 +960,8 @@ if (browseSection && isBrowseByStudioCatalogFullyResolved(existingItems)) { LOG('Browse by Studio catalog fully resolved; skipping /Studios fetch'); } else { - const studios = await fetchAllStudiosForBrowseSection(); + const studiosPromise = fetchAllStudiosForBrowseSection(); + const studios = await studiosPromise; if (!browseSection) { const items = buildBrowseByStudioItems(studios, []); @@ -999,16 +1000,184 @@ queryOptions: ['Fields', 'Filters', 'IncludeItemTypes', 'MediaTypes', 'IsActive', 'IsScheduled', 'IsAiring', 'HasAired', 'IsInProgress'], }; - function buildCanonicalHomeSectionMap() { + function buildCanonicalSectionMapForGroups(groupKey) { const defaults = window.KefinHomeConfig2; const map = new Map(); - if (!defaults?.HOME_SECTION_GROUPS) return map; - flattenSectionGroups(defaults.HOME_SECTION_GROUPS).forEach(section => { + flattenSectionGroups(defaults?.[groupKey] || []).forEach((section) => { if (section?.id) map.set(section.id, section); }); return map; } + function buildCanonicalHomeSectionMap() { + return buildCanonicalSectionMapForGroups('HOME_SECTION_GROUPS'); + } + + function getAllKefinDefaultSectionIds() { + const ids = new Set(); + ['HOME_SECTION_GROUPS', 'SEASONAL_SECTION_GROUPS', 'DISCOVERY_SECTION_GROUPS'].forEach((key) => { + buildCanonicalSectionMapForGroups(key).forEach((_, id) => ids.add(id)); + }); + return ids; + } + + const KEFIN_DEFAULT_GROUP_KEYS = ['HOME_SECTION_GROUPS', 'SEASONAL_SECTION_GROUPS', 'DISCOVERY_SECTION_GROUPS']; + const LIBRARY_RESOLVED_ID_PREFIXES = ['recently-added-', 'popular-genres-']; + const FIXED_TEMPLATE_SECTION_IDS = new Set([BROWSE_BY_STUDIO_SECTION_ID]); + + function isLibraryResolvedSectionId(sectionId) { + if (!sectionId) return false; + return LIBRARY_RESOLVED_ID_PREFIXES.some((prefix) => sectionId.startsWith(prefix)); + } + + function shouldKeepDefaultSurfaceSection(section, defaultSectionIds) { + if (!section?.id) return false; + if (section.deleted === true) return true; + if (defaultSectionIds.has(section.id)) return true; + if (FIXED_TEMPLATE_SECTION_IDS.has(section.id)) return true; + if (isLibraryResolvedSectionId(section.id)) return true; + return false; + } + + function addMissingKefinDefaultSections(config) { + const defaults = window.KefinHomeConfig2; + if (!defaults) return false; + let hasChanges = false; + + KEFIN_DEFAULT_GROUP_KEYS.forEach((groupKey) => { + const defaultGroups = defaults[groupKey] || []; + if (!Array.isArray(config[groupKey])) config[groupKey] = []; + + defaultGroups.forEach((defaultGroup) => { + let savedGroup = null; + if (defaultGroup.id) { + savedGroup = config[groupKey].find((g) => g.id === defaultGroup.id) || null; + } + if (!savedGroup && defaultGroup.name) { + savedGroup = config[groupKey].find((g) => g.name === defaultGroup.name) || null; + } + + if (!savedGroup) { + config[groupKey].push(JSON.parse(JSON.stringify(defaultGroup))); + hasChanges = true; + return; + } + + if (!Array.isArray(savedGroup.sections)) savedGroup.sections = []; + const existingIds = new Set(savedGroup.sections.map((s) => s?.id).filter(Boolean)); + (defaultGroup.sections || []).forEach((defaultSection) => { + if (!defaultSection?.id || existingIds.has(defaultSection.id)) return; + const tombstone = savedGroup.sections.find((s) => s?.id === defaultSection.id && s.deleted === true); + if (tombstone) return; + savedGroup.sections.push(JSON.parse(JSON.stringify(defaultSection))); + existingIds.add(defaultSection.id); + hasChanges = true; + LOG(`Added missing default section: ${defaultSection.id}`); + }); + }); + }); + + return hasChanges; + } + + function countOrphanKefinDefaultSections(config) { + const defaultSectionIds = getAllKefinDefaultSectionIds(); + let count = 0; + KEFIN_DEFAULT_GROUP_KEYS.forEach((groupKey) => { + (config[groupKey] || []).forEach((group) => { + (group.sections || []).forEach((section) => { + if (!shouldKeepDefaultSurfaceSection(section, defaultSectionIds)) count += 1; + }); + }); + }); + return count; + } + + function pruneOrphanKefinDefaultSections(config) { + const defaultSectionIds = getAllKefinDefaultSectionIds(); + let hasChanges = false; + + KEFIN_DEFAULT_GROUP_KEYS.forEach((groupKey) => { + (config[groupKey] || []).forEach((group) => { + if (!Array.isArray(group.sections)) return; + const before = group.sections.length; + group.sections = group.sections.filter((section) => { + const keep = shouldKeepDefaultSurfaceSection(section, defaultSectionIds); + if (!keep) LOG(`Pruning orphan default-surface section: ${section?.id}`); + return keep; + }); + if (group.sections.length !== before) hasChanges = true; + }); + }); + + return hasChanges; + } + + async function backupConfigBeforeDefaultPrune(configToBackup) { + try { + if (!window.ApiClient || !window.ApiClient._serverAddress || !window.ApiClient.accessToken) { + WARN('ApiClient not available, cannot create pre-prune backup'); + return false; + } + if (!window.KefinTweaksUtils?.resolvePluginId || !window.KefinTweaksUtils?.getPluginConfiguration) { + WARN('Cannot create pre-prune backup: plugin helpers unavailable'); + return false; + } + if (!window.apiHelper?.getAuthHeader) { + WARN('Cannot create pre-prune backup: apiHelper unavailable'); + return false; + } + + const injectorAliases = ['JavaScript Injector', 'JS Injector']; + const pluginId = await window.KefinTweaksUtils.resolvePluginId(injectorAliases); + if (!pluginId) { + WARN('Cannot create pre-prune backup: Injector plugin not found'); + return false; + } + + const injectorConfig = await window.KefinTweaksUtils.getPluginConfiguration(injectorAliases); + if (!injectorConfig.CustomJavaScripts) injectorConfig.CustomJavaScripts = []; + + const timestamp = new Date().toISOString().replace(/[:.]/g, '-').slice(0, -5); + const backupName = `KefinTweaks-Config-Backup-${timestamp}`; + const backupScriptContent = `// KefinTweaks Configuration Backup +// Automatically created before default-section prune sync +// Created: ${new Date().toISOString()} +// Do not edit manually + +window.KefinTweaksConfig = ${JSON.stringify(configToBackup, null, 2)};`; + + injectorConfig.CustomJavaScripts.push({ + Name: backupName, + Script: backupScriptContent, + Enabled: false, + RequiresAuthentication: false + }); + + const server = window.ApiClient._serverAddress; + const configUrl = `${server}/Plugins/${pluginId}/Configuration`; + const saveResponse = await fetch(configUrl, { + method: 'POST', + headers: { + 'Authorization': window.apiHelper.getAuthHeader(), + 'Content-Type': 'application/json' + }, + body: JSON.stringify(injectorConfig) + }); + + if (!saveResponse.ok) { + WARN(`Failed to save pre-prune backup: ${saveResponse.statusText}`); + return false; + } + + LOG(`Created pre-prune backup: ${backupName}`); + return true; + } catch (error) { + ERR('Error creating pre-prune config backup:', error); + return false; + } + } + function getRecentlyAddedCanonicalQuery() { return { path: '/Items/Latest', @@ -1059,37 +1228,54 @@ } function syncKefinTweaksDefaultSections(config) { - const canonicalMap = buildCanonicalHomeSectionMap(); const recentlyAddedTemplate = getRecentlyAddedCanonicalQuery(); let hasChanges = false; - const groups = config.HOME_SECTION_GROUPS || []; - for (const group of groups) { - for (const section of group.sections || []) { - if (!section?.id) continue; + KEFIN_DEFAULT_GROUP_KEYS.forEach((groupKey) => { + const canonicalMap = buildCanonicalSectionMapForGroups(groupKey); + const groups = config[groupKey] || []; + for (const group of groups) { + for (const section of group.sections || []) { + if (!section?.id) continue; - let canonical = canonicalMap.get(section.id); - if (!canonical && section.id.startsWith('recently-added-')) { - canonical = { queries: [recentlyAddedTemplate] }; - } - if (!canonical?.queries?.length) continue; - - if (!Array.isArray(section.queries)) section.queries = []; - - for (let i = 0; i < canonical.queries.length; i++) { - if (!section.queries[i]) { - section.queries[i] = { queryOptions: {} }; + let canonical = canonicalMap.get(section.id); + if (!canonical && section.id.startsWith('recently-added-')) { + canonical = { queries: [recentlyAddedTemplate] }; } - if (syncQueryFromCanonical(section.queries[i], canonical.queries[i])) { - hasChanges = true; + if (!canonical?.queries?.length) continue; + + if (!Array.isArray(section.queries)) section.queries = []; + + for (let i = 0; i < canonical.queries.length; i++) { + if (!section.queries[i]) { + section.queries[i] = { queryOptions: {} }; + } + if (syncQueryFromCanonical(section.queries[i], canonical.queries[i])) { + hasChanges = true; + } } } } - } + }); return hasChanges; } + /** + * Always overwrite seasonalThemes from KefinHomeConfig2 (not user-editable yet). + * Preserves enableSeasonalAnimations / enableSeasonalBackground / seasonToggles / enabled. + */ + function syncSeasonalThemesFromDefaults(config) { + const defaults = window.KefinHomeConfig2?.SEASONAL_THEME_SETTINGS; + if (!defaults?.seasonalThemes) return false; + const next = JSON.parse(JSON.stringify(defaults.seasonalThemes)); + const prev = config.SEASONAL_THEME_SETTINGS?.seasonalThemes; + const changed = JSON.stringify(prev) !== JSON.stringify(next); + if (!config.SEASONAL_THEME_SETTINGS) config.SEASONAL_THEME_SETTINGS = {}; + config.SEASONAL_THEME_SETTINGS.seasonalThemes = next; + return changed; + } + async function ensureKefinTweaksDefaultSections() { try { if (!window.ApiClient || !window.ApiClient._loggedIn) { @@ -1106,11 +1292,31 @@ const config = loadConfig(); const { hasChanges: libraryChanges } = await verifyLibrarySectionsConfig(config); - const queryChanges = syncKefinTweaksDefaultSections(config); + const addedDefaults = addMissingKefinDefaultSections(config); - if (libraryChanges || queryChanges) { + const orphanCount = countOrphanKefinDefaultSections(config); + let prunedOrphans = false; + if (orphanCount > 0) { + const snapshot = JSON.parse(JSON.stringify(window.KefinTweaksConfig || {})); + if (!snapshot.homeScreenConfig) { + snapshot.homeScreenConfig = JSON.parse(JSON.stringify(config)); + } + await backupConfigBeforeDefaultPrune(snapshot); + prunedOrphans = pruneOrphanKefinDefaultSections(config); + } + + const queryChanges = syncKefinTweaksDefaultSections(config); + const seasonalThemeChanges = syncSeasonalThemesFromDefaults(config); + + if (libraryChanges || addedDefaults || prunedOrphans || queryChanges || seasonalThemeChanges) { await saveConfig(config); - LOG('KefinTweaks default home sections synced on startup'); + LOG('KefinTweaks default home sections synced on startup', { + libraryChanges, + addedDefaults, + prunedOrphans, + queryChanges, + seasonalThemeChanges + }); } } catch (error) { ERR('Error ensuring KefinTweaks default sections:', error); @@ -1133,6 +1339,15 @@ } try { + let admin = false; + try { + admin = !!(await window.apiHelper?.isAdmin?.()); + } catch { + return; + } + if (!admin) return; + + // Legacy homeScreen → homeScreenConfig (no-ops once homeScreenConfig exists) if (window.migrateHomeScreenConfig) { await window.migrateHomeScreenConfig(); } @@ -1179,7 +1394,13 @@ 'custom' ), DISCOVERY_SETTINGS: { ...defaults.DISCOVERY_SETTINGS, ...(existingConfig.DISCOVERY_SETTINGS || {}) }, - SEASONAL_THEME_SETTINGS: { ...defaults.SEASONAL_THEME_SETTINGS, ...(existingConfig.SEASONAL_THEME_SETTINGS || {}) }, + SEASONAL_THEME_SETTINGS: { + ...defaults.SEASONAL_THEME_SETTINGS, + ...(existingConfig.SEASONAL_THEME_SETTINGS || {}), + seasonalThemes: JSON.parse(JSON.stringify( + defaults.SEASONAL_THEME_SETTINGS?.seasonalThemes || {} + )) + }, CACHE: { ...defaults.CACHE, ...(existingConfig.CACHE || {}) }, SPOTLIGHT_SETTINGS: { ...defaults.SPOTLIGHT_SETTINGS, ...(existingConfig.SPOTLIGHT_SETTINGS || {}) }, HOME_SETTINGS: { ...defaults.HOME_SETTINGS, ...(existingConfig.HOME_SETTINGS || {}) }, diff --git a/configuration/homeScreenAdvancedEditor.css b/configuration/homeScreenAdvancedEditor.css index b8780a1..ccc2506 100644 --- a/configuration/homeScreenAdvancedEditor.css +++ b/configuration/homeScreenAdvancedEditor.css @@ -569,7 +569,7 @@ :is([data-modal-id="kefin-homescreen-section-editor"], [data-modal-id="kefin-homescreen-section-create-flow"]) .hsse-custom-item-tile, :is([data-modal-id="kefin-homescreen-section-editor"], [data-modal-id="kefin-homescreen-section-create-flow"]) .hsse-custom-item-add-tile { - width: 6.5rem; + width: auto; display: flex; flex-direction: column; align-items: center; @@ -578,7 +578,7 @@ :is([data-modal-id="kefin-homescreen-section-editor"], [data-modal-id="kefin-homescreen-section-create-flow"]) .hsse-custom-item-tile-image, :is([data-modal-id="kefin-homescreen-section-editor"], [data-modal-id="kefin-homescreen-section-create-flow"]) .hsse-custom-item-add-tile { - width: 6.5rem; + width: auto; height: 9.75rem; border-radius: 8px; border: 1px solid rgba(255, 255, 255, 0.12); diff --git a/configuration/homeScreenSectionEditor.js b/configuration/homeScreenSectionEditor.js index 77f4f4b..0d06b4b 100644 --- a/configuration/homeScreenSectionEditor.js +++ b/configuration/homeScreenSectionEditor.js @@ -378,11 +378,11 @@ */ async function resolveQueryPayload(result) { if (!result) return null; - if (typeof result.ensureData === 'function' || result.dataPromise != null) { - if (result.data != null) return result.data; - return await (typeof result.ensureData === 'function' - ? result.ensureData() - : result.dataPromise); + if (typeof result.ensureData === 'function') { + return await result.ensureData(); + } + if (result.dataPromise != null) { + return await result.dataPromise; } return result; } diff --git a/scripts/homeScreen.css b/scripts/homeScreen.css index dff0c77..dcb50f7 100644 --- a/scripts/homeScreen.css +++ b/scripts/homeScreen.css @@ -6,10 +6,6 @@ gap: 1rem; } -.snowverlay { - display: none; -} - .homePage [data-section-id] h2.sectionTitle, .homePage [data-section-id] .sectionTitleTextButton > .material-icons { margin-bottom: 0 !important; @@ -243,24 +239,6 @@ div#discovery-loading-indicator:not(.show)::after { font-size: 1.5rem !important; line-height: 1.8rem !important; } - -/* .homeSectionsContainer [data-section-id*="thematic-"] .card[data-type="Folder"] .cardText { - display: none; -} - -[data-card-format="Square"] [data-type="Person"] .cardScalable, -[data-card-format="Square"] [data-type="Actor"] .cardScalable, -[data-card-format="Square"] [data-type="Writer"] .cardScalable, -[data-card-format="Square"] [data-type="Director"] .cardScalable { - border-radius: 50% !important; - overflow: hidden; -} - -[data-card-format="Square"] [data-type="Actor"] .cardText { - display: flex; - justify-content: center; -} */ - .section0:not([data-page]), .section1:not([data-page]), .section2:not([data-page]), @@ -270,33 +248,7 @@ div#discovery-loading-indicator:not(.show)::after { .section6:not([data-page]), .section7:not([data-page]), .section8:not([data-page]), -.section9:not([data-page]), -.homeSectionsContainer:has([data-page], [data-section-id]) - > :is( - .section0:not([data-page]), - .section1:not([data-page]), - .section2:not([data-page]), - .section3:not([data-page]), - .section4:not([data-page]), - .section5:not([data-page]), - .section6:not([data-page]), - .section7:not([data-page]), - .section8:not([data-page]), - .section9:not([data-page]) - ), -.homeSectionsContainer[data-sections-rendered="true"] - > :is( - .section0:not([data-page]), - .section1:not([data-page]), - .section2:not([data-page]), - .section3:not([data-page]), - .section4:not([data-page]), - .section5:not([data-page]), - .section6:not([data-page]), - .section7:not([data-page]), - .section8:not([data-page]), - .section9:not([data-page]) - ) { +.section9:not([data-page]) { display: none !important; } @@ -318,30 +270,27 @@ div#discovery-loading-indicator:not(.show)::after { display: none; } -body:not(:has(#slides-container)) .homeSectionsContainer .section9 + .custom-scroller-container { - margin-top: 1em; +body[data-seasonal-background="true"]:has(.homePage:not(.hide)) + #reactRoot:has(.homePage:not(.hide) #homeTab.is-active) + > .backdropContainer + .backdropImage { + background: none !important; } -/* .pageTabContent:has(.kefinHomeSectionsContainer[data-kefin-home-screen]) .homeSectionsContainer:not([data-kefin-home-screen]) { - display: none !important; +body[data-seasonal-background="true"]:has(.homePage:not(.hide)) + #reactRoot:has(.homePage:not(.hide) #homeTab.is-active) + > .backgroundContainer::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0%; + background: linear-gradient(rgba(0, 0, 0, 0.65), rgba(0, 0, 0, 0.85)), var(--seasonal-background-image); + background-position: center; + background-size: cover; } -body[data-seasonal-background="true"]:has(.homePage:not(.hide)) #reactRoot:has(.libraryPage:not(.hide) .pageTabContent.is-active[data-index="0"])>.backgroundContainer::after { - content: ''; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0%; - background: linear-gradient(rgba(0, 0, 0, 0.65), rgba(0, 0, 0, 0.85)), var(--seasonal-background-image); - background-position: center; - background-size: cover; -} - -body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay { - display: block; -} */ - .homePage[data-discovery-ready] div#discovery-loading-indicator:not(.show) { min-height: 50px; } @@ -402,10 +351,18 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay { .kefin-home-chrome:focus-within { opacity: 1; } -.kefin-home-chrome[hidden], -#kefin-home-category-rail[hidden] { +.kefin-home-chrome.hide, +#kefin-home-category-rail.hide, +#kefin-home-category-rail.hide .kefin-home-category-rail { display: none !important; } + +/* Hide home chrome off-home via root page key (no :has; JMP-safe) */ +html:not([data-active-page="home"]) #kefin-home-category-rail, +html:not([data-active-page="home"]) #kefin-home-create-section-btn { + display: none !important; +} + .kefin-home-chrome button, #kefin-home-category-rail button { pointer-events: auto; @@ -477,6 +434,14 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay { -webkit-backdrop-filter: blur(10px); backdrop-filter: blur(10px); border: none !important; + transition: + transform 0.25s ease, + opacity 0.25s ease; + } + #kefin-home-category-rail.is-scroll-hidden .kefin-home-category-rail { + transform: translateY(100%); + pointer-events: none; + opacity: 0; } .kefin-home-create-section, #kefin-home-create-section-btn { @@ -491,11 +456,6 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay { margin-left: auto !important; } -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 { @@ -531,8 +491,12 @@ body:not(:has(#slides-container)):has(.MuiBox-root) .homeSectionsContainer:has([ height: auto !important; } -body:has(.kefin-home-category-btn.is-active[data-category-id="discovery"]) .sections.homeSectionsContainer::before { +.sections.homeSectionsContainer[data-category="discovery"]::before { content: "Discover Something To Watch"; padding-left: max(env(safe-area-inset-left), 3.3%); font-size: 1.5rem; } + +html:not([data-active-page="home"]) .snowverlay { + display: none; +} diff --git a/scripts/homeScreen3.js b/scripts/homeScreen3.js index 9cbb49b..c9e7791 100644 --- a/scripts/homeScreen3.js +++ b/scripts/homeScreen3.js @@ -91,11 +91,6 @@ } state.getDisplayPrefernces = fetchDisplayPreferences; - const performanceTimer = { - loadTimeStart: null, - loadTimeEnd: null, - }; - // Cache used to support re-rendering our sections after Jellyfin overwrites the container const sectionCache = { renderedSections: [], // Array of section configs + results @@ -105,14 +100,6 @@ pendingJellyfinSectionObservers: [] }; - // Basic performance metrics for measuring initial and recovery renders - const performanceMetrics = { - initialRenderStart: null, - initialRenderEnd: null, - jellyfinDetected: null, - recoveryRenderEnd: null - }; - // Create loading indicator element (always last child of the sections container) function createDiscoveryLoadingIndicator(container) { // Prefer the provided container, but fall back to the standard sections container @@ -420,7 +407,10 @@ document.body.dataset.seasonalAnimations = 'true'; // Add the animation script to the head - const animation = seasonalThemes[activeSeasonalThemeGroup.id]?.animation; + const theme = seasonalThemes[activeSeasonalThemeGroup.id] || {}; + const animation = theme.animation; + document.body.dataset.seasonalShape = theme.shape || 'snowflake'; + document.body.dataset.seasonalAnimationStyle = theme.animationStyle || 'fall'; if (animation) { const animationScript = document.querySelector(`script[src*="${animation}.js"]`); if (!animationScript) { @@ -439,26 +429,16 @@ */ async function enhanceHomeScreen() { LOG('Initializing Home Screen v3...'); - performanceTimer.loadTimeStart = performance.now(); - performanceMetrics.initialRenderStart = performance.now(); mountCreateSectionButton(); // Defer mountCategoryRail until pinned availability is known (avoids first-load tab jump) syncHomeScreenChromeVisibility(); - // Reset cache/metrics for a new initialization cycle + // Reset cache for a new initialization cycle sectionCache.renderedSections = []; sectionCache.fragmentCache = null; sectionCache.isInitialRender = true; sectionCache.jellyfinHasRendered = false; - performanceMetrics.initialRenderEnd = null; - performanceMetrics.jellyfinDetected = null; - performanceMetrics.recoveryRenderEnd = null; - - // Run migration if needed (check for legacy homeScreen config) - if (window.migrateHomeScreenConfig) { - await window.migrateHomeScreenConfig(); - } const container = document.querySelector(HOME_SECTIONS_SELECTOR); if (!container) { @@ -501,18 +481,10 @@ return; } - let performanceStartTime = performance.now(); - // Fetch display preferences //state.userDisplayPreferences = await fetchDisplayPreferences(); - - let performanceEndTime = performance.now(); - let performanceDuration = performanceEndTime - performanceStartTime; - LOG(`Home Screen v3 Fetch display preferences initialization time: ${performanceDuration.toFixed(2)}ms`); // Load user preferences and apply filtering/ordering - performanceStartTime = performance.now(); - const userHomeConfig = window.KefinUserHomeScreenConfig; let filteredHomeSections; let homeScreen; @@ -546,10 +518,6 @@ mountCategoryRail(); syncHomeScreenChromeVisibility(); - performanceEndTime = performance.now(); - performanceDuration = performanceEndTime - performanceStartTime; - LOG(`Home Screen v3 Load user home screen sections initialization time: ${performanceDuration.toFixed(2)}ms`); - const nextupSectionConfig = filteredHomeSections.find(s => s.id === 'nextUp'); const continueWatchingSectionConfig = filteredHomeSections.find(s => s.id === 'continueWatching'); const continueWatchingAndNextUpSectionConfig = filteredHomeSections.find(s => s.id === 'continueWatchingAndNextUp'); @@ -589,11 +557,6 @@ observeSectionUntilChildrenThenClone(sectionEl, kefinHome); } }); - - performanceMetrics.recoveryRenderEnd = performance.now(); - if (performanceMetrics.jellyfinDetected) { - LOG(`Home Screen v3 Jellyfin section mirroring duration: ${(performanceMetrics.recoveryRenderEnd - performanceMetrics.jellyfinDetected).toFixed(2)}ms`); - } }; */ //let containerForRender = container; @@ -607,18 +570,9 @@ //containerForRender.dataset.kefinHomeScreen = 'true'; // Initial render directly into Kefin container (no waiting for Jellyfin) - performanceStartTime = performance.now(); LOG('Rendering Standard/Seasonal Sections (initial render)...'); await renderHomeSections(filteredHomeSections, container, false); - performanceEndTime = performance.now(); - performanceDuration = performanceEndTime - performanceStartTime; - LOG(`Home Screen v3 Render home sections (initial) initialization time: ${performanceDuration.toFixed(2)}ms`); - performanceMetrics.initialRenderEnd = performanceEndTime; - - performanceTimer.loadTimeEnd = performance.now(); - LOG(`Home Screen v3 load time initialization: ${(performanceTimer.loadTimeEnd - performanceTimer.loadTimeStart).toFixed(2)}ms`); - LOG('Initializing Discovery Sections...'); setupDiscoveryInteraction(container); @@ -653,13 +607,7 @@ * Now reads from all 4 groups: HOME, SEASONAL, DISCOVERY, CUSTOM */ async function mergeHomeSectionConfigs(defaultSections) { - - const performanceStartTime = performance.now(); const kefinSections = await window.KefinHomeScreen.getSections(); - const performanceEndTime = performance.now(); - const performanceDuration = performanceEndTime - performanceStartTime; - LOG(`Home Screen v3 Get kefin home screen config initialization time: ${performanceDuration.toFixed(2)}ms`); - return kefinSections.enabledHomeSections; // Return all enabled sections from HOME_SECTION_GROUPS @@ -1195,14 +1143,7 @@ const sectionPromises = []; - let performanceStartTime = performance.now(); - let performanceEndTime; - let performanceDuration; - - const performanceTimes = []; - for (const sectionConfig of sortedSections) { - const loopStartTime = performance.now(); if (sectionConfig.enabled === false) { LOG(`Skipping disabled section: ${sectionConfig.id}`); continue; @@ -1223,30 +1164,13 @@ } sectionPromises.push(loadSectionForRendering(sectionConfig)); - - const loopEndTime = performance.now(); - const loopDuration = loopEndTime - loopStartTime; - performanceTimes.push(loopDuration); - LOG(`Section ${sectionConfig.id} initialization time: ${loopDuration.toFixed(2)}ms`); } - performanceEndTime = performance.now(); - performanceDuration = performanceEndTime - performanceStartTime; - LOG(`Render home sections configs loaded initialization time: ${performanceDuration.toFixed(2)}ms`); - - LOG(`Total performance time: ${performanceTimes.reduce((a, b) => a + b, 0).toFixed(2)}ms`); - - performanceStartTime = performance.now(); - // Resolve all section promises let sectionResults = await Promise.all(sectionPromises); sectionResults = sectionResults.filter(result => result !== null); sectionsToRender.push(...sectionResults); - performanceEndTime = performance.now(); - performanceDuration = performanceEndTime - performanceStartTime; - LOG(`Home Screen v3 Resolve all section promises initialization time: ${performanceDuration.toFixed(2)}ms`); - // Cache the rendered sections for potential re-render after Jellyfin overwrites the container sectionCache.renderedSections = [...sectionsToRender]; } @@ -1263,7 +1187,6 @@ } if (!state.isRenderingHome && !kefinSections) { - const renderStartTime = performance.now(); state.isRenderingHome = true; await window.cardBuilder.renderProgressiveSections(targetContainer, sectionsToRender, { @@ -1274,10 +1197,6 @@ 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(); @@ -1374,7 +1293,7 @@ const isDynamic = !!(sectionConfig.discoveryType || sectionConfig.source === 'Dynamic'); if (isDynamic) { // Sync stub + lazy resolve; do not await ensureData here - return window.sectionHelper.buildDiscoverySectionPromise(sectionConfig, { + return await window.sectionHelper.buildDiscoverySectionPromise(sectionConfig, { order: sectionConfig.order, pairSpotlight: true }); @@ -1869,6 +1788,70 @@ let homeChromeCategoryMounted = false; let homeChromeHasPinnedContent = false; let homeChromeVisibilityObserver = null; + let homeChromeRailScrollBound = false; + let homeChromeRailLastY = 0; + const HOME_CHROME_RAIL_SCROLL_DELTA = 8; + const HOME_CHROME_RAIL_MOBILE_MQ = '(max-width: 899px)'; + + function isHomeCategoryRailMobile() { + return typeof window.matchMedia === 'function' + ? window.matchMedia(HOME_CHROME_RAIL_MOBILE_MQ).matches + : window.innerWidth < 900; + } + + function setHomeCategoryRailScrollHidden(hidden) { + const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID); + if (!rail) return; + rail.classList.toggle('is-scroll-hidden', !!hidden); + } + + function clearHomeCategoryRailScrollHidden() { + setHomeCategoryRailScrollHidden(false); + } + + function handleHomeCategoryRailScroll() { + const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID); + if (!rail || rail.classList.contains('hide') || !isHomeCategoryRailMobile()) { + clearHomeCategoryRailScrollHidden(); + return; + } + const y = window.pageYOffset || document.documentElement.scrollTop || 0; + if (y <= HOME_CHROME_RAIL_SCROLL_DELTA) { + clearHomeCategoryRailScrollHidden(); + homeChromeRailLastY = y; + return; + } + const delta = y - homeChromeRailLastY; + if (Math.abs(delta) < HOME_CHROME_RAIL_SCROLL_DELTA) return; + if (delta > 0) { + setHomeCategoryRailScrollHidden(true); + } else { + setHomeCategoryRailScrollHidden(false); + } + homeChromeRailLastY = y; + } + + function syncHomeCategoryRailScrollListener() { + const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID); + const showFilters = getHomeSettings().showCategoryFilters !== false; + const shouldListen = !!(rail && !rail.classList.contains('hide') && isHomePageVisible() && showFilters && isHomeCategoryRailMobile()); + if (shouldListen) { + if (!homeChromeRailScrollBound) { + homeChromeRailLastY = window.pageYOffset || document.documentElement.scrollTop || 0; + window.addEventListener('scroll', handleHomeCategoryRailScroll, { passive: true }); + window.addEventListener('resize', syncHomeCategoryRailScrollListener, { passive: true }); + homeChromeRailScrollBound = true; + } + if (!isHomeCategoryRailMobile()) clearHomeCategoryRailScrollHidden(); + return; + } + if (homeChromeRailScrollBound) { + window.removeEventListener('scroll', handleHomeCategoryRailScroll); + window.removeEventListener('resize', syncHomeCategoryRailScrollListener); + homeChromeRailScrollBound = false; + } + clearHomeCategoryRailScrollHidden(); + } function getHomeSettings() { return window.KefinTweaksConfig?.homeScreenConfig?.HOME_SETTINGS @@ -1915,26 +1898,32 @@ } function isHomePageVisible() { - return !!document.querySelector('.homePage:not(.hide)'); + const active = document.documentElement.getAttribute('data-active-page'); + if (active !== null) { + return active === 'home'; + } + return !!document.querySelector('.homePage:not(.hide) #homeTab.is-active'); } function syncHomeScreenChromeVisibility() { const onHome = isHomePageVisible(); const createBtn = document.getElementById(HOME_CHROME_CREATE_ID); if (createBtn) { - createBtn.hidden = !onHome; + createBtn.classList.toggle('hide', !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.classList.toggle('hide', !visible); rail.setAttribute('aria-hidden', visible ? 'false' : 'true'); + if (!visible) clearHomeCategoryRailScrollHidden(); } if (onHome) { applyActiveHomeCategory(homeChromeActiveCategory || 'none'); } + syncHomeCategoryRailScrollListener(); } function ensureHomeChromeVisibilityObserver() { @@ -1980,7 +1969,8 @@ const wrap = document.createElement('div'); wrap.id = HOME_CHROME_CREATE_ID; wrap.className = 'kefin-home-chrome kefin-home-create-section'; - wrap.hidden = !isHomePageVisible(); + wrap.classList.toggle('hide', !isHomePageVisible()); + wrap.setAttribute('aria-hidden', isHomePageVisible() ? 'false' : 'true'); wrap.innerHTML = `