// KefinTweaks User Home Screen Configuration // Allows users to customize their home screen by re-ordering and enabling/disabling sections (function() { 'use strict'; const LOG = (...args) => console.log('[KefinTweaks User HomeScreen Config]', ...args); const WARN = (...args) => console.warn('[KefinTweaks User HomeScreen Config]', ...args); const ERR = (...args) => console.error('[KefinTweaks User HomeScreen Config]', ...args); // All Jellyfin home section base IDs (homesection0-8) const JELLYFIN_HOME_SECTIONS = [ 'smalllibrarytiles', 'librarybuttons', 'resume', 'resumeaudio', 'resumebook', 'nextup', 'latestmedia', 'livetv', 'activerecordings' ]; // Map Jellyfin sections to their KefinTweaks equivalents // When a KefinTweaks section exists, it replaces the Jellyfin section in the UI const JELLYFIN_HOME_SECTIONS_MAP = { 'resume': 'continueWatching', 'nextup': 'nextUp', '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 */ function flattenSectionGroups(groups) { if (!Array.isArray(groups)) return []; const flattened = []; groups.forEach(group => { if (group && Array.isArray(group.sections)) { flattened.push(...group.sections); } }); return flattened; } /** * Get Jellyfin default sections from display preferences */ function getJellyfinSectionsFromPrefs(displayPrefs) { const sections = []; const customPrefs = displayPrefs?.CustomPrefs || {}; // Build a set of all base IDs that appear in any homesectionN field (0-8) const enabledBaseIds = new Set(); for (let i = 0; i <= 8; i++) { const homesectionValue = customPrefs[`homesection${i}`]; if (homesectionValue && homesectionValue !== '' && homesectionValue !== 'none') { enabledBaseIds.add(homesectionValue); } } // Parse kefinHomeScreen if it's a string (Jellyfin stores CustomPrefs as strings) let kefinHomeScreen = []; if (customPrefs.kefinHomeScreen) { if (typeof customPrefs.kefinHomeScreen === 'string') { try { kefinHomeScreen = JSON.parse(customPrefs.kefinHomeScreen); } catch (e) { // Ignore parse errors, use empty array } } else if (Array.isArray(customPrefs.kefinHomeScreen)) { kefinHomeScreen = customPrefs.kefinHomeScreen; } } // Process each Jellyfin section JELLYFIN_HOME_SECTIONS.forEach((baseId, index) => { const homesectionKey = `homesection${index}`; // Section is enabled if its base ID appears in ANY homesectionN field (0-8) const isEnabled = enabledBaseIds.has(baseId); // Get order and enabled status from kefinHomeScreen if available let order = 0; let enabledFromPrefs = isEnabled; // Default to homesectionN value if (Array.isArray(kefinHomeScreen) && kefinHomeScreen.length > 0) { // Look for section by base ID (new format) or homesectionKey (old format for backward compatibility) const sectionPref = kefinHomeScreen.find(s => s.id === baseId || s.id === homesectionKey ); if (sectionPref) { if (sectionPref.order !== undefined) { order = sectionPref.order; } if (sectionPref.enabled !== undefined) { enabledFromPrefs = sectionPref.enabled; } } } sections.push({ id: baseId, // Use base ID directly, no -jellyfin suffix name: getJellyfinSectionName(baseId), enabled: enabledFromPrefs, order: order, hidden: false, isJellyfin: true, jellyfinBaseId: baseId, // Track the Jellyfin base ID homesectionKey: homesectionKey // Keep for backward compatibility }); }); return sections; } /** * Get display name for Jellyfin section */ function getJellyfinSectionName(baseId) { const names = { 'smalllibrarytiles': 'My Media', 'librarybuttons': 'My Media (small)', 'resume': 'Resume', 'resumeaudio': 'Resume Audio', 'resumebook': 'Resume Book', 'nextup': 'Next Up', 'latestmedia': 'Latest Media', 'livetv': 'Live TV', 'activerecordings': 'Active Recordings' }; return names[baseId] || baseId; } /** * Load user preferences from display preferences * @param {Object} displayPrefs - Optional display preferences object to use instead of fetching */ async function loadUserPreferences(displayPrefs = null) { try { if (!displayPrefs) { if (!window.userHelper || !window.userHelper.getUserDisplayPreferences) { WARN('userHelper not available'); return { kefinHomeScreen: [], homesections: {} }; } const { promise } = await window.userHelper.getUserDisplayPreferences(); displayPrefs = await promise; } if (!displayPrefs || !displayPrefs.CustomPrefs) { return { kefinHomeScreen: [], homesections: {} }; } const customPrefs = displayPrefs.CustomPrefs; let kefinHomeScreen = []; // kefinHomeScreen is stored as a JSON string in CustomPrefs if (customPrefs.kefinHomeScreen) { if (typeof customPrefs.kefinHomeScreen === 'string') { try { kefinHomeScreen = JSON.parse(customPrefs.kefinHomeScreen); } catch (e) { WARN('Failed to parse kefinHomeScreen:', e); kefinHomeScreen = []; } } else if (Array.isArray(customPrefs.kefinHomeScreen)) { // Handle legacy format (direct array) kefinHomeScreen = customPrefs.kefinHomeScreen; } } // Extract homesectionN values const homesections = {}; for (let i = 0; i <= 8; i++) { const key = `homesection${i}`; homesections[key] = customPrefs[key] || 'none'; } return { kefinHomeScreen: Array.isArray(kefinHomeScreen) ? kefinHomeScreen : [], homesections: homesections }; } catch (error) { ERR('Error loading user preferences:', error); return { kefinHomeScreen: [], homesections: {} }; } } /** * Merge user preferences with sections * @param {Array} sections - Sections from config * @param {Object} userPrefs - User preferences object * @param {Object} mergedConfig - Merged config from getConfig() for fallback defaults * @param {Object} displayPrefs - Display preferences object to check homesectionN fields */ function mergeUserPreferences(sections, userPrefs, mergedConfig = null, displayPrefs = null) { const kefinHomeScreen = userPrefs.kefinHomeScreen || []; const customPrefs = displayPrefs?.CustomPrefs || {}; // Create maps of section-id -> order and enabled status const orderMap = new Map(); const enabledMap = new Map(); kefinHomeScreen.forEach(pref => { if (pref.id) { if (pref.order !== undefined) { orderMap.set(pref.id, pref.order); } if (pref.enabled !== undefined) { enabledMap.set(pref.id, pref.enabled); } } }); // Build set of base IDs that appear in ANY homesectionN field (0-8) for Jellyfin sections const enabledJellyfinBaseIds = new Set(); for (let i = 0; i <= 8; i++) { const homesectionValue = customPrefs[`homesection${i}`]; if (homesectionValue && homesectionValue !== '' && homesectionValue !== 'none') { enabledJellyfinBaseIds.add(homesectionValue); } } // Helper to get default enabled status from config const getDefaultEnabled = (sectionId) => { if (!mergedConfig) return true; // Default to enabled if no config // Search through all section groups const allGroups = [ ...(mergedConfig.HOME_SECTION_GROUPS || []), ...(mergedConfig.SEASONAL_SECTION_GROUPS || []), ...(mergedConfig.DISCOVERY_SECTION_GROUPS || []), ...(mergedConfig.CUSTOM_SECTION_GROUPS || []) ]; for (const group of allGroups) { if (group.sections) { const section = group.sections.find(s => s.id === sectionId); if (section) { return section.enabled !== false; } } } return true; // Default to enabled if not found }; // Helper to check if a section ID is a Jellyfin section const isJellyfinSection = (sectionId) => { return JELLYFIN_HOME_SECTIONS.includes(sectionId); }; // Helper to get the Jellyfin base ID for a KefinTweaks section that maps to Jellyfin const getMappedJellyfinId = (sectionId) => { // Check if this KefinTweaks section maps to a Jellyfin section for (const [jellyfinId, kefinId] of Object.entries(JELLYFIN_HOME_SECTIONS_MAP)) { if (jellyfinId === 'latestmedia' && sectionId.startsWith('recently-added-')) { return jellyfinId; } if (kefinId === sectionId) { return jellyfinId; } } return null; }; // Update sections with user preferences return sections.map(section => { const sectionId = section.id; let isEnabled; // For Jellyfin sections, check if base ID appears in any homesectionN field if (section.isJellyfin || isJellyfinSection(sectionId)) { const baseId = section.jellyfinBaseId || sectionId; isEnabled = enabledJellyfinBaseIds.has(baseId); } else { // For KefinTweaks sections, check if user has explicit enabled/disabled preference, otherwise use default from config // But also check homesectionN if this section maps to a Jellyfin section const mappedJellyfinId = getMappedJellyfinId(sectionId); if (mappedJellyfinId && enabledJellyfinBaseIds.has(mappedJellyfinId)) { // If mapped Jellyfin section is enabled in homesectionN, use that isEnabled = true; } else { const hasUserEnabledPref = enabledMap.has(sectionId); isEnabled = hasUserEnabledPref ? enabledMap.get(sectionId) : getDefaultEnabled(sectionId); } } const userOrder = orderMap.get(sectionId); return { ...section, enabled: isEnabled, order: userOrder !== undefined ? userOrder : (section.order || 0) }; }); } /** * Save user preferences to display preferences */ async function saveUserPreferences(sections) { try { if (!window.userHelper || !window.userHelper.getUserDisplayPreferences || !window.userHelper.updateDisplayPreferences) { ERR('userHelper not available'); return false; } // Get current display preferences const { promise } = await window.userHelper.getUserDisplayPreferences(); const displayPrefs = await promise; if (!displayPrefs) { ERR('Could not load display preferences'); return false; } if (!displayPrefs.CustomPrefs) { displayPrefs.CustomPrefs = {}; } const customPrefs = displayPrefs.CustomPrefs; // Helper to check if a section ID is a Jellyfin section const isJellyfinSection = (sectionId) => { if (JELLYFIN_HOME_SECTIONS.includes(sectionId)) { return true; } return getMappedJellyfinId(sectionId) !== null; }; // Helper to get the Jellyfin base ID for a KefinTweaks section that maps to Jellyfin const getMappedJellyfinId = (sectionId) => { for (const [jellyfinId, kefinId] of Object.entries(JELLYFIN_HOME_SECTIONS_MAP)) { if (jellyfinId === 'latestmedia' && sectionId.startsWith('recently-added-')) { return jellyfinId; } if (kefinId === sectionId) { return jellyfinId; } } return null; }; // Build kefinHomeScreen array with all sections (including enabled status) // Use section IDs directly, no homesectionKey const kefinHomeScreen = sections.map(section => ({ id: section.id, order: section.order || 0, enabled: section.enabled !== false })); // Jellyfin expects CustomPrefs values to be JSON strings customPrefs.kefinHomeScreen = JSON.stringify(kefinHomeScreen); // Update homesectionN properties for Jellyfin sections // First, clear all homesectionN (0-9) for (let i = 0; i <= 9; i++) { customPrefs[`homesection${i}`] = 'none'; } // Collect sections that should be saved to homesectionN fields const sectionsForHomesectionN = []; // Add non-mapped Jellyfin sections sections.forEach(section => { if (section.enabled === true && (section.isJellyfin || isJellyfinSection(section.id) || section.mapsToJellyfin)) { const baseId = section.mapsToJellyfin || section.jellyfinBaseId || section.id; sectionsForHomesectionN.push({ jellyfinBaseId: baseId, order: section.order || 0 }); } if (section.enabled === true && section.id === 'continueWatchingAndNextUp') { sectionsForHomesectionN.push({ jellyfinBaseId: 'resume', order: section.order || 0 }); sectionsForHomesectionN.push({ jellyfinBaseId: 'nextup', order: section.order || 0 }); } }); // Ensure each section has a unique jellyfinBaseId const uniqueHomeSections = sectionsForHomesectionN.filter((section, index, self) => index === self.findIndex(s => s.jellyfinBaseId === section.jellyfinBaseId) ); // Sort all sections by order uniqueHomeSections.sort((a, b) => (a.order || 0) - (b.order || 0)); // Assign each section to homesectionN sequentially for (let i = 0; i <= 9; i++) { const selectHomeSection = document.querySelector(`.homeScreenSettingsContainer #selectHomeSection${i+1}`); if (i <= uniqueHomeSections.length - 1) { const selectedSection = uniqueHomeSections[i].jellyfinBaseId; customPrefs[`homesection${i}`] = selectedSection; const selectedOption = selectHomeSection.querySelector(`option[value="${selectedSection}"]`); if (selectedOption) { selectHomeSection.value = selectedOption.value; } else { selectHomeSection.value = ''; } } else { customPrefs[`homesection${i}`] = 'none'; // Find the value of the "None" option label, some have value="none" and some have no value at all const noneOption = selectHomeSection.querySelector('option[value="none"]'); if (noneOption) { selectHomeSection.value = noneOption.value; } else { selectHomeSection.value = ''; } } } // Save const success = await window.userHelper.updateDisplayPreferences(displayPrefs); if (success) { LOG('User preferences saved successfully'); } else { ERR('Failed to save user preferences'); } return success; } catch (error) { ERR('Error saving user preferences:', error); return false; } } /** * Render user home sections editor */ async function renderUserHomeSectionsEditor(container) { if (!container) { ERR('Container not provided'); return; } try { // Check dependencies if (!window.KefinHomeScreen || !window.KefinHomeScreen.getConfig) { ERR('KefinHomeScreen.getConfig not available'); return; } if (!window.KefinTweaksUI || !window.KefinTweaksUI.renderHomeSectionsOrderEditor) { ERR('KefinTweaksUI.renderHomeSectionsOrderEditor not available'); return; } // Get merged config const mergedConfig = await window.KefinHomeScreen.getConfig(); // Flatten all section groups const allGroups = [ ...(mergedConfig.HOME_SECTION_GROUPS || []), ...(mergedConfig.SEASONAL_SECTION_GROUPS || []), /* ...(mergedConfig.DISCOVERY_SECTION_GROUPS || []), */ ...(mergedConfig.CUSTOM_SECTION_GROUPS || []) ]; let kefinSections = flattenSectionGroups(allGroups); // 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); // Filter out discovery sections kefinSections = kefinSections.filter(s => s.type !== 'discovery'); // Filter out discovery sections kefinSections = kefinSections.filter(s => s.discoveryEnabled !== true); // Get display preferences once if (!window.userHelper || !window.userHelper.getUserDisplayPreferences) { ERR('userHelper not available'); container.innerHTML = '
User helper not available.
'; return; } const { promise } = await window.userHelper.getUserDisplayPreferences(); const displayPrefs = await promise; // Load user preferences using the fetched displayPrefs const userPrefs = await loadUserPreferences(displayPrefs); // Get Jellyfin sections from display preferences let jellyfinSections = getJellyfinSectionsFromPrefs(displayPrefs); // Mark KefinTweaks sections that map to Jellyfin sections kefinSections = kefinSections.map(section => { const mappedJellyfinId = Object.entries(JELLYFIN_HOME_SECTIONS_MAP).find(([jellyfinId, kefinId]) => { if (jellyfinId === 'latestmedia' && section.id && section.id.startsWith('recently-added-')) { return true; } return kefinId === section.id; }); if (mappedJellyfinId) { return { ...section, mapsToJellyfin: mappedJellyfinId[0] // Store the Jellyfin base ID }; } return section; }); // Filter out mapped Jellyfin sections if their KefinTweaks equivalent exists const kefinSectionIds = new Set(kefinSections.map(s => s.id)); 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); const isKefinRecentlyAddedEnabled = adminKefinSections.some(s => s.id && s.id.startsWith('recently-added-') && s.enabled === true); jellyfinSections = jellyfinSections.filter(jellyfinSection => { const baseId = jellyfinSection.jellyfinBaseId || jellyfinSection.id; const mappedKefinId = JELLYFIN_HOME_SECTIONS_MAP[baseId]; // For latestmedia, check if any recently-added-* section exists if (baseId === 'latestmedia') { return !isKefinRecentlyAddedEnabled; // Remove if any recently-added exists } if (baseId === 'nextup') { return !isKefinNextUpEnabled && !isKefinMergeNextUpEnabled; } if (baseId === 'resume') { return !isKefinContinueWatchingEnabled && !isKefinMergeNextUpEnabled; } // For other mapped sections, check if the mapped KefinTweaks section exists if (mappedKefinId) { return !kefinSectionIds.has(mappedKefinId); // Remove if mapped Kefin section exists } return true; // Keep non-mapped sections }); // Combine all sections let allSections = [...kefinSections, ...jellyfinSections]; // Merge user preferences (pass mergedConfig for fallback defaults and displayPrefs for homesectionN checks) allSections = mergeUserPreferences(allSections, userPrefs, mergedConfig, displayPrefs); // Sort by order allSections.sort((a, b) => (a.order || 0) - (b.order || 0)); // Set any section types that aren't "seasonal" to "home" allSections.forEach(section => { if (section.type && section.type.toLowerCase() !== 'seasonal') { section.type = 'home'; } }); // Create editor HTML const editorHTML = window.KefinTweaksUI.renderHomeSectionsOrderEditor(allSections, { showUpDownButtons: true }); container.innerHTML = `

Home Sections Order

${editorHTML}
`; // 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 = '

Restoring the default Home Screen Settings will override your existing settings. Are you sure you want to proceed?

'; const footer = ` `; window.ModalSystem.create({ id: modalId, title: 'Restore Defaults', content: content, footer: footer, closeOnBackdrop: true, closeOnEscape: true, showCloseButton: true, onOpen: (modal) => { const confirmBtn = modal.dialogFooter && modal.dialogFooter.querySelector('#kefin-restore-defaults-confirm'); if (confirmBtn) { confirmBtn.addEventListener('click', async () => { try { if (!window.userHelper || !window.userHelper.getUserDisplayPreferences || !window.userHelper.updateDisplayPreferences) { WARN('userHelper not available'); return; } const { promise } = await window.userHelper.getUserDisplayPreferences(); const displayPrefs = await promise; if (!displayPrefs) { WARN('Could not load display preferences'); return; } if (!displayPrefs.CustomPrefs) { displayPrefs.CustomPrefs = {}; } displayPrefs.CustomPrefs.kefinHomeScreen = '[]'; const ok = await window.userHelper.updateDisplayPreferences(displayPrefs); if (ok) { window.ModalSystem.close(modalId); await renderUserHomeSectionsEditor(container); if (window.KefinTweaksToaster && window.KefinTweaksToaster.toast) { window.KefinTweaksToaster.toast('Home Screen settings restored to defaults.'); } } else { WARN('Failed to restore defaults'); } } catch (e) { ERR('Error restoring defaults:', e); } }); } } }); }); } // Setup event listeners // Find the sections-container which wraps both enabled and disabled lists const editorContainer = container.querySelector('.sections-container') || container.querySelector('.group-sections-list'); if (editorContainer && window.KefinTweaksUI.setupOrderEditorListeners) { // Track current section state let currentSections = [...allSections]; let isSaving = false; // Extract save logic into reusable function const performSave = async () => { if (isSaving) return; // Prevent concurrent saves isSaving = true; try { // Read DOM order and update sections array // Only read order from enabled sections list (disabled sections preserve their order) const enabledList = container.querySelector('.enabled-sections-list') || editorContainer.querySelector('.enabled-sections-list'); const sectionIdToOrder = new Map(); if (enabledList) { // Get all section rows in enabled list in their current DOM order const sectionRows = Array.from(enabledList.querySelectorAll('.section-row')); // Assign order values starting from 1, incrementing by 1 for each section in DOM order sectionRows.forEach((row, index) => { const sectionId = row.dataset.sectionId; if (sectionId) { sectionIdToOrder.set(sectionId, index + 1); } }); } // Update order values in currentSections based on DOM order (only for enabled sections) currentSections.forEach(section => { const domOrder = sectionIdToOrder.get(section.id); if (domOrder !== undefined) { // Only update order if section is in enabled list section.order = domOrder; } // Disabled sections keep their existing order value (not updated) }); await saveUserPreferences(currentSections); } finally { isSaving = false; } }; const handleToggleChange = async (sectionId, enabled) => { const section = currentSections.find(s => (s.id || '') === sectionId); if (section) { section.enabled = enabled; } // Auto-save when toggle changes await performSave(); }; // Setup listeners with auto-save on order changes window.KefinTweaksUI.setupOrderEditorListeners(editorContainer, { onOrderChange: null, // Don't update order values during drag/drop, we'll read from DOM onToggleChange: handleToggleChange, getSectionOrder: null, // Not needed since we're not calculating orders sections: currentSections, onSave: performSave // Pass save function to be called on drag/drop and up/down }); } LOG('User home sections editor rendered'); } catch (error) { ERR('Error rendering user home sections editor:', error); container.innerHTML = '
Error loading editor. Please refresh the page.
'; } } // Register onViewPage handler for mypreferenceshome page function registerPreferencesPageHandler() { if (!window.KefinTweaksUtils || !window.KefinTweaksUtils.onViewPage) { setTimeout(registerPreferencesPageHandler, 1000); return; } window.KefinTweaksUtils.onViewPage(async (view, element, hash) => { // Check if we're on the mypreferenceshome page LOG('Preferences home page detected'); const form = document.querySelector('.libraryPage:not(.hide) .homeScreenSettingsContainer > form'); if (form && form.firstChild) { // Check if editor already rendered if (form.querySelector('#kefin-user-home-sections-editor')) { return; } // Create container for editor const editorContainer = document.createElement('div'); editorContainer.id = 'kefin-user-home-sections-editor'; // Insert as last child of the form form.appendChild(editorContainer); // Render editor await renderUserHomeSectionsEditor(editorContainer); } }, { pages: ['mypreferenceshome', 'userpreferences'] }); LOG('Registered preferences page handler'); } // Initialize registerPreferencesPageHandler(); // Expose render function globally window.KefinUserHomeScreenConfig = { renderUserHomeSectionsEditor }; LOG('User Home Screen Configuration loaded'); })();