From 1fb8733981d5e3eaccb056fdb7e4b2ffd21a6ba9 Mon Sep 17 00:00:00 2001 From: HighImKevin Date: Sun, 7 Dec 2025 05:50:19 -0500 Subject: [PATCH] Add Recently Added in Library sections to Home Screen configuration --- kefinTweaks-default-config.js | 1 + kefinTweaks.minimal.js | 1 + scripts/configuration.js | 119 ++++++++++++++++++++++++++++--- scripts/homeScreen.js | 129 ++++++++++++++++++++++++++++++++++ 4 files changed, 242 insertions(+), 8 deletions(-) diff --git a/kefinTweaks-default-config.js b/kefinTweaks-default-config.js index f9defc3..9b00528 100644 --- a/kefinTweaks-default-config.js +++ b/kefinTweaks-default-config.js @@ -23,6 +23,7 @@ window.KefinTweaksDefaultConfig = { "defaultItemLimit": 16, "defaultSortOrder": "Random", "defaultCardFormat": "Poster", + "recentlyAddedInLibrary": {}, "recentlyReleased": { "enabled": true, "order": 30, diff --git a/kefinTweaks.minimal.js b/kefinTweaks.minimal.js index 53dc9f3..5f8ca4b 100644 --- a/kefinTweaks.minimal.js +++ b/kefinTweaks.minimal.js @@ -23,6 +23,7 @@ window.KefinTweaksConfig = { "defaultItemLimit": 16, "defaultSortOrder": "Random", "defaultCardFormat": "Poster", + "recentlyAddedInLibrary": {}, "recentlyReleased": { "enabled": true, "order": 30, diff --git a/scripts/configuration.js b/scripts/configuration.js index 8442798..9a3e504 100644 --- a/scripts/configuration.js +++ b/scripts/configuration.js @@ -432,7 +432,7 @@ // Build the configuration page content - function buildConfigPageContent(config) { + function buildConfigPageContent(config, libraries = []) { const scripts = config.scripts || {}; const homeScreen = config.homeScreen || {}; const exclusiveElsewhere = config.exclusiveElsewhere || {}; @@ -509,7 +509,7 @@
- ${buildHomeScreenConfig(homeScreen)} + ${buildHomeScreenConfig(homeScreen, libraries)}
@@ -1487,11 +1487,11 @@ } = options; const nameValue = config.name ?? defaultName ?? ''; - const itemLimit = config.itemLimit ?? 16; + const itemLimit = config.itemLimit ?? options.defaultItemLimit ?? 16; const sortOrder = config.sortOrder ?? 'Random'; const sortOrderDirection = config.sortOrderDirection ?? 'Ascending'; - const cardFormat = config.cardFormat ?? 'Poster'; - const order = config.order ?? 100; + const cardFormat = config.cardFormat ?? options.defaultCardFormat ?? 'Poster'; + const order = config.order ?? options.defaultOrder ?? 100; // IsPlayed logic // If isPlayed is undefined or null, it means the filter is disabled. @@ -1638,8 +1638,9 @@ } // Build home screen configuration - function buildHomeScreenConfig(homeScreen) { + function buildHomeScreenConfig(homeScreen, libraries = []) { const recentlyReleased = homeScreen.recentlyReleased || {}; + const recentlyAddedInLibrary = homeScreen.recentlyAddedInLibrary || {}; const trending = homeScreen.trending || {}; const popularTVNetworks = homeScreen.popularTVNetworks || {}; const watchlist = homeScreen.watchlist || {}; @@ -1707,7 +1708,63 @@ `; }).join(''); + // Build library sections HTML for "Recently Added in Library" + let recentlyAddedInLibraryHtml = ''; + if (libraries && libraries.length > 0) { + recentlyAddedInLibraryHtml = libraries.map(library => { + const libConfig = recentlyAddedInLibrary[library.Id] || {}; + const prefix = `homeScreen_recentlyAddedInLibrary_${library.Id}`; + const defaultName = `Recently Added in ${library.Name}`; + + return ` +
+ +
+ chevron_right + ${library.Name} + (${libConfig.enabled !== false ? 'Enabled' : 'Disabled'}, Order: ${libConfig.order || 11}) +
+
+
+
+ ${buildJellyfinCheckbox(`${prefix}_enabled`, libConfig.enabled !== false, 'Enabled', { 'data-library-id': library.Id, 'class': 'recently-added-library-enabled' })} +
+
+ ${createSectionConfiguration(prefix, libConfig, { + includeName: true, + defaultName: defaultName, + includeSortOrder: false, + includeOrder: true, + defaultOrder: 11, + includeItemLimit: true, + defaultItemLimit: 30, + defaultCardFormat: library.CollectionType === 'tvshows' ? 'Thumb' : 'Poster' + })} +
+
+
+ `; + }).join(''); + } + return ` + + ${libraries && libraries.length > 0 ? ` +
+ + chevron_right + Recently Added in Library Sections + +
+
+
+ ${recentlyAddedInLibraryHtml} +
+
+
+
+ ` : ''} +
@@ -2036,6 +2093,29 @@ } // Open configuration modal + // Fetch libraries (Movies and TV) + async function fetchLibraries() { + if (!window.ApiClient) return []; + try { + // Get user views (libraries) + const result = await window.ApiClient.getUserViews({}, window.ApiClient.getCurrentUserId()); + if (result && result.Items) { + // Filter for movies and tvshows + return result.Items.filter(item => + item.CollectionType === 'movies' || item.CollectionType === 'tvshows' + ).map(item => ({ + Id: item.Id, + Name: item.Name, + CollectionType: item.CollectionType + })); + } + return []; + } catch (error) { + console.error('[KefinTweaks Configuration] Error fetching libraries:', error); + return []; + } + } + async function openConfigurationModal() { console.log('[KefinTweaks Configuration] Opening configuration modal...'); @@ -2055,13 +2135,16 @@ currentLoadedConfig = config; window.KefinTweaksCurrentConfig = config; + // Fetch libraries for configuration + const libraries = await fetchLibraries(); + // Build the content (without title, as ModalSystem adds it) const content = document.createElement('div'); content.className = 'modal-content-wrapper'; content.id = MODAL_ID; content.innerHTML = `
- ${buildConfigPageContent(config)} + ${buildConfigPageContent(config, libraries)}
`; @@ -5111,7 +5194,27 @@ window.KefinTweaksConfig = ${JSON.stringify(config, null, 2)};`; ...getSectionConfig('homeScreen_recentlyReleased_episodes', { includeName: true, defaultName: 'Recently Aired Episodes', includeSortOrder: true, includeOrder: true, includeIsPlayed: true, includePremiereDays: true }) } }, - trending: { + recentlyAddedInLibrary: (() => { + const libraryConfigs = {}; + const libraryCheckboxes = modalInstance.dialogContent.querySelectorAll('.recently-added-library-enabled'); + libraryCheckboxes.forEach(checkbox => { + const libraryId = checkbox.getAttribute('data-library-id'); + if (libraryId) { + const prefix = `homeScreen_recentlyAddedInLibrary_${libraryId}`; + libraryConfigs[libraryId] = { + enabled: checkbox.checked !== false, + ...getSectionConfig(prefix, { + includeName: true, + defaultName: '', + includeSortOrder: false, + includeOrder: true + }) + }; + } + }); + return libraryConfigs; + })(), + trending: { enabled: document.getElementById('homeScreen_trending_enabled')?.checked === true, ...getSectionConfig('homeScreen_trending', { includeName: true, defaultName: 'Trending' }) }, diff --git a/scripts/homeScreen.js b/scripts/homeScreen.js index c2bfb95..77b22e7 100644 --- a/scripts/homeScreen.js +++ b/scripts/homeScreen.js @@ -777,6 +777,9 @@ // Custom home sections configuration const customHomeSections = homeScreenConfig.customSections || []; + // Recently Added in Library configuration + const recentlyAddedInLibraryConfig = homeScreenConfig.recentlyAddedInLibrary || {}; + // Recently Released sections configuration const recentlyReleasedConfig = homeScreenConfig.recentlyReleased || {}; const enableNewAndTrending = recentlyReleasedConfig.enabled !== false; @@ -1429,6 +1432,127 @@ LOG(`Rendered ${successCount}/${sectionsToRender.length} new and trending sections`); } + /** + * Renders Recently Added in Library sections + * @param {HTMLElement} container - Container to append sections to + */ + async function renderAllRecentlyAddedInLibrarySections(container) { + if (!recentlyAddedInLibraryConfig || Object.keys(recentlyAddedInLibraryConfig).length === 0) { + return; + } + + const libraryIds = Object.keys(recentlyAddedInLibraryConfig); + const sectionsToRender = []; + + libraryIds.forEach(libraryId => { + const config = recentlyAddedInLibraryConfig[libraryId]; + if (config && config.enabled !== false) { + // Check if already rendered + const sectionId = `recently-added-${libraryId}`; + if (!container.querySelector(`[data-custom-section-id="${sectionId}"]`)) { + sectionsToRender.push(renderRecentlyAddedInLibrarySection(libraryId, config, container)); + } + } + }); + + if (sectionsToRender.length === 0) { + return; + } + + const results = await Promise.all(sectionsToRender); + const successCount = results.filter(Boolean).length; + LOG(`Rendered ${successCount}/${sectionsToRender.length} recently added library sections`); + } + + /** + * Renders a single Recently Added in Library section + * @param {string} libraryId - The library ID + * @param {Object} sectionConfig - Configuration for this section + * @param {HTMLElement} container - Container to append section to + */ + async function renderRecentlyAddedInLibrarySection(libraryId, sectionConfig, container) { + if (sectionConfig.enabled === false) return false; + + const userId = ApiClient.getCurrentUserId(); + const itemLimit = sectionConfig.itemLimit || 30; + + // Determine IncludeItemTypes and viewMoreUrl based on library type + let includeItemTypes = 'Movie,Episode'; // Default fallback + let viewMoreUrl = `#/list.html?parentId=${libraryId}&serverId=${ApiClient.serverId()}`; // Default fallback + + try { + const library = await window.ApiClient.getItem(userId, libraryId); + if (library) { + if (library.CollectionType === 'movies') { + includeItemTypes = 'Movie'; + viewMoreUrl = `#/movies.html?topParentId=${libraryId}&collectionType=movies&tab=1`; + } else if (library.CollectionType === 'tvshows') { + includeItemTypes = 'Episode'; + viewMoreUrl = `#/tv.html?topParentId=${libraryId}&collectionType=tvshows&tab=1`; + } + } + } catch (e) { + WARN(`Failed to fetch library info for ${libraryId}`, e); + } + + // Use Users/Items/Latest endpoint logic + // Since apiHelper might not have a dedicated getLatestItems, we construct query + const params = { + Limit: itemLimit, + ParentId: libraryId, + ImageTypeLimit: 1, + EnableImageTypes: 'Primary,Backdrop,Thumb', + IncludeItemTypes: includeItemTypes + }; + + const queryParams = Object.entries(params) + .map(([key, value]) => `${key}=${encodeURIComponent(value)}`) + .join('&'); + + const url = `${ApiClient.serverAddress()}/Users/${userId}/Items/Latest?${queryParams}`; + + try { + const response = await fetch(url, { + headers: { + 'Authorization': `MediaBrowser Token="${ApiClient.accessToken()}"` + } + }); + + if (!response.ok) throw new Error(`Fetch failed: ${response.status}`); + const items = await response.json(); + + if (!items || items.length === 0) return false; + + // Render section + if (!window.cardBuilder || !window.cardBuilder.renderCards) { + WARN("cardBuilder.renderCards not available"); + return false; + } + + const cardContainer = window.cardBuilder.renderCards( + items, + sectionConfig.name || `Recently Added`, + viewMoreUrl, + true, // overflowCard (use standard overflow style for horizontal scroll) + sectionConfig.cardFormat || 'Poster', + false, // sortOrder (already sorted) + 'Ascending' + ); + + const sectionId = `recently-added-${libraryId}`; + cardContainer.setAttribute('data-custom-section-id', sectionId); + cardContainer.setAttribute('data-custom-section-name', sectionConfig.name || `Recently Added`); + cardContainer.style.order = sectionConfig.order || 11; + + container.appendChild(cardContainer); + return true; + + } catch (err) { + ERR(`Error rendering recently added library section ${libraryId}:`, err); + return false; + } + } + /************ Seasonal Section Helpers ************/ /** @@ -5810,6 +5934,11 @@ initPromises.push(renderAllCustomSections(homeSectionsContainer)); } + // Add Recently Added in Library sections + if (recentlyAddedInLibraryConfig && Object.keys(recentlyAddedInLibraryConfig).length > 0) { + initPromises.push(renderAllRecentlyAddedInLibrarySections(homeSectionsContainer)); + } + // Add new and trending sections if enabled if (enableNewAndTrending) { initPromises.push(renderAllNewAndTrendingSections(homeSectionsContainer));