mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Add checkmark and X icons inside of the toggle control
This commit is contained in:
1 parent
67bcb1f7a4
commit
60a9429807
1 file changed
+162
-117
+162
-117
@@ -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<string>} sectionTypes - Array of unique section types
|
||||
* @param {Array<string>} 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<string>} sectionTypes
|
||||
* @param {Array<string>} 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 `
|
||||
<div class="section-type-toggles" style="display: flex; gap: 0.5em; margin-bottom: 1em;">
|
||||
${sectionTypes.map(type => {
|
||||
<div class="section-type-toggles" style="display: flex; flex-wrap: wrap; gap: 0.5em; margin-bottom: 1em;">
|
||||
${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 `
|
||||
<button type="button"
|
||||
class="${buttonClasses}"
|
||||
data-section-type="${typeLower}"
|
||||
style="padding: 0.5em 1em; font-size: 0.9em;">
|
||||
${capitalize(typeLower)}
|
||||
<button type="button"
|
||||
class="kefin-chip section-type-toggle-btn${isEnabled ? ' hsae-active' : ''}"
|
||||
data-section-type="${typeLower}">
|
||||
${escapeHtml(getSectionTypePillLabel(typeLower))}
|
||||
</button>
|
||||
`;
|
||||
}).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
|
||||
? `<span class="listItemBodyText secondary seasonal-tag" style="font-size: 0.8em; background: rgba(255,255,255,0.1); padding: 0.15em 0.45em; border-radius: 3px;">Seasonal · ${escapeHtml(String(section.startDate))} – ${escapeHtml(String(section.endDate))}</span>`
|
||||
: `<span class="listItemBodyText secondary seasonal-tag" style="font-size: 0.8em; background: rgba(255,255,255,0.1); padding: 0.15em 0.45em; border-radius: 3px;">Seasonal</span>`)
|
||||
: '';
|
||||
const discoveryBadge = isDiscovery
|
||||
? `<span class="listItemBodyText secondary discovery-tag" style="font-size: 0.8em; background: rgba(255,255,255,0.1); padding: 0.15em 0.45em; border-radius: 3px;">Discovery</span>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="listItem viewItem section-row" data-section-id="${sectionId}" data-section-type="${sectionType}" ${draggableAttr} ${dataAttrsStr}>
|
||||
@@ -856,8 +894,10 @@
|
||||
dataAttributes: { 'section-id': sectionId }
|
||||
})}
|
||||
<div class="listItemBody">
|
||||
<div style="display: flex; align-items: center; gap: 0.5em;">
|
||||
<div style="display: flex; align-items: center; gap: 0.5em; flex-wrap: wrap;">
|
||||
<span>${sectionName.replace(/"/g, '"')}</span>
|
||||
${seasonalBadge}
|
||||
${discoveryBadge}
|
||||
</div>
|
||||
</div>
|
||||
<div class="section-row-actions">
|
||||
@@ -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 `
|
||||
<div class="sections-container" ${typeFilterAttrs}>
|
||||
@@ -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;
|
||||
"></span>`;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
"><span class="material-icons" aria-hidden="true" style="font-size: 14px; line-height: 1; color: rgba(0,0,0,0.55);">${isEnabled ? 'check' : 'close'}</span></span>`;
|
||||
|
||||
const toggleButton = `
|
||||
<button type="button" class="${escapeHtml(cssClass)}" ${dataAttrsString} ${id ? `data-checkbox-id="${escapeHtml(id)}"` : ''} data-enabled="${isEnabled}" aria-pressed="${isEnabled}"${inlineStyle}>
|
||||
@@ -1947,6 +1990,8 @@
|
||||
const knob = toggleButton.querySelector('.toggle-slider-thumb');
|
||||
if (knob) {
|
||||
knob.style.left = enabled ? 'calc(100% - 25px)' : '3px';
|
||||
const icon = knob.querySelector('.material-icons');
|
||||
if (icon) icon.textContent = enabled ? 'check' : 'close';
|
||||
}
|
||||
|
||||
const checkboxId = toggleButton.dataset.checkboxId;
|
||||
|
||||
Reference in new issue
Block a user