Add a configurable timer to dismiss empty sections

This commit is contained in:
HighImKevin committed 2026-09-19 19:22:07 -04:00
1 parent 6bb1c3b5ae
commit b493ea1516
2 files changed
+129 -19

No files matched your search

@@ -2237,6 +2237,12 @@
<div class="hsc-settings-span-2">
${hscToggleCard('home-ensureThumbsForPopularTVNetworks', homeSettings.ensureThumbsForPopularTVNetworks === true, 'Require Studio Thumbs', D)}
</div>
<div class="hsc-settings-span-2">
${buildTextInput('home-dismissEmptySectionTimer', homeSettings.dismissEmptySectionTimer ?? 0, 'Empty Section Dismiss Timer (ms)', 'number')}
<div class="listItemBodyText secondary" style="font-size: 0.85em; margin-top: 0.35em;">
When a section loads with no items: <code>0</code> removes it immediately. Any other value shows “No items found” for that many milliseconds, then fades the section out.
</div>
</div>
</div>
<div class="hsc-settings-group" style="${settingsGroupStyle}">
<div class="listItemBodyText" style="font-weight: 500; font-size: 1.1em;">Top People</div>
@@ -2420,6 +2426,7 @@
|| root.querySelector('#userHome-inlineConfigure')
|| root.querySelector('#libraryCache-mdblistApiKey')
|| root.querySelector('#home-minPeopleAppearancesTotal')
|| root.querySelector('#home-dismissEmptySectionTimer')
|| root.querySelector('#home-loadPeopleEpisodeData')) {
const prevLoadPeopleEpisodeData = currentConfig.HOME_SETTINGS?.loadPeopleEpisodeData === true;
currentConfig.SEASONAL_THEME_SETTINGS = {
@@ -2433,11 +2440,13 @@
: season.enabled
}))
};
const dismissEmptyRaw = parseInt(root.querySelector('#home-dismissEmptySectionTimer')?.value ?? '0', 10);
currentConfig.HOME_SETTINGS = {
...(currentConfig.HOME_SETTINGS || {}),
fadeInSections: root.querySelector('#home-fadeInSections')?.checked === true,
ensureThumbsForPopularTVNetworks: root.querySelector('#home-ensureThumbsForPopularTVNetworks')?.checked === true,
showStaleDataBeforeRefresh: root.querySelector('#home-showStaleDataBeforeRefresh')?.checked === true,
dismissEmptySectionTimer: Number.isFinite(dismissEmptyRaw) && dismissEmptyRaw > 0 ? dismissEmptyRaw : 0,
minPeopleAppearancesTotal: parseInt(root.querySelector('#home-minPeopleAppearancesTotal')?.value || '10', 10),
minPeopleAppearancesMovies: parseInt(root.querySelector('#home-minPeopleAppearancesMovies')?.value || '10', 10),
minPeopleAppearancesSeries: parseInt(root.querySelector('#home-minPeopleAppearancesSeries')?.value || '10', 10),
+120 -19
View File
@@ -13,6 +13,10 @@
const SPOTLIGHT_WINDOW_PREV = 4;
const SPOTLIGHT_WINDOW_NEXT = 4;
const state = {
defaultCardBackgroundNumber: 1
}
function getSpotlightWindowIndices(currentIndex, totalLength) {
if (totalLength <= 9) {
return Array.from({ length: totalLength }, (_, i) => i);
@@ -1898,10 +1902,11 @@
* @param {boolean} overflowCard - Use overflow card classes instead of normal card classes
* @param {string} cardFormat - Override card format: 'portrait', 'backdrop', 'thumb', or 'square'
* @param {string} customFooterText - Optional custom footer text (e.g., air date for episodes)
* @param {boolean} useParentCard - When true, the parent item will be rendered as the card image and link
* @returns {HTMLElement} - The constructed card element
*/
buildCard: function(item, overflowCard = false, cardFormat = null, customFooterText = null) {
return createJellyfinCardElement(item, overflowCard, cardFormat, customFooterText);
buildCard: function(item, overflowCard = false, cardFormat = null, customFooterText = null, useParentCard = false) {
return createJellyfinCardElement(item, overflowCard, cardFormat, customFooterText, useParentCard);
},
/**
@@ -3045,9 +3050,10 @@
* @param {boolean} overflowCard - Use overflow card classes instead of normal card classes
* @param {string} cardFormat - Override card format: 'portrait', 'backdrop', 'thumb', 'square', 'series thumb', 'series poster'
* @param {string} customFooterText - Optional custom footer text (e.g., air date for episodes)
* @param {boolean} useParentCard - When true, the parent item will be rendered as the card image and link
* @returns {HTMLElement} - The constructed card element
*/
function createJellyfinCardElement(item, overflowCard = false, cardFormat = null, customFooterText = null) {
function createJellyfinCardElement(item, overflowCard = false, cardFormat = null, customFooterText = null, useParentCard = false) {
cardFormat = cardFormat?.toLowerCase() || null;
if (cardFormat === 'button') {
return createLibraryButtonElement(item);
@@ -3110,6 +3116,8 @@
imageParams = 'fillHeight=446&fillWidth=297';
}
}
const cardLinkId = useParentCard && (item.SeriesId || item.ParentId) ? (item.SeriesId || item.ParentId) : itemId;
// Create the main card container
const card = document.createElement('div');
@@ -3117,7 +3125,7 @@
card.setAttribute('data-index', '0');
card.setAttribute('data-isfolder', itemType === 'MusicAlbum' || itemType === 'Artist' || item.IsFolder ? 'true' : 'false');
card.setAttribute('data-serverid', serverId);
if (!isCustomCard) card.setAttribute('data-id', itemId);
if (!isCustomCard) card.setAttribute('data-id', cardLinkId);
if (isCustomCard) card.setAttribute('data-custom-card', 'true');
card.setAttribute('data-type', itemType);
card.setAttribute('data-retro-marquee', getRetroPosterMarqueeLabel(item));
@@ -3184,7 +3192,7 @@
libraryUrl = buildCollectionFolderHref(item, serverId, { useHtmlExtension: true }) || '';
}
let cardUrl = item.cardUrl || libraryUrl || `#/details?id=${itemId}&serverId=${serverId}`;
let cardUrl = item.cardUrl || libraryUrl || `#/details?id=${cardLinkId}&serverId=${serverId}`;
if (!item.cardUrl && itemType === 'Genre') {
const parentParam = item.ParentId ? `&parentId=${item.ParentId}` : '';
@@ -3212,7 +3220,13 @@
imageUrl = `${item.posterUrl || item.imageUrl || item.thumbUrl || item.squareUrl}`;
}
} else if (cardFormat === 'backdrop') {
if (item.BackdropImageTags[0]) {
if (useParentCard && item.ParentBackdropImageTags && item.ParentBackdropImageTags[0]) {
imageUrl = `${serverAddress}/Items/${item.ParentBackdropItemId}/Images/Backdrop?${imageParams}&quality=96&tag=${item.ParentBackdropImageTags[0]}`;
} else if (useParentCard && item.ParentThumbImageTag) {
imageUrl = `${serverAddress}/Items/${item.ParentThumbItemId}/Images/Thumb?${imageParams}&quality=96&tag=${item.ParentThumbImageTag}`;
} else if (useParentCard && item.SeriesPrimaryImageTag) {
imageUrl = `${serverAddress}/Items/${item.SeriesId}/Images/Primary?${imageParams}&quality=96&tag=${item.SeriesPrimaryImageTag}`;
} else if (item.BackdropImageTags[0]) {
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Backdrop?${imageParams}&quality=96&tag=${item.BackdropImageTags[0]}`;
} else if (item.ParentBackdropImageTags && item.ParentBackdropImageTags[0]) {
imageUrl = `${serverAddress}/Items/${item.ParentBackdropItemId}/Images/Backdrop?${imageParams}&quality=96&tag=${item.ParentBackdropImageTags[0]}`;
@@ -3222,7 +3236,13 @@
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.ImageTags?.Primary}`;
}
} else if (cardFormat === 'square') {
if (item.Type === 'Season' || item.Type === 'Series' || item.Type === 'Movie') {
if (useParentCard && item.ParentBackdropImageTags && item.ParentBackdropImageTags[0]) {
imageUrl = `${serverAddress}/Items/${item.ParentBackdropItemId}/Images/Backdrop?${imageParams}&quality=96&tag=${item.ParentBackdropImageTags[0]}`;
} else if (useParentCard && item.ParentThumbImageTag) {
imageUrl = `${serverAddress}/Items/${item.ParentThumbItemId}/Images/Thumb?${imageParams}&quality=96&tag=${item.ParentThumbImageTag}`;
} else if (useParentCard && item.SeriesPrimaryImageTag) {
imageUrl = `${serverAddress}/Items/${item.SeriesId}/Images/Primary?${imageParams}&quality=96&tag=${item.SeriesPrimaryImageTag}`;
} else if (item.Type === 'Season' || item.Type === 'Series' || item.Type === 'Movie') {
imageUrl = item.BackdropImageTags && item.BackdropImageTags[0] ? `${serverAddress}/Items/${item.Id}/Images/Backdrop?${imageParams}&quality=96&tag=${item.BackdropImageTags[0]}` : item.ImageTags?.Primary ? `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.ImageTags?.Primary}` : '';
} else if (item.ImageTags?.Primary) {
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.ImageTags?.Primary}`;
@@ -3242,7 +3262,7 @@
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.PrimaryImageTag}`;
}
} else if (cardFormat === 'portrait' || cardFormat === 'poster') {
if (item.Type === 'Episode' && item.SeriesPrimaryImageTag) {
if ((useParentCard || item.Type === 'Episode') && item.SeriesPrimaryImageTag) {
imageUrl = `${serverAddress}/Items/${item.SeriesId}/Images/Primary?${imageParams}&quality=96&tag=${item.SeriesPrimaryImageTag}`;
} else if (item.ImageTags?.Primary) {
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.ImageTags?.Primary}`;
@@ -3270,7 +3290,7 @@
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.ImageTags.Primary}`;
}
} else if (cardFormat === 'thumb') {
if (item.Type === 'Episode') {
if (item.Type === 'Episode' && !useParentCard) {
if (item.ImageTags?.Primary) {
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Primary?${imageParams}&quality=96&tag=${item.ImageTags.Primary}`;
} else if (item.PrimaryImageTag) {
@@ -3284,6 +3304,10 @@
} else if (item.SeriesPrimaryImageTag) {
imageUrl = `${serverAddress}/Items/${item.SeriesId}/Images/Primary?${imageParams}&quality=96&tag=${item.SeriesPrimaryImageTag}`;
}
} else if (useParentCard && item.ParentThumbImageTag) {
imageUrl = `${serverAddress}/Items/${item.ParentThumbItemId}/Images/Thumb?${imageParams}&quality=96&tag=${item.ParentThumbImageTag}`;
} else if (useParentCard && item.ParentBackdropImageTags && item.ParentBackdropImageTags[0]) {
imageUrl = `${serverAddress}/Items/${item.ParentBackdropItemId}/Images/Backdrop?${imageParams}&quality=96&tag=${item.ParentBackdropImageTags[0]}`;
} else if (item.ImageTags?.Thumb) {
// Prefer explicit thumb on the item
imageUrl = `${serverAddress}/Items/${item.Id}/Images/Thumb?${imageParams}&quality=96&tag=${item.ImageTags?.Thumb}`;
@@ -3352,9 +3376,13 @@
// Card image container
const cardImageContainer = document.createElement('a');
cardImageContainer.href = cardUrl;
cardImageContainer.className = `cardImageContainer ${!imageUrl ? 'defaultCardBackground defaultCardBackground2' : ''} coveredImage cardContent itemAction lazy blurhashed lazy-image-fadein-fast`;
cardImageContainer.className = `cardImageContainer ${!imageUrl ? 'defaultCardBackground' + state.defaultCardBackgroundNumber++ : ''} coveredImage cardContent itemAction lazy blurhashed lazy-image-fadein-fast`;
cardImageContainer.setAttribute('data-action', 'link');
cardImageContainer.setAttribute('aria-label', item.Name || 'Unknown');
if (state.defaultCardBackgroundNumber > 5) {
state.defaultCardBackgroundNumber = 1;
}
// No image - add icon as inner element
if (!imageUrl) {
@@ -6136,8 +6164,9 @@
const itemsContainer = createItemsSliderElement();
// Add items to container
const useParentCard = !!sectionConfig?.useParentCard;
items.forEach((item, index) => {
const card = createJellyfinCardElement(item, overflowCard, cardFormat, item.cardFooter);
const card = createJellyfinCardElement(item, overflowCard, cardFormat, item.cardFooter, useParentCard);
card.setAttribute('data-index', index);
itemsContainer.appendChild(card);
});
@@ -6945,10 +6974,10 @@
});
}
function appendCardsForItems(itemsContainer, items, overflowCard, cardFormat, startIndex = 0) {
function appendCardsForItems(itemsContainer, items, overflowCard, cardFormat, startIndex = 0, useParentCard = false) {
const cards = [];
items.forEach((item, offset) => {
const card = createJellyfinCardElement(item, overflowCard, cardFormat, item.cardFooter);
const card = createJellyfinCardElement(item, overflowCard, cardFormat, item.cardFooter, useParentCard);
card.setAttribute('data-index', String(startIndex + offset));
itemsContainer.appendChild(card);
cards.push(card);
@@ -7017,7 +7046,7 @@
if (id && !cardById.has(id)) cardById.set(id, card);
});
const freshIds = items.map((item) => item?.Id).filter(Boolean);
const freshIds = sectionConfig?.useParentCard ? items.map((item) => item?.ParentId || item?.SeriesId || item?.Id).filter(Boolean) : items.map((item) => item?.Id).filter(Boolean);
const freshSet = new Set(freshIds);
const outgoing = paintedCards.filter((card) => !freshSet.has(card.getAttribute('data-id')));
const inserted = [];
@@ -7038,7 +7067,7 @@
return;
}
const card = createJellyfinCardElement(item, overflowCard, cardFormat, item.cardFooter);
const card = createJellyfinCardElement(item, overflowCard, cardFormat, item.cardFooter, !!sectionConfig?.useParentCard);
if (nextNode) itemsContainer.insertBefore(card, nextNode);
else itemsContainer.appendChild(card);
inserted.push(card);
@@ -7146,8 +7175,9 @@
fragment.appendChild(button);
});
} else {
const useParentCard = !!sectionConfig?.useParentCard;
items.forEach((item, index) => {
const card = createJellyfinCardElement(item, overflowCard, cardFormat, item.cardFooter);
const card = createJellyfinCardElement(item, overflowCard, cardFormat, item.cardFooter, useParentCard);
card.setAttribute('data-index', String(index));
fragment.appendChild(card);
});
@@ -7256,6 +7286,72 @@
}
}
function getDismissEmptySectionTimerMs() {
const raw = window.KefinTweaksConfig?.homeScreenConfig?.HOME_SETTINGS?.dismissEmptySectionTimer
?? window.KefinHomeConfig2?.HOME_SETTINGS?.dismissEmptySectionTimer
?? 0;
const n = Number(raw);
return Number.isFinite(n) && n > 0 ? Math.floor(n) : 0;
}
/**
* Show "No items found" at locked section height, then fade and remove.
* Used when progressive enhance resolves to an empty item list and the timer is > 0.
*/
function dismissEmptyProgressiveSection(sectionElement, { holdMs = 1000 } = {}) {
if (!sectionElement || !sectionElement.isConnected) return;
if (sectionElement.dataset.emptyDismissing === 'true') return;
sectionElement.dataset.emptyDismissing = 'true';
const lockedHeight = Math.max(sectionElement.offsetHeight, 0);
if (lockedHeight > 0) {
sectionElement.style.minHeight = `${lockedHeight}px`;
}
const isSpotlight = sectionElement.classList.contains('spotlight-section')
|| !!sectionElement.querySelector('.spotlight-items-container, .skeleton-spotlight-item');
const message = document.createElement('div');
message.className = 'cardbuilder-empty-message';
message.textContent = 'No items found';
if (isSpotlight) {
const itemsContainer = sectionElement.querySelector('.spotlight-items-container');
if (itemsContainer) {
itemsContainer.innerHTML = '';
itemsContainer.appendChild(message);
} else {
const body = sectionElement.querySelector('.spotlight-banner-container') || sectionElement;
Array.from(body.children).forEach((child) => {
if (!child.classList.contains('spotlight-section-title-container')
&& !child.classList.contains('sectionTitleContainer')) {
child.remove();
}
});
body.appendChild(message);
}
} else {
const itemsContainer = sectionElement.querySelector('.itemsContainer');
if (itemsContainer) {
itemsContainer.innerHTML = '';
itemsContainer.appendChild(message);
} else {
sectionElement.appendChild(message);
}
}
const hold = Math.max(0, Number(holdMs) || 0);
setTimeout(() => {
if (!sectionElement.isConnected) return;
sectionElement.classList.add('cardbuilder-items-fade');
sectionElement.offsetHeight;
sectionElement.classList.add('is-hidden');
runAfterTransition(sectionElement, 320, () => {
sectionElement.remove();
});
}, hold);
}
function hasSectionDeferredData(result) {
if (!result) return false;
if (typeof result.ensureData === 'function') return true;
@@ -7300,7 +7396,12 @@
const hasSkeletonTitle = !!sectionElement.querySelector('.skeleton-section-title');
if (items.length === 0) {
sectionElement.remove();
const holdMs = getDismissEmptySectionTimerMs();
if (holdMs > 0) {
dismissEmptyProgressiveSection(sectionElement, { holdMs });
} else {
sectionElement.remove();
}
return;
}
@@ -7320,7 +7421,7 @@
: Array.from(sectionElement.querySelectorAll('.spotlight-item[data-id]:not(.skeleton-spotlight-item)'))
.map((el) => el.getAttribute('data-id'))
.filter(Boolean);
const freshIds = items.map((item) => item.Id).filter(Boolean);
const freshIds = sectionConfig?.useParentCard ? items.map((item) => item?.ParentId || item?.SeriesId || item?.Id).filter(Boolean) : items.map((item) => item?.Id).filter(Boolean);
const match = classifyIdSequence(paintedIds, freshIds);
if (match.type === 'perfect') {
markSectionEnhanced(sectionElement, sectionConfig);
@@ -7334,7 +7435,7 @@
if (!isSkeleton && !isSpotlight) {
const paintedIds = getPaintedCardIds(sectionElement);
const freshIds = items.map((item) => item.Id).filter(Boolean);
const freshIds = sectionConfig?.useParentCard ? items.map((item) => item?.ParentId || item?.SeriesId || item?.Id).filter(Boolean) : items.map((item) => item?.Id).filter(Boolean);
const match = classifyIdSequence(paintedIds, freshIds);
if (match.type === 'perfect') {