Add support for Thematic Collections to Enhanced Home Screen

This commit is contained in:
HighImKevin
2026-03-14 15:49:47 -04:00
parent e661e315ca
commit fbc3665f76
6 changed files with 588 additions and 150 deletions
+9 -1
View File
@@ -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';
}
+361 -122
View File
@@ -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 `
<div class="listItemBodyText secondary" style="font-size: 0.85em; margin-bottom: 0.5em;">Default behavior for spotlight sections on the home screen. Per-section overrides can be set when editing a section.</div>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.75em 1.5em;">
${buildSelect('spotlight-spotlightLayout', SPOTLIGHT_LAYOUT_OPTIONS, spotlightLayout, 'Layout')}
${buildSelect('spotlight-spotlightSize', SPOTLIGHT_SIZE_OPTIONS, spotlightSize, 'Size')}
${buildSelect('spotlight-tileCount', SPOTLIGHT_TILE_COUNT_OPTIONS, tileCountStr, 'Tiled backdrop count')}
${buildToggleSlider('spotlight-autoPlay', spotlight.autoPlay !== false, 'Auto-play', { includeHiddenCheckbox: true })}
${buildSelect('spotlight-slideState', SLIDE_STATE_OPTIONS, (spotlight.showSlideState === false ? 'none' : (spotlight.showDots === false ? 'numeric' : 'dots')), 'Slide state')}
${buildToggleSlider('spotlight-showNavButtons', spotlight.showNavButtons !== false, 'Show prev/next buttons', { includeHiddenCheckbox: true })}
${buildToggleSlider('spotlight-showClearArt', spotlight.showClearArt === true, 'Show clear art', { includeHiddenCheckbox: true })}
${buildToggleSlider('spotlight-panAnimation', spotlight.panAnimation !== false, 'Pan animation', { includeHiddenCheckbox: true })}
${buildToggleSlider('spotlight-fullScreen', spotlight.fullScreen === true, 'Full screen', { includeHiddenCheckbox: true })}
${buildToggleSlider('spotlight-dualBackdrops', spotlight.dualBackdrops !== false, 'Dual backdrops', { includeHiddenCheckbox: true })}
<div id="spotlight-animation-dropdowns" style="grid-column: 1 / -1; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.75em 1.5em;">
<div id="spotlight-anim-first">${animRow(1)}</div>
<div id="spotlight-anim-second" style="display: ${tc >= 2 ? 'block' : 'none'};">${animRow(2)}</div>
<div id="spotlight-anim-third" style="display: ${tc >= 3 ? 'block' : 'none'};">${animRow(3)}</div>
</div>
<div id="spotlight-cycle-backdrops-row" style="display: ${cycleRowDisplay}; grid-column: 1 / -1;">
${buildToggleSlider('spotlight-cycleBackdrops', spotlight.cycleBackdrops === true, 'Cycle backdrops', { includeHiddenCheckbox: true })}
${buildTextInput('spotlight-cycleBackdropsTime', spotlight.cycleBackdropsTime ?? defaults.cycleBackdropsTime ?? 10000, 'Cycle backdrops time (ms)', 'number')}
</div>
${buildTextInput('spotlight-interval', spotlight.interval ?? defaults.interval ?? 10000, 'Auto-play interval (ms)', 'number')}
</div>
`;
@@ -1917,24 +2053,79 @@
const spotlightDefaults = window.KefinHomeConfig2?.SPOTLIGHT_SETTINGS || {};
const sectionSpotlight = section.spotlightConfig || {};
const spotlightOpts = { ...spotlightDefaults, ...globalSpotlight, ...sectionSpotlight };
const spotlightLayout = spotlightOpts.spotlightLayout ?? (spotlightOpts.fullScreen === true ? 'Borderless' : 'Border');
const spotlightSize = spotlightOpts.spotlightSize ?? (spotlightOpts.fullScreen === true ? 'full' : 'normal');
const tileCount = spotlightOpts.tileCount ?? (spotlightSize === 'full' ? 1 : spotlightSize === 'large' ? 2 : 3);
const tc = Math.max(1, Math.min(3, parseInt(tileCount, 10) || 1));
const tileCountStr = String(tc);
const cycleRowDisplay = tc > 1 ? 'none' : 'block';
function animRow(n) {
const ord = n === 1 ? 'First' : n === 2 ? 'Second' : 'Third';
return buildSelect(idPrefix + 'spotlight-entranceAnimation' + ord, SPOTLIGHT_ENTRANCE_OPTIONS, spotlightOpts['entranceAnimation' + ord] ?? 'fadeIn', 'Entrance animation ' + n) +
buildSelect(idPrefix + 'spotlight-slideAnimation' + ord, SPOTLIGHT_SLIDE_OPTIONS, spotlightOpts['slideAnimation' + ord] ?? 'kenBurnsZoomIn', 'Slide animation ' + n);
}
const containerId = idPrefix + 'spotlight-options-container';
return `
<div id="${containerId}" style="border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 0.75em; margin-bottom: 1em; display: ${isSpotlight ? 'block' : 'none'};">
<div class="listItemBodyText" style="font-weight: 500; margin-bottom: 0.75em;">Spotlight Options</div>
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.75em 1.5em;">
${buildSelect(idPrefix + 'spotlight-spotlightLayout', SPOTLIGHT_LAYOUT_OPTIONS, spotlightLayout, 'Layout')}
${buildSelect(idPrefix + 'spotlight-spotlightSize', SPOTLIGHT_SIZE_OPTIONS, spotlightSize, 'Size')}
${buildSelect(idPrefix + 'spotlight-tileCount', SPOTLIGHT_TILE_COUNT_OPTIONS, tileCountStr, 'Tiled backdrop count')}
${buildToggleSlider(idPrefix + 'spotlight-autoPlay', spotlightOpts.autoPlay !== false, 'Auto-play', { includeHiddenCheckbox: true })}
${buildSelect(idPrefix + 'spotlight-slideState', SLIDE_STATE_OPTIONS, (spotlightOpts.showSlideState === false ? 'none' : (spotlightOpts.showDots === false ? 'numeric' : 'dots')), 'Slide state')}
${buildToggleSlider(idPrefix + 'spotlight-showNavButtons', spotlightOpts.showNavButtons !== false, 'Show prev/next buttons', { includeHiddenCheckbox: true })}
${buildToggleSlider(idPrefix + 'spotlight-showClearArt', spotlightOpts.showClearArt === true, 'Show clear art', { includeHiddenCheckbox: true })}
${buildToggleSlider(idPrefix + 'spotlight-panAnimation', spotlightOpts.panAnimation !== false, 'Pan animation', { includeHiddenCheckbox: true })}
${buildToggleSlider(idPrefix + 'spotlight-fullScreen', spotlightOpts.fullScreen === true, 'Full screen', { includeHiddenCheckbox: true })}
${buildToggleSlider(idPrefix + 'spotlight-dualBackdrops', spotlightOpts.dualBackdrops !== false, 'Dual backdrops', { includeHiddenCheckbox: true })}
<div id="${idPrefix}spotlight-animation-dropdowns" style="grid-column: 1 / -1; display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.75em 1.5em;">
<div id="${idPrefix}spotlight-anim-first">${animRow(1)}</div>
<div id="${idPrefix}spotlight-anim-second" style="display: ${tc >= 2 ? 'block' : 'none'};">${animRow(2)}</div>
<div id="${idPrefix}spotlight-anim-third" style="display: ${tc >= 3 ? 'block' : 'none'};">${animRow(3)}</div>
</div>
<div id="${idPrefix}spotlight-cycle-backdrops-row" style="display: ${cycleRowDisplay}; grid-column: 1 / -1;">
${buildToggleSlider(idPrefix + 'spotlight-cycleBackdrops', spotlightOpts.cycleBackdrops === true, 'Cycle backdrops', { includeHiddenCheckbox: true })}
${buildTextInput(idPrefix + 'spotlight-cycleBackdropsTime', spotlightOpts.cycleBackdropsTime ?? 10000, 'Cycle backdrops time (ms)', 'number')}
</div>
${buildTextInput(idPrefix + 'spotlight-interval', spotlightOpts.interval ?? 10000, 'Auto-play interval (ms)', 'number')}
</div>
</div>
`;
}
/**
* Attach event listeners for spotlight settings (Size -> tileCount, tileCount -> cycle row + animation dropdown visibility).
* @param {HTMLElement} container - Container that has the spotlight form (e.g. #global-settings-content or spotlight options div)
* @param {string} idPrefix - Prefix for input ids, e.g. 'spotlight-' or 'section-spotlight-' or 'discovery-spotlight-'
*/
function attachSpotlightSettingsListeners(container, idPrefix) {
if (!container) return;
const sizeSelect = container.querySelector('#' + idPrefix + 'spotlightSize');
const tileCountSelect = container.querySelector('#' + idPrefix + 'tileCount');
const cycleRow = container.querySelector('#' + idPrefix + 'cycle-backdrops-row');
const cycleCheckbox = container.querySelector('#' + idPrefix + 'cycleBackdrops');
const animSecond = container.querySelector('#' + idPrefix + 'anim-second');
const animThird = container.querySelector('#' + idPrefix + 'anim-third');
function updateFromTileCount(tc) {
const t = parseInt(tc, 10) || 1;
if (cycleRow) cycleRow.style.display = t > 1 ? 'none' : 'block';
if (cycleCheckbox && t > 1) cycleCheckbox.checked = false;
if (animSecond) animSecond.style.display = t >= 2 ? 'block' : 'none';
if (animThird) animThird.style.display = t >= 3 ? 'block' : 'none';
}
if (sizeSelect) {
sizeSelect.addEventListener('change', () => {
if (!tileCountSelect) return;
const v = sizeSelect.value;
tileCountSelect.value = v === 'full' ? '1' : v === 'large' ? '2' : '3';
updateFromTileCount(tileCountSelect.value);
});
}
if (tileCountSelect) {
tileCountSelect.addEventListener('change', () => updateFromTileCount(tileCountSelect.value));
updateFromTileCount(tileCountSelect.value);
}
}
/**
* Get field configuration for default sections
*/
@@ -2768,17 +2959,28 @@
// Spotlight options (saved when Render Mode = Spotlight)
if (renderMode === 'Spotlight') {
const slideStateVal = dialog.querySelector('#discovery-spotlight-slideState')?.value || 'dots';
const p = 'discovery-spotlight-';
const slideStateVal = dialog.querySelector('#' + p + 'slideState')?.value || 'dots';
const tileCountVal = Math.max(1, Math.min(3, parseInt(dialog.querySelector('#' + p + 'tileCount')?.value || '1', 10)));
discovery.spotlightConfig = {
autoPlay: dialog.querySelector('#discovery-spotlight-autoPlay')?.checked !== false,
interval: parseInt(dialog.querySelector('#discovery-spotlight-interval')?.value || '10000', 10),
spotlightLayout: dialog.querySelector('#' + p + 'spotlightLayout')?.value || 'Border',
spotlightSize: dialog.querySelector('#' + p + 'spotlightSize')?.value || 'normal',
tileCount: tileCountVal,
autoPlay: dialog.querySelector('#' + p + 'autoPlay')?.checked !== false,
interval: parseInt(dialog.querySelector('#' + p + 'interval')?.value || '10000', 10),
showSlideState: slideStateVal !== 'none',
showDots: slideStateVal === 'dots',
showNavButtons: dialog.querySelector('#discovery-spotlight-showNavButtons')?.checked !== false,
showClearArt: dialog.querySelector('#discovery-spotlight-showClearArt')?.checked === true,
panAnimation: dialog.querySelector('#discovery-spotlight-panAnimation')?.checked !== false,
fullScreen: dialog.querySelector('#discovery-spotlight-fullScreen')?.checked === true,
dualBackdrops: dialog.querySelector('#discovery-spotlight-dualBackdrops')?.checked !== false
showNavButtons: dialog.querySelector('#' + p + 'showNavButtons')?.checked !== false,
showClearArt: dialog.querySelector('#' + p + 'showClearArt')?.checked === true,
panAnimation: dialog.querySelector('#' + p + 'panAnimation')?.checked !== false,
entranceAnimationFirst: dialog.querySelector('#' + p + 'entranceAnimationFirst')?.value || 'fadeIn',
entranceAnimationSecond: dialog.querySelector('#' + p + 'entranceAnimationSecond')?.value || 'fadeIn',
entranceAnimationThird: dialog.querySelector('#' + p + 'entranceAnimationThird')?.value || 'fadeIn',
slideAnimationFirst: dialog.querySelector('#' + p + 'slideAnimationFirst')?.value || 'kenBurnsZoomIn',
slideAnimationSecond: dialog.querySelector('#' + p + 'slideAnimationSecond')?.value || 'kenBurnsZoomIn',
slideAnimationThird: dialog.querySelector('#' + p + 'slideAnimationThird')?.value || 'kenBurnsZoomIn',
cycleBackdrops: dialog.querySelector('#' + p + 'cycleBackdrops')?.checked === true,
cycleBackdropsTime: parseInt(dialog.querySelector('#' + p + 'cycleBackdropsTime')?.value || '10000', 10)
};
} else {
delete discovery.spotlightConfig;
@@ -2876,17 +3078,28 @@
section.renderMode = renderMode;
if (renderMode === 'Spotlight') {
const slideStateVal = dialog.querySelector('#section-spotlight-slideState')?.value || 'dots';
const p = 'section-spotlight-';
const slideStateVal = dialog.querySelector('#' + p + 'slideState')?.value || 'dots';
const tileCountVal = Math.max(1, Math.min(3, parseInt(dialog.querySelector('#' + p + 'tileCount')?.value || '1', 10)));
section.spotlightConfig = {
autoPlay: dialog.querySelector('#section-spotlight-autoPlay')?.checked !== false,
interval: parseInt(dialog.querySelector('#section-spotlight-interval')?.value || '10000', 10),
spotlightLayout: dialog.querySelector('#' + p + 'spotlightLayout')?.value || 'Border',
spotlightSize: dialog.querySelector('#' + p + 'spotlightSize')?.value || 'normal',
tileCount: tileCountVal,
autoPlay: dialog.querySelector('#' + p + 'autoPlay')?.checked !== false,
interval: parseInt(dialog.querySelector('#' + p + 'interval')?.value || '10000', 10),
showSlideState: slideStateVal !== 'none',
showDots: slideStateVal === 'dots',
showNavButtons: dialog.querySelector('#section-spotlight-showNavButtons')?.checked !== false,
showClearArt: dialog.querySelector('#section-spotlight-showClearArt')?.checked === true,
panAnimation: dialog.querySelector('#section-spotlight-panAnimation')?.checked !== false,
fullScreen: dialog.querySelector('#section-spotlight-fullScreen')?.checked === true,
dualBackdrops: dialog.querySelector('#section-spotlight-dualBackdrops')?.checked !== false
showNavButtons: dialog.querySelector('#' + p + 'showNavButtons')?.checked !== false,
showClearArt: dialog.querySelector('#' + p + 'showClearArt')?.checked === true,
panAnimation: dialog.querySelector('#' + p + 'panAnimation')?.checked !== false,
entranceAnimationFirst: dialog.querySelector('#' + p + 'entranceAnimationFirst')?.value || 'fadeIn',
entranceAnimationSecond: dialog.querySelector('#' + p + 'entranceAnimationSecond')?.value || 'fadeIn',
entranceAnimationThird: dialog.querySelector('#' + p + 'entranceAnimationThird')?.value || 'fadeIn',
slideAnimationFirst: dialog.querySelector('#' + p + 'slideAnimationFirst')?.value || 'kenBurnsZoomIn',
slideAnimationSecond: dialog.querySelector('#' + p + 'slideAnimationSecond')?.value || 'kenBurnsZoomIn',
slideAnimationThird: dialog.querySelector('#' + p + 'slideAnimationThird')?.value || 'kenBurnsZoomIn',
cycleBackdrops: dialog.querySelector('#' + p + 'cycleBackdrops')?.checked === true,
cycleBackdropsTime: parseInt(dialog.querySelector('#' + p + 'cycleBackdropsTime')?.value || '10000', 10)
};
}
}
@@ -3132,17 +3345,28 @@
// Spotlight options (saved to spotlightConfig when Render Mode = Spotlight)
if (renderMode === 'Spotlight') {
const slideStateVal = dialog.querySelector('#section-spotlight-slideState')?.value || 'dots';
const p = 'section-spotlight-';
const slideStateVal = dialog.querySelector('#' + p + 'slideState')?.value || 'dots';
const tileCountVal = Math.max(1, Math.min(3, parseInt(dialog.querySelector('#' + p + 'tileCount')?.value || '1', 10)));
section.spotlightConfig = {
autoPlay: dialog.querySelector('#section-spotlight-autoPlay')?.checked !== false,
interval: parseInt(dialog.querySelector('#section-spotlight-interval')?.value || '10000', 10),
spotlightLayout: dialog.querySelector('#' + p + 'spotlightLayout')?.value || 'Border',
spotlightSize: dialog.querySelector('#' + p + 'spotlightSize')?.value || 'normal',
tileCount: tileCountVal,
autoPlay: dialog.querySelector('#' + p + 'autoPlay')?.checked !== false,
interval: parseInt(dialog.querySelector('#' + p + 'interval')?.value || '10000', 10),
showSlideState: slideStateVal !== 'none',
showDots: slideStateVal === 'dots',
showNavButtons: dialog.querySelector('#section-spotlight-showNavButtons')?.checked !== false,
showClearArt: dialog.querySelector('#section-spotlight-showClearArt')?.checked === true,
panAnimation: dialog.querySelector('#section-spotlight-panAnimation')?.checked !== false,
fullScreen: dialog.querySelector('#section-spotlight-fullScreen')?.checked === true,
dualBackdrops: dialog.querySelector('#section-spotlight-dualBackdrops')?.checked !== false
showNavButtons: dialog.querySelector('#' + p + 'showNavButtons')?.checked !== false,
showClearArt: dialog.querySelector('#' + p + 'showClearArt')?.checked === true,
panAnimation: dialog.querySelector('#' + p + 'panAnimation')?.checked !== false,
entranceAnimationFirst: dialog.querySelector('#' + p + 'entranceAnimationFirst')?.value || 'fadeIn',
entranceAnimationSecond: dialog.querySelector('#' + p + 'entranceAnimationSecond')?.value || 'fadeIn',
entranceAnimationThird: dialog.querySelector('#' + p + 'entranceAnimationThird')?.value || 'fadeIn',
slideAnimationFirst: dialog.querySelector('#' + p + 'slideAnimationFirst')?.value || 'kenBurnsZoomIn',
slideAnimationSecond: dialog.querySelector('#' + p + 'slideAnimationSecond')?.value || 'kenBurnsZoomIn',
slideAnimationThird: dialog.querySelector('#' + p + 'slideAnimationThird')?.value || 'kenBurnsZoomIn',
cycleBackdrops: dialog.querySelector('#' + p + 'cycleBackdrops')?.checked === true,
cycleBackdropsTime: parseInt(dialog.querySelector('#' + p + 'cycleBackdropsTime')?.value || '10000', 10)
};
} else {
delete section.spotlightConfig;
@@ -3205,7 +3429,11 @@
};
const slideStateVal = dialog.querySelector('#spotlight-slideState')?.value || 'dots';
const tileCountVal = parseInt(dialog.querySelector('#spotlight-tileCount')?.value || '1', 10);
const spotlight = {
spotlightLayout: dialog.querySelector('#spotlight-spotlightLayout')?.value || 'Border',
spotlightSize: dialog.querySelector('#spotlight-spotlightSize')?.value || 'normal',
tileCount: Math.max(1, Math.min(3, tileCountVal)),
autoPlay: dialog.querySelector('#spotlight-autoPlay')?.checked !== false,
interval: parseInt(dialog.querySelector('#spotlight-interval')?.value || '10000', 10),
showSlideState: slideStateVal !== 'none',
@@ -3213,8 +3441,14 @@
showNavButtons: dialog.querySelector('#spotlight-showNavButtons')?.checked !== false,
showClearArt: dialog.querySelector('#spotlight-showClearArt')?.checked === true,
panAnimation: dialog.querySelector('#spotlight-panAnimation')?.checked !== false,
fullScreen: dialog.querySelector('#spotlight-fullScreen')?.checked === true,
dualBackdrops: dialog.querySelector('#spotlight-dualBackdrops')?.checked !== false
entranceAnimationFirst: dialog.querySelector('#spotlight-entranceAnimationFirst')?.value || 'fadeIn',
entranceAnimationSecond: dialog.querySelector('#spotlight-entranceAnimationSecond')?.value || 'fadeIn',
entranceAnimationThird: dialog.querySelector('#spotlight-entranceAnimationThird')?.value || 'fadeIn',
slideAnimationFirst: dialog.querySelector('#spotlight-slideAnimationFirst')?.value || 'kenBurnsZoomIn',
slideAnimationSecond: dialog.querySelector('#spotlight-slideAnimationSecond')?.value || 'kenBurnsZoomIn',
slideAnimationThird: dialog.querySelector('#spotlight-slideAnimationThird')?.value || 'kenBurnsZoomIn',
cycleBackdrops: dialog.querySelector('#spotlight-cycleBackdrops')?.checked === true,
cycleBackdropsTime: parseInt(dialog.querySelector('#spotlight-cycleBackdropsTime')?.value || '10000', 10)
};
const homeSettings = {
@@ -3633,6 +3867,7 @@
const isSpotlight = renderModeSelect.value === 'Spotlight';
if (cardFormatContainer) cardFormatContainer.style.display = isSpotlight ? 'none' : 'block';
if (spotlightOptionsContainer) spotlightOptionsContainer.style.display = isSpotlight ? 'block' : 'none';
if (isSpotlight && spotlightOptionsContainer) attachSpotlightSettingsListeners(spotlightOptionsContainer, 'section-spotlight-');
};
renderModeSelect.addEventListener('change', updateRenderModeDependentVisibility);
updateRenderModeDependentVisibility(); // Set initial state
@@ -3643,7 +3878,9 @@
const discoverySpotlightOptionsContainer = modalInstance.dialogContent.querySelector('#discovery-spotlight-options-container');
if (discoveryRenderModeSelect && discoverySpotlightOptionsContainer) {
const updateDiscoverySpotlightVisibility = () => {
discoverySpotlightOptionsContainer.style.display = discoveryRenderModeSelect.value === 'Spotlight' ? 'block' : 'none';
const isSpotlight = discoveryRenderModeSelect.value === 'Spotlight';
discoverySpotlightOptionsContainer.style.display = isSpotlight ? 'block' : 'none';
if (isSpotlight) attachSpotlightSettingsListeners(discoverySpotlightOptionsContainer, 'discovery-spotlight-');
};
discoveryRenderModeSelect.addEventListener('change', updateDiscoverySpotlightVisibility);
updateDiscoverySpotlightVisibility(); // Set initial state
@@ -4287,6 +4524,7 @@
const contentContainer = dialog.querySelector('#global-settings-content');
if (contentContainer) {
contentContainer.innerHTML = buildGlobalSettingsPanelContent(subTab);
if (subTab === 'spotlight') attachSpotlightSettingsListeners(contentContainer, 'spotlight-');
}
});
/* dialog.querySelectorAll('.section-type-nav-btn').forEach(btn => {
@@ -5617,7 +5855,7 @@
// Load config
const config = await loadConfig();
await verifyRecentlyAddedInLibraryConfig(config);
await verifyLibrarySectionsConfig(config);
// Build content
const content = document.createElement('div');
@@ -5740,7 +5978,7 @@
queryUrl = null;
} else if (window.apiHelper && window.apiHelper.buildQueryFromSection) {
// Use shared builder for consistency
queryUrl = window.apiHelper.buildQueryFromSection(query, userId, serverUrl);
queryUrl = window.apiHelper.buildQueryFromSection(query, userId, serverUrl, section.renderMode === 'Spotlight');
} else {
// Fallback to individual builders
if (query.path) {
@@ -5948,6 +6186,7 @@
// Export getConfig for use by other scripts
window.KefinHomeScreen = window.KefinHomeScreen || {};
window.KefinHomeScreen.getConfig = getConfig;
window.KefinHomeScreen.getSections = getSections;
LOG('Home Screen Configuration UI loaded');
+1
View File
@@ -111,6 +111,7 @@
if (discovery.renderSpotlightAboveMatching !== undefined) settings.renderSpotlightAboveMatching = discovery.renderSpotlightAboveMatching;
if (discovery.randomizeOrder !== undefined) settings.randomizeOrder = discovery.randomizeOrder;
if (discovery.spotlight && typeof discovery.spotlight === 'object') {
settings.spotlight = {};
Object.assign(settings.spotlight, discovery.spotlight);
}
}
+89 -2
View File
@@ -29,6 +29,23 @@
'latestmedia': 'recently-added*' // Pattern match - any section starting with 'recently-added-'
};
/**
* Check if current date is within a seasonal period (MM-DD format).
* @param {string} start - Start date MM-DD
* @param {string} end - End date MM-DD
* @returns {boolean}
*/
function isInSeasonalPeriod(start, end) {
if (!start || !end) return true;
const now = new Date();
const currentYear = now.getFullYear();
const startDate = new Date(`${start}-${currentYear}`);
let endDate = new Date(`${end}-${currentYear}`);
if (endDate < startDate) endDate.setFullYear(currentYear + 1);
else if (endDate > startDate) endDate.setFullYear(currentYear);
return now >= startDate && now <= endDate;
}
/**
* Flatten section groups into a flat array
*/
@@ -470,6 +487,14 @@
// Filter out hidden sections
kefinSections = kefinSections.filter(s => !s.hidden);
// Hide seasonal sections that are not currently in their active period
kefinSections = kefinSections.filter(s => {
if (s.startDate && s.endDate) {
return isInSeasonalPeriod(s.startDate, s.endDate);
}
return true;
});
// Filter out disabled sections
kefinSections = kefinSections.filter(s => s.enabled !== false);
@@ -516,7 +541,7 @@
// Filter out mapped Jellyfin sections if their KefinTweaks equivalent exists
const kefinSectionIds = new Set(kefinSections.map(s => s.id));
const adminKefinSections = window.KefinHomeScreen.getConfig().ENABLED_NORMAL_SECTIONS;
const adminKefinSections = window.KefinHomeScreen.getSections().enabledHomeSections;
const isKefinNextUpEnabled = adminKefinSections.some(s => s.id && s.id === 'nextUp' && s.enabled === true);
const isKefinContinueWatchingEnabled = adminKefinSections.some(s => s.id && s.id === 'continueWatching' && s.enabled === true);
const isKefinMergeNextUpEnabled = adminKefinSections.some(s => s.id && s.id === 'continueWatchingAndNextUp' && s.enabled === true);
@@ -570,11 +595,73 @@
container.innerHTML = `
<div class="verticalSection verticalSection-extrabottompadding">
<h2 class="sectionTitle">Home Sections Order</h2>
<div class="sectionTitleContainer" style="display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 0.5em;">
<h2 class="sectionTitle">Home Sections Order</h2>
<button type="button" class="emby-button raised kefin-restore-defaults-btn" title="Restore Defaults" aria-label="Restore Defaults">
<span class="restoreBtn" aria-hidden="true"></span>
</button>
</div>
${editorHTML}
</div>
`;
// Restore Defaults: confirm then clear kefinHomeScreen and re-render
const restoreBtn = container.querySelector('.kefin-restore-defaults-btn');
if (restoreBtn && window.ModalSystem) {
restoreBtn.addEventListener('click', () => {
const modalId = 'kefin-user-homescreen-restore-defaults';
const content = '<p class="listItemBodyText">Restoring the default Home Screen Settings will override your existing settings. Are you sure you want to proceed?</p>';
const footer = `
<button type="button" class="emby-button" onclick="window.ModalSystem.close('${modalId}')">Cancel</button>
<button type="button" class="emby-button raised button-submit" id="kefin-restore-defaults-confirm">Proceed</button>
`;
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');
+83 -21
View File
@@ -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;
}
+45 -4
View File
@@ -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;
}