diff --git a/configuration/homeScreen-configuration.js b/configuration/homeScreen-configuration.js index df4f123..f422547 100644 --- a/configuration/homeScreen-configuration.js +++ b/configuration/homeScreen-configuration.js @@ -2237,6 +2237,12 @@
${hscToggleCard('home-ensureThumbsForPopularTVNetworks', homeSettings.ensureThumbsForPopularTVNetworks === true, 'Require Studio Thumbs', D)}
+
+ ${buildTextInput('home-dismissEmptySectionTimer', homeSettings.dismissEmptySectionTimer ?? 0, 'Empty Section Dismiss Timer (ms)', 'number')} +
+ When a section loads with no items: 0 removes it immediately. Any other value shows “No items found” for that many milliseconds, then fades the section out. +
+
Top People
@@ -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), diff --git a/scripts/cardBuilder.js b/scripts/cardBuilder.js index 0981965..b601023 100644 --- a/scripts/cardBuilder.js +++ b/scripts/cardBuilder.js @@ -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') {