diff --git a/scripts/ui.js b/scripts/ui.js index e97f132..88507f1 100644 --- a/scripts/ui.js +++ b/scripts/ui.js @@ -758,61 +758,87 @@ } } - /** - * Get section type toggle states from localStorage - * @returns {Object} Object mapping section types (lowercase) to their enabled state (boolean) - */ + /** Session-only section-type pill states (cleared on full page reload). */ + let sectionTypeToggleStatesSession = null; + function getSectionTypeToggleStates() { - try { - const stored = localStorage.getItem('kefinTweaks-sectionTypeToggles'); - if (stored) { - return JSON.parse(stored); - } - } catch (e) { - // Ignore parse errors, return defaults - } - return {}; + return sectionTypeToggleStatesSession && typeof sectionTypeToggleStatesSession === 'object' + ? { ...sectionTypeToggleStatesSession } + : {}; } - /** - * Save section type toggle states to localStorage - * @param {Object} states - Object mapping section types (lowercase) to their enabled state (boolean) - */ function saveSectionTypeToggleStates(states) { - try { - localStorage.setItem('kefinTweaks-sectionTypeToggles', JSON.stringify(states)); - } catch (e) { - // Ignore storage errors (e.g., quota exceeded) - } + sectionTypeToggleStatesSession = states && typeof states === 'object' ? { ...states } : {}; + } + + /** Display label for type pills (home โ†’ Normal). */ + function getSectionTypePillLabel(type) { + const t = String(type || '').toLowerCase(); + if (t === 'home') return 'Normal'; + if (!t) return ''; + return t.charAt(0).toUpperCase() + t.slice(1); } /** - * Render section type toggle buttons - * @param {Array} sectionTypes - Array of unique section types - * @param {Array} enabledTypes - Array of types that should be initially enabled (defaults to all) + * Resolve which type pills are active. + * Defaults: Normal + Seasonal on, Discovery off. Discovery is exclusive. + */ + function resolveSectionTypePillStates(sectionTypes, savedStates) { + const types = (sectionTypes || []).map((t) => String(t).toLowerCase()); + const saved = savedStates && typeof savedStates === 'object' ? savedStates : null; + const hasSaved = saved && Object.keys(saved).length > 0; + const states = {}; + + if (!hasSaved) { + types.forEach((type) => { + states[type] = type !== 'discovery'; + }); + return states; + } + + const discoveryOn = types.includes('discovery') && saved.discovery === true; + types.forEach((type) => { + if (discoveryOn) { + states[type] = type === 'discovery'; + } else if (Object.prototype.hasOwnProperty.call(saved, type)) { + states[type] = saved[type] === true; + } else { + // Newly available while Discovery off: Normal/Seasonal default on + states[type] = type !== 'discovery'; + } + }); + + // Ensure at least one active + if (!types.some((t) => states[t])) { + if (types.includes('home')) states.home = true; + else if (types.length) states[types[0]] = true; + } + return states; + } + + /** + * Render section type filter pills (multi-select chips; Discovery exclusive). + * @param {Array} sectionTypes + * @param {Array} enabledTypes - types that should be initially active * @returns {string} HTML string */ function renderSectionTypeToggles(sectionTypes, enabledTypes = null) { if (!sectionTypes || sectionTypes.length === 0) return ''; - - // If enabledTypes not provided, all types are enabled by default - const enabledSet = enabledTypes ? new Set(enabledTypes.map(t => t.toLowerCase())) : new Set(sectionTypes.map(t => t.toLowerCase())); - - // Helper to capitalize first letter for display - const capitalize = (str) => str.charAt(0).toUpperCase() + str.slice(1).toLowerCase(); - + + const enabledSet = enabledTypes + ? new Set(enabledTypes.map((t) => t.toLowerCase())) + : new Set(sectionTypes.map((t) => t.toLowerCase())); + return ` -
- ${sectionTypes.map(type => { +
+ ${sectionTypes.map((type) => { const typeLower = type.toLowerCase(); const isEnabled = enabledSet.has(typeLower); - const buttonClasses = `emby-button section-type-toggle-btn raised block${isEnabled ? ' button-submit' : ''}`; return ` - `; }).join('')} @@ -846,6 +872,18 @@ const dataAttrsStr = dataAttrs.join(' '); const isPinnedSection = sectionId.startsWith('pinned-parent-') || sectionId.startsWith('pinned-list-'); + const isSeasonal = sectionType === 'seasonal' || (section.startDate && section.endDate); + const isDiscovery = sectionType === 'discovery' + || section.discoveryEnabled === true + || !!section.discoveryType; + const seasonalBadge = isSeasonal + ? (section.startDate && section.endDate + ? `Seasonal ยท ${escapeHtml(String(section.startDate))} โ€“ ${escapeHtml(String(section.endDate))}` + : `Seasonal`) + : ''; + const discoveryBadge = isDiscovery + ? `Discovery` + : ''; return `
@@ -856,8 +894,10 @@ dataAttributes: { 'section-id': sectionId } })}
-
+
${sectionName.replace(/"/g, '"')} + ${seasonalBadge} + ${discoveryBadge}
@@ -1060,40 +1100,19 @@ // Get unique section types (sorted for consistent display) all in lowercase const renderedSectionTypes = Array.from(sectionTypesSet).sort().map(type => type.toLowerCase()); - // Preferred Section Types order in array: Jellyfin, Home, Seasonal, Discovery, Custom - let sectionTypes = ['jellyfin', 'home', 'seasonal', 'discovery', 'custom']; - sectionTypes = sectionTypes.filter(type => renderedSectionTypes.includes(type)); + // Preferred order: Normal (home), Seasonal, Discovery + let sectionTypes = ['home', 'seasonal', 'discovery']; + sectionTypes = sectionTypes.filter((type) => renderedSectionTypes.includes(type)); - // Load saved toggle states from localStorage - const savedToggleStates = getSectionTypeToggleStates(); - - // Build data attributes for type filtering - // Use saved states if available, otherwise use defaults (seasonal = false, others = true) - const typeFilterAttrs = sectionTypes.map(type => { + const pillStates = resolveSectionTypePillStates(sectionTypes, getSectionTypeToggleStates()); + saveSectionTypeToggleStates(pillStates); + + const typeFilterAttrs = sectionTypes.map((type) => { const typeLower = type.toLowerCase(); - // Check if we have a saved state for this type - const hasSavedState = savedToggleStates.hasOwnProperty(typeLower); - let isEnabled; - if (hasSavedState) { - // Use saved state - isEnabled = savedToggleStates[typeLower] ? 'true' : 'false'; - } else { - // Use default: seasonal is hidden by default, all others are visible - isEnabled = typeLower === 'seasonal' ? 'false' : 'true'; - } - return `data-section-type-${typeLower}="${isEnabled}"`; + return `data-section-type-${typeLower}="${pillStates[typeLower] ? 'true' : 'false'}"`; }).join(' '); - - // Determine which types should have their toggle buttons initially enabled - const enabledTypes = sectionTypes.filter(type => { - const typeLower = type.toLowerCase(); - const hasSavedState = savedToggleStates.hasOwnProperty(typeLower); - if (hasSavedState) { - return savedToggleStates[typeLower]; - } - // Default: seasonal is off, others are on - return typeLower !== 'seasonal'; - }); + + const enabledTypes = sectionTypes.filter((type) => pillStates[type.toLowerCase()]); return `
@@ -1235,6 +1254,31 @@ display: none !important; } } + .section-type-toggles .kefin-chip { + padding: 0.375rem 0.8125rem; + border-radius: 999px; + border: 1px solid rgba(255, 255, 255, 0.15); + background: rgba(255, 255, 255, 0.04); + font-size: 0.875rem; + cursor: pointer; + font-family: inherit; + color: inherit; + } + .section-type-toggles .kefin-chip.hsae-active { + border-color: rgba(0, 164, 220, 0.6); + background: rgba(0, 164, 220, 0.15); + color: var(--theme-primary-color, #00a4dc); + } + .toggle-slider-thumb { + display: inline-flex; + align-items: center; + justify-content: center; + } + .toggle-slider-thumb .material-icons { + font-size: 14px; + line-height: 1; + color: rgba(0, 0, 0, 0.55); + } `; document.head.appendChild(style); } @@ -1426,12 +1470,7 @@ if (!enabledList || !disabledList) { // Fallback to old behavior if lists don't exist - toggle.dataset.enabled = newEnabled; - toggle.style.background = newEnabled ? 'rgba(0, 164, 220, 0.8)' : 'rgba(158, 158, 158, 0.5)'; - const span = toggle.querySelector('span'); - if (span) { - span.style.left = newEnabled ? '32px' : '2px'; - } + updateToggleSliderUI(toggle, newEnabled); if (onToggleChange) { onToggleChange(sectionId, newEnabled); } @@ -1439,12 +1478,7 @@ } // Update toggle UI - toggle.dataset.enabled = newEnabled; - toggle.style.background = newEnabled ? 'rgba(0, 164, 220, 0.8)' : 'rgba(158, 158, 158, 0.5)'; - const span = toggle.querySelector('span'); - if (span) { - span.style.left = newEnabled ? '32px' : '2px'; - } + updateToggleSliderUI(toggle, newEnabled); // Move section between lists if (newEnabled) { @@ -1492,52 +1526,58 @@ } }); - // Section type toggle handlers + // Section type pill handlers (Normal/Seasonal multi-select; Discovery exclusive) container.addEventListener('click', (e) => { const toggleBtn = e.target.closest('.section-type-toggle-btn'); if (!toggleBtn) return; - + e.stopPropagation(); - const sectionType = toggleBtn.dataset.sectionType; - const sectionTypeLower = sectionType.toLowerCase(); - - // Find the sections container (might be the container itself or a parent) + const sectionTypeLower = String(toggleBtn.dataset.sectionType || '').toLowerCase(); + if (!sectionTypeLower) return; + const sectionsContainer = container.classList.contains('sections-container') ? container : (container.closest('.sections-container') || container.querySelector('.sections-container')); if (!sectionsContainer) return; - - // Get current state and toggle it - const attrName = `data-section-type-${sectionTypeLower}`; - const currentState = sectionsContainer.getAttribute(attrName); - const newState = currentState === 'false' ? 'true' : 'false'; - const newStateBool = newState === 'true'; - // Keep at least one section type visible โ€” do not turn off the last active type - if (!newStateBool && currentState === 'true') { - let enabledTypeCount = 0; - for (const attr of sectionsContainer.attributes) { - if (attr.name.startsWith('data-section-type-') && attr.value === 'true') { - enabledTypeCount++; - } - } - if (enabledTypeCount <= 1) { + const pillBtns = Array.from(sectionsContainer.querySelectorAll('.section-type-toggle-btn')); + const typeKeys = pillBtns.map((btn) => String(btn.dataset.sectionType || '').toLowerCase()).filter(Boolean); + const getActive = (type) => sectionsContainer.getAttribute(`data-section-type-${type}`) === 'true'; + const setActive = (type, on) => { + sectionsContainer.setAttribute(`data-section-type-${type}`, on ? 'true' : 'false'); + }; + + const currentlyOn = getActive(sectionTypeLower); + const discoveryCurrentlyOn = typeKeys.includes('discovery') && getActive('discovery'); + + if (sectionTypeLower === 'discovery') { + if (currentlyOn) { + // Sole-active Discovery: must leave via Normal/Seasonal return; } - } - - sectionsContainer.setAttribute(attrName, newState); - - if (newStateBool) { - toggleBtn.classList.add('button-submit'); + typeKeys.forEach((type) => setActive(type, type === 'discovery')); + } else if (discoveryCurrentlyOn) { + // Leave Discovery: only the clicked pill on + typeKeys.forEach((type) => setActive(type, type === sectionTypeLower)); + } else if (currentlyOn) { + const enabledCount = typeKeys.filter((type) => getActive(type)).length; + if (enabledCount <= 1) return; + setActive(sectionTypeLower, false); } else { - toggleBtn.classList.remove('button-submit'); + setActive(sectionTypeLower, true); } - // Save the new state to localStorage - const savedToggleStates = getSectionTypeToggleStates(); - savedToggleStates[sectionTypeLower] = newStateBool; - saveSectionTypeToggleStates(savedToggleStates); + const nextStates = {}; + pillBtns.forEach((btn) => { + const type = String(btn.dataset.sectionType || '').toLowerCase(); + const on = getActive(type); + nextStates[type] = on; + btn.classList.toggle('hsae-active', on); + }); + saveSectionTypeToggleStates(nextStates); + updateFirstLastAttributes( + sectionsContainer.querySelector('.enabled-sections-list') + ); }); // Drag and drop handlers - only work within enabled sections list @@ -1906,7 +1946,10 @@ transition: left 0.3s ease; box-shadow: 0 2px 4px rgba(0,0,0,0.3); pointer-events: none; - ">`; + display: inline-flex; + align-items: center; + justify-content: center; + ">`; const toggleButton = `