diff --git a/configuration/homeScreen-configuration.css b/configuration/homeScreen-configuration.css index 0457130..b22d9c4 100644 --- a/configuration/homeScreen-configuration.css +++ b/configuration/homeScreen-configuration.css @@ -77,10 +77,14 @@ span.listItemBodyText.secondary.seasonal-tag { border-radius: 5px; } -.homeScreenSettingsContainer:has(#kefin-user-home-sections-editor) form>div:first-child { +.homeScreenSettingsContainer:has(#kefin-user-home-sections-editor) form>div:first-child > *:not(.verticalSection-extrabottompadding) { display: none; } +.homeScreenSettingsContainer:has(#kefin-user-home-sections-editor) form>div:first-child { + margin-bottom: 0; +} + .homeScreenSettingsContainer form { display: flex; flex-direction: column; @@ -92,4 +96,8 @@ span.listItemBodyText.secondary.seasonal-tag { .homeScreenSettingsContainer #kefin-user-home-sections-editor { order: 1; +} + +.kefin-restore-defaults-btn .restoreBtn::after { + content: 'Restore Defaults'; } \ No newline at end of file diff --git a/configuration/homeScreen-configuration.js b/configuration/homeScreen-configuration.js index f1a4045..d087a1f 100644 --- a/configuration/homeScreen-configuration.js +++ b/configuration/homeScreen-configuration.js @@ -21,6 +21,8 @@ ExcludeItemIds: { label: 'Exclude Item IDs', type: 'array', hint: 'Comma-separated GUIDs' }, GenreIds: { label: 'Genre IDs', type: 'array', hint: 'Comma-separated GUIDs' }, PersonIds: { label: 'Person IDs', type: 'array', hint: 'Comma-separated GUIDs' }, + PersonTypes: { label: 'Person Types', type: 'array', hint: 'Actor, Director, Writer, etc.' }, + ExcludePersonTypes: { label: 'Exclude Person Types', type: 'array', hint: 'Actor, Director, Writer, etc.' }, StudioIds: { label: 'Studio IDs', type: 'array', hint: 'Comma-separated GUIDs' }, Fields: { label: 'Fields', type: 'array', hint: 'Comma-separated field names' }, MinPremiereDate: { label: 'Min Premiere Date', type: 'date', hint: 'YYYY-MM-DD' }, @@ -66,7 +68,7 @@ EnableUserData: { label: 'Enable User Data', type: 'boolean' } }; - const CARD_FORMATS = ['Poster', 'Thumb', 'Backdrop', 'Square', 'Random']; + const CARD_FORMATS = ['Poster', 'Thumb', 'Backdrop', 'Square', 'Random', 'Button', 'Banner', 'Logo', 'Clear Art', 'Disc']; const SORT_ORDERS = ['None', 'Random', 'Name', 'DateCreated', 'PremiereDate', 'CommunityRating', 'CriticRating', 'DatePlayed', 'SortName', 'PlayCount', 'PlayedPercentage', 'StartDate', 'Runtime', 'ProductionYear', 'IsPlayed', 'IsUnplayed', 'ParentIndexNumber', 'IndexNumber', 'IsFolder', 'SimilarityScore', 'SearchScore', 'DateLastContentAdded', 'SeriesDatePlayed', 'ChildCount']; const SORT_ORDER_DIRECTIONS = ['Ascending', 'Descending']; const RENDER_MODE_OPTIONS = [{ value: 'Normal', label: 'Normal' }, { value: 'Spotlight', label: 'Spotlight' }, { value: 'Random', label: 'Random' }]; @@ -76,6 +78,44 @@ { value: 'numeric', label: 'Numeric' } ]; + const SPOTLIGHT_LAYOUT_OPTIONS = [ + { value: 'Border', label: 'Border' }, + { value: 'Borderless', label: 'Borderless' } + ]; + const SPOTLIGHT_SIZE_OPTIONS = [ + { value: 'normal', label: 'Normal' }, + { value: 'large', label: 'Large' }, + { value: 'full', label: 'Full' } + ]; + const SPOTLIGHT_TILE_COUNT_OPTIONS = [ + { value: '1', label: '1' }, + { value: '2', label: '2' }, + { value: '3', label: '3' } + ]; + const SPOTLIGHT_ENTRANCE_OPTIONS = [ + { value: 'fadeIn', label: 'Fade in' }, + { value: 'fadeInUp', label: 'Fade in up' }, + { value: 'fadeInDown', label: 'Fade in down' } + ]; + const SPOTLIGHT_SLIDE_OPTIONS = [ + { value: 'kenBurnsZoomIn', label: 'Ken Burns zoom in' }, + { value: 'kenBurnsZoomOut', label: 'Ken Burns zoom out' }, + { value: 'kenBurnsZoomInFullscreen', label: 'Ken Burns zoom in (fullscreen)' }, + { value: 'kenBurnsZoomOutFullscreen', label: 'Ken Burns zoom out (fullscreen)' }, + { value: 'kenBurnsPanRight', label: 'Ken Burns pan right' }, + { value: 'kenBurnsPanLeft', label: 'Ken Burns pan left' }, + { value: 'kenBurnsPanUp', label: 'Ken Burns pan up' }, + { value: 'kenBurnsDiagonal', label: 'Ken Burns diagonal' }, + { value: 'fadeInScale', label: 'Fade in scale' }, + { value: 'parallaxFloat', label: 'Parallax float' }, + { value: 'depthPulse', label: 'Depth pulse' }, + { value: 'slowRotate', label: 'Slow rotate' }, + { value: 'breathe', label: 'Breathe' }, + { value: 'heatHaze', label: 'Heat haze' }, + { value: 'colorWash', label: 'Color wash' }, + { value: 'vignetteIn', label: 'Vignette in' } + ]; + // Current config state let currentConfig = null; let mainModalInstance = null; @@ -348,116 +388,158 @@ } /** - * Verify and sync recently-added library sections with actual Jellyfin libraries + * Verify and sync library-based section groups (Recently Added, Popular Genres, etc.) with actual Jellyfin libraries * @param {Object} config - Current configuration object - * @returns {Object} Updated configuration with synced recently-added sections + * @returns {Object} Updated configuration with synced library-based sections */ - async function verifyRecentlyAddedInLibraryConfig(config) { + async function verifyLibrarySectionsConfig(config) { try { if (!window.ApiClient || !window.ApiClient._loggedIn) { - WARN('User not logged in, skipping recently-added verification'); + WARN('User not logged in, skipping library sections verification'); return config; } if (!window.dataHelper || !window.dataHelper.getLibraries) { - WARN('dataHelper.getLibraries not available, skipping recently-added verification'); + WARN('dataHelper.getLibraries not available, skipping library sections verification'); return config; } - // Get current libraries const libraries = await window.dataHelper.getLibraries(); if (!libraries || libraries.length === 0) { - LOG('No libraries found, skipping recently-added verification'); + LOG('No libraries found, skipping library sections verification'); return config; } - // Flatten groups to get all sections - const allSections = flattenSectionGroups(config.HOME_SECTION_GROUPS || []); - - // Extract existing recently-added sections - const recentlyAddedSections = allSections.filter(s => s.id && s.id.startsWith('recently-added-')); - const existingLibraryIds = new Set(recentlyAddedSections.map(s => s.id.replace('recently-added-', ''))); - - // Get current library IDs - const currentLibraryIds = new Set(libraries.map(l => l.Id)); - - // Find sections to remove (libraries that no longer exist) - const sectionsToRemove = recentlyAddedSections.filter(s => { - const libId = s.id.replace('recently-added-', ''); - return !currentLibraryIds.has(libId); - }); - - // Find libraries to add (new libraries not in config) - const librariesToAdd = libraries.filter(l => !existingLibraryIds.has(l.Id) && l.CollectionType && l.CollectionType !== 'boxsets' && l.CollectionType !== 'playlists'); - - if (sectionsToRemove.length === 0 && librariesToAdd.length === 0) { - LOG('Recently-added sections are in sync with libraries'); - return config; - } - - LOG(`Syncing recently-added sections: Removing ${sectionsToRemove.length}, Adding ${librariesToAdd.length}`); - - // Remove sections for non-existent libraries - const sectionsToRemoveIds = new Set(sectionsToRemove.map(s => s.id)); - - // Update groups by removing sections - const updatedGroups = (config.HOME_SECTION_GROUPS || []).map(group => { - if (!group.sections) return group; - const filteredSections = group.sections.filter(s => !sectionsToRemoveIds.has(s.id)); - return { - ...group, - sections: filteredSections - }; - }); - - // Add sections for new libraries - librariesToAdd.forEach(library => { - let viewMoreUrl = null; - - if (library.CollectionType === 'movies') { - viewMoreUrl = `#/movies.html?topParentId=${library.Id}&collectionType=movies&tab=1`; - } else if (library.CollectionType === 'tvshows') { - viewMoreUrl = `#/tv.html?topParentId=${library.Id}&collectionType=tvshows&tab=1`; - } - - const newSection = { - id: `recently-added-${library.Id}`, - name: `Recently Added ${library.Name}`, - enabled: false, // Default to disabled - order: 61, - cardFormat: 'Poster', - viewMoreUrl: viewMoreUrl, - queries: [{ - path: '/Items/Latest', - queryOptions: { - ParentId: library.Id, - SortBy: 'DateCreated', - SortOrder: 'Descending', - Limit: 16 + // Filter out boxsets and playlists; all templates apply to this list + const filteredLibraries = libraries.filter(l => l.CollectionType && l.CollectionType !== 'boxsets' && l.CollectionType !== 'playlists'); + const currentLibraryIds = new Set(filteredLibraries.map(l => l.Id)); + + const libraryTemplates = [ + { + idPrefix: 'recently-added-', + groupId: 'home-recently-added', + groupName: 'Recently Added', + sectionTemplate: { + enabled: false, + order: 61, + cardFormat: 'Poster', + buildSection: function (library) { + let viewMoreUrl = null; + if (library.CollectionType === 'movies') { + viewMoreUrl = `#/movies.html?topParentId=${library.Id}&collectionType=movies&tab=1`; + } else if (library.CollectionType === 'tvshows') { + viewMoreUrl = `#/tv.html?topParentId=${library.Id}&collectionType=tvshows&tab=1`; + } + return { + id: `recently-added-${library.Id}`, + name: `Recently Added ${library.Name}`, + viewMoreUrl: viewMoreUrl, + queries: [{ + path: '/Items/Latest', + queryOptions: { + ParentId: library.Id, + SortBy: 'DateCreated', + SortOrder: 'Descending', + Limit: 16 + } + }] + }; } - }] - }; - - // Find or create "Recently Added" group - let recentlyAddedGroup = updatedGroups.find(g => g.name === 'Recently Added'); - if (!recentlyAddedGroup) { - recentlyAddedGroup = { - name: 'Recently Added', + } + }, + { + idPrefix: 'popular-genres-', + groupId: 'home-popular-genres', + groupName: 'Popular Genres', + sectionTemplate: { + enabled: false, + order: 81, + cardFormat: 'Thumb', + ttl: 604800000, + buildSection: function (library) { + const ct = (library.CollectionType || '').toLowerCase(); + let includeItemTypes = ['Movie', 'Series', 'Video', 'MusicArtist', 'Book', 'AudioBook']; + if (ct === 'music') includeItemTypes = ['MusicArtist']; + else if (ct === 'tvshows') includeItemTypes = ['Series']; + else if (ct === 'movies') includeItemTypes = ['Movie']; + else if (ct === 'books') includeItemTypes = ['Book', 'AudioBook']; + else if (ct === 'homevideos') includeItemTypes = ['Video']; + return { + id: `popular-genres-${library.Id}`, + name: `Popular ${library.Name} Genres`, + queries: [{ + path: '/Genres', + ParentId: library.Id, + queryOptions: { + ParentId: library.Id, + IncludeItemTypes: includeItemTypes, + SortBy: 'ChildCount', + SortOrder: 'Descending', + Limit: 20 + } + }] + }; + } + } + } + ]; + + const groups = config.HOME_SECTION_GROUPS || []; + let updatedGroups = groups.map(g => ({ ...g, sections: Array.isArray(g.sections) ? [...g.sections] : [] })); + let hasChanges = false; + + for (const template of libraryTemplates) { + let group = updatedGroups.find(g => g.id === template.groupId); + if (!group) { + group = { + id: template.groupId, + name: template.groupName, sections: [] }; - updatedGroups.push(recentlyAddedGroup); + updatedGroups.push(group); + hasChanges = true; } - - recentlyAddedGroup.sections.push(newSection); - }); - - // Update config + + const groupSections = group.sections || []; + const templateSectionIds = groupSections.filter(s => s.id && s.id.startsWith(template.idPrefix)).map(s => s.id); + const existingLibraryIdsForTemplate = new Set(templateSectionIds.map(id => id.replace(template.idPrefix, ''))); + + const sectionsToRemove = groupSections.filter(s => { + if (!s.id || !s.id.startsWith(template.idPrefix)) return false; + const libId = s.id.replace(template.idPrefix, ''); + return !currentLibraryIds.has(libId); + }); + const sectionsToRemoveIds = new Set(sectionsToRemove.map(s => s.id)); + const librariesToAdd = filteredLibraries.filter(l => !existingLibraryIdsForTemplate.has(l.Id)); + + if (sectionsToRemoveIds.size > 0 || librariesToAdd.length > 0) hasChanges = true; + + group.sections = groupSections.filter(s => !sectionsToRemoveIds.has(s.id)); + + librariesToAdd.forEach(library => { + const base = template.sectionTemplate.buildSection(library); + const newSection = { + ...template.sectionTemplate, + ...base, + id: base.id, + name: base.name, + queries: base.queries + }; + delete newSection.buildSection; + group.sections.push(newSection); + }); + } + + if (!hasChanges) { + LOG('Library-based sections are in sync with libraries'); + return config; + } + config.HOME_SECTION_GROUPS = updatedGroups; - - LOG('Recently-added sections synced successfully'); + LOG('Library-based sections synced successfully'); return config; } catch (error) { - ERR('Error verifying recently-added library config:', error); + ERR('Error verifying library sections config:', error); return config; } } @@ -534,7 +616,7 @@ mergedConfig.ENABLED_DISCOVERY_SECTIONS = [...enabledDiscoverySections, ...enabledCustomDiscoverySections]; // Verify and sync recently-added library sections - //await verifyRecentlyAddedInLibraryConfig(mergedConfig); + //await verifyLibrarySectionsConfig(mergedConfig); currentConfig = mergedConfig; return mergedConfig; @@ -556,6 +638,39 @@ return config; } + /** + * Get enabled home sections and discovery sections + * @returns {Object} Object with enabledHomeSections and enabledDiscoverySections + */ + function getSections() { + + const config = getConfig(); + + const isInSeasonalPeriod = (startDate, endDate) => { + const currentDate = new Date(); + const start = new Date(startDate); + const end = new Date(endDate); + + // Ensure the years are the same as the current date year + start.setFullYear(currentDate.getFullYear()); + end.setFullYear(currentDate.getFullYear()); + + return currentDate >= start && currentDate <= end; + }; + + const enabledHomeSections = flattenSectionGroups(config.HOME_SECTION_GROUPS).filter(s => s.enabled === true && s.discoveryEnabled !== true && (s.startDate && s.endDate ? isInSeasonalPeriod(s.startDate, s.endDate) : true)); + const enabledSeasonalSections = flattenSectionGroups(config.SEASONAL_SECTION_GROUPS).filter(s => s.enabled === true && s.discoveryEnabled !== true && isInSeasonalPeriod(s.startDate, s.endDate)); + const enabledCustomSections = flattenSectionGroups(config.CUSTOM_SECTION_GROUPS).filter(s => s.enabled === true && s.discoveryEnabled !== true && (s.startDate && s.endDate ? isInSeasonalPeriod(s.startDate, s.endDate) : true)); + const enabledDiscoverySections = flattenSectionGroups(config.DISCOVERY_SECTION_GROUPS).filter(s => s.enabled === true && (s.startDate && s.endDate ? isInSeasonalPeriod(s.startDate, s.endDate) : true)); + const enabledCustomDiscoverySections = flattenSectionGroups(config.CUSTOM_SECTION_GROUPS).filter(s => s.enabled === true && s.discoveryEnabled === true && (s.startDate && s.endDate ? isInSeasonalPeriod(s.startDate, s.endDate) : true)); + + + return { + enabledHomeSections: [...enabledHomeSections, ...enabledSeasonalSections, ...enabledCustomSections], + enabledDiscoverySections: [...enabledDiscoverySections, ...enabledCustomDiscoverySections] + } + } + /** * Merge default groups with admin overrides * @param {Array} defaultGroups - Array of default HomeScreenSectionGroup objects @@ -1889,16 +2004,37 @@ if (!currentConfig) return ''; const spotlight = currentConfig.SPOTLIGHT_SETTINGS || {}; const defaults = window.KefinHomeConfig2?.SPOTLIGHT_SETTINGS || {}; + const spotlightLayout = spotlight.spotlightLayout ?? (spotlight.fullScreen === true ? 'Borderless' : 'Border'); + const spotlightSize = spotlight.spotlightSize ?? (spotlight.fullScreen === true ? 'full' : 'normal'); + const tileCount = spotlight.tileCount ?? (spotlightSize === 'full' ? 1 : spotlightSize === 'large' ? 2 : 3); + const tileCountStr = String(Math.max(1, Math.min(3, parseInt(tileCount, 10) || 1))); + const tc = Math.max(1, Math.min(3, parseInt(tileCount, 10) || 1)); + const cycleRowDisplay = tc > 1 ? 'none' : 'block'; + function animRow(n) { + const ord = n === 1 ? 'First' : n === 2 ? 'Second' : 'Third'; + return buildSelect('spotlight-entranceAnimation' + ord, SPOTLIGHT_ENTRANCE_OPTIONS, spotlight['entranceAnimation' + ord] ?? defaults['entranceAnimation' + ord] ?? 'fadeIn', 'Entrance animation ' + n) + + buildSelect('spotlight-slideAnimation' + ord, SPOTLIGHT_SLIDE_OPTIONS, spotlight['slideAnimation' + ord] ?? defaults['slideAnimation' + ord] ?? 'kenBurnsZoomIn', 'Slide animation ' + n); + } return `
Restoring the default Home Screen Settings will override your existing settings. Are you sure you want to proceed?
'; + const footer = ` + + + `; + window.ModalSystem.create({ + id: modalId, + title: 'Restore Defaults', + content: content, + footer: footer, + closeOnBackdrop: true, + closeOnEscape: true, + showCloseButton: true, + onOpen: (modal) => { + const confirmBtn = modal.dialogFooter && modal.dialogFooter.querySelector('#kefin-restore-defaults-confirm'); + if (confirmBtn) { + confirmBtn.addEventListener('click', async () => { + try { + if (!window.userHelper || !window.userHelper.getUserDisplayPreferences || !window.userHelper.updateDisplayPreferences) { + WARN('userHelper not available'); + return; + } + const { promise } = await window.userHelper.getUserDisplayPreferences(); + const displayPrefs = await promise; + if (!displayPrefs) { + WARN('Could not load display preferences'); + return; + } + if (!displayPrefs.CustomPrefs) { + displayPrefs.CustomPrefs = {}; + } + displayPrefs.CustomPrefs.kefinHomeScreen = '[]'; + const ok = await window.userHelper.updateDisplayPreferences(displayPrefs); + if (ok) { + window.ModalSystem.close(modalId); + await renderUserHomeSectionsEditor(container); + if (window.KefinTweaksToaster && window.KefinTweaksToaster.toast) { + window.KefinTweaksToaster.toast('Home Screen settings restored to defaults.'); + } + } else { + WARN('Failed to restore defaults'); + } + } catch (e) { + ERR('Error restoring defaults:', e); + } + }); + } + } + }); + }); + } + // Setup event listeners // Find the sections-container which wraps both enabled and disabled lists const editorContainer = container.querySelector('.sections-container') || container.querySelector('.group-sections-list'); diff --git a/scripts/homeScreen.css b/scripts/homeScreen.css index bcf1257..0b875e0 100644 --- a/scripts/homeScreen.css +++ b/scripts/homeScreen.css @@ -1,27 +1,10 @@ -/* Please use the CSS commented below to customize the order of KefinTweaks custom home sections */ - -/* -[data-custom-section-id="watchlist"] { - order: 60 !important; -} - -[data-custom-section-id="popular-tv-networks"] { - order: 61 !important; -} - -[data-custom-section-id="new-movies"] { - order: 30 !important; -} - -[data-custom-section-id="new-episodes"] { - order: 31 !important; -} -*/ +@import url('https://fonts.googleapis.com/css2?family=Oswald:wght@200..700&display=swap'); .sections.homeSectionsContainer { display: flex; flex-direction: column; padding-bottom: 100px; + gap: 1rem; } body[data-seasonal-background="true"]:has(.homePage:not(.hide)) #reactRoot:has(.libraryPage:not(.hide) .pageTabContent.is-active[data-index="0"])>.backgroundContainer::after { @@ -238,6 +221,85 @@ div#discovery-loading-indicator:not(.show)::after { display: none !important; } -.homeSectionsContainer > * { - content-visibility: auto; +.homeSectionsContainer .card[data-type="Folder"] .cardScalable > a.cardImageContainer::after { + content: attr(aria-label); + font-family: 'Oswald'; + font-size: 1.5rem; + line-height: 1.8rem; + max-width: calc(100% - 2rem); + white-space: pre-wrap; + text-align: left; + overflow: hidden; + text-overflow: ellipsis; + text-transform: uppercase; + position: absolute; + top: 1rem; + left: 1rem; + background: none; +} + +.homeSectionsContainer .card[data-type="Folder"] .cardScalable > a.cardImageContainer::before { + content: ''; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background: linear-gradient(#000000, rgb(0 0 0 / 0%) 50%); + box-shadow: inset 0px 0px 20px 5px black; +} + +.layout-mobile .homeSectionsContainer .card[data-type="Folder"] a.cardImageContainer::after { + top: 0.5rem; + left: 0.5rem; + font-size: 1.5rem !important; + line-height: 1.8rem !important; +} + +.homeSectionsContainer .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; +} + +.homeSectionsContainer .section0, +.homeSectionsContainer .section1, +.homeSectionsContainer .section2, +.homeSectionsContainer .section3, +.homeSectionsContainer .section4, +.homeSectionsContainer .section5, +.homeSectionsContainer .section6, +.homeSectionsContainer .section7, +.homeSectionsContainer .section8, +.homeSectionsContainer .section9 { + display: none !important; +} + +.homeSectionsContainer .card[data-type="TvChannel"] a.cardImageContainer:not([style])::after { + content: attr(aria-label); + font-size: 2rem; + text-wrap: auto; + text-align: center; +} + +.emby-scroller-container[data-section-id="liveTv-channels"] .itemsContainer { + gap: 4rem +} +.layout-mobile .emby-scroller-container[data-section-id="liveTv-channels"] .itemsContainer { + gap: 2rem; +} + +[data-section-id="upcoming"] .missingIndicator { + display: none; } \ No newline at end of file diff --git a/scripts/homeScreen3.js b/scripts/homeScreen3.js index e00341d..2277be9 100644 --- a/scripts/homeScreen3.js +++ b/scripts/homeScreen3.js @@ -571,12 +571,12 @@ async function mergeHomeSectionConfigs(defaultSections) { const performanceStartTime = performance.now(); - const kefinHomeScreenConfig = await window.KefinHomeScreen.getConfig(); + 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 kefinHomeScreenConfig.ENABLED_NORMAL_SECTIONS; + return kefinSections.enabledHomeSections; // Return all enabled sections from HOME_SECTION_GROUPS const homeSections = flattenSectionGroups(kefinHomeScreenConfig.HOME_SECTION_GROUPS).filter(s => s.enabled === true); @@ -1306,9 +1306,50 @@ } } - // Ensure queries array exists + // Static items: normalize and return immediately (no queries, no dataPromise) + if (sectionConfig.items && sectionConfig.items.length > 0) { + const kefinTweaksRoot = window.KefinTweaksConfig?.kefinTweaksRoot || ''; + const serverId = ApiClient.serverId(); + const normalizeTemplate = (value) => (value || '') + .replace(/\$\{kefinTweaksRoot\}/g, kefinTweaksRoot) + .replace(/\$\{serverId\}/g, serverId); + + const normalizedItems = sectionConfig.items.map((item, index) => { + const posterUrl = normalizeTemplate(item.posterUrl); + const thumbUrl = normalizeTemplate(item.thumbUrl); + const squareUrl = normalizeTemplate(item.squareUrl); + const imageUrl = normalizeTemplate(item.imageUrl); + const cardUrl = normalizeTemplate(item.cardUrl); + + return { + Name: item.Name, + Id: item.Id || 'static-' + sectionConfig.id + '-' + index, + Type: item.Type || 'Folder', + posterUrl: posterUrl, + thumbUrl: thumbUrl, + squareUrl: squareUrl, + imageUrl: imageUrl, + cardUrl: cardUrl, + CustomFooterText: item.cardFooter || undefined + }; + }); + const loadSectionTimerEnd = performance.now(); + const loadSectionDuration = loadSectionTimerEnd - loadSectionTimerStart; + LOG(`Section ${sectionConfig.id} (static items) loaded for rendering in time: ${loadSectionDuration.toFixed(2)}ms`); + let postProcessedItems = normalizedItems; + if (window.cardBuilder.postProcessItems) { + postProcessedItems = window.cardBuilder.postProcessItems(sectionConfig, normalizedItems); + } + + return { + config: sectionConfig, + result: { data: postProcessedItems } + }; + } + + // Ensure queries array exists when section has no static items if (!sectionConfig.queries || !Array.isArray(sectionConfig.queries) || sectionConfig.queries.length === 0) { - WARN(`Section ${sectionConfig.id} has no queries array`); + WARN(`Section ${sectionConfig.id} has no queries array and no static items`); return null; }