mirror of
https://github.com/ranaldsgift/KefinTweaks.git
synced 2026-11-05 15:52:47 +01:00
Add Home Screen Category filters and fix bugs related to Home Screen not rendering when navigating back to it
This commit is contained in:
1 parent
ea0483ff92
commit
eda0a94420
12 files changed
+1069
-152
No files matched your search
@@ -169,7 +169,7 @@ span.listItemBodyText.secondary.seasonal-tag {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 899px) {
|
||||
[data-modal-id="kefin-homescreen-config-main"] .hsc-split-pane {
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1em;
|
||||
@@ -315,7 +315,7 @@ span.listItemBodyText.secondary.seasonal-tag {
|
||||
align-items: end;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 899px) {
|
||||
[data-modal-id="kefin-homescreen-config-main"] #global-settings-content .hsae-field-grid-4 {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@@ -70,6 +70,18 @@
|
||||
'home-loadPeopleEpisodeData': {
|
||||
on: 'Loads all Person appearances from all episodes in your library. This data takes much longer to cache than just the Movies/Series level data.',
|
||||
off: 'Person appearance data is limited to only Movies and Series.'
|
||||
},
|
||||
'home-showCreateSectionButtonOnHome': {
|
||||
on: 'Adds a hidden button to the bottom left corner of the home screen so that you can quickly create a new section directly from the Home Screen. Only admins will see this button.',
|
||||
off: 'You must use the KefinTweaks Configuration > Home Screen > New Section flow to create new Home Screen sections.'
|
||||
},
|
||||
'home-showCategoryFilters': {
|
||||
on: 'Category Filters will appear on the Home Screen to quickly navigate between Discovery, Pinned or any custom Category sections.',
|
||||
off: 'Category Filters will not appear on the Home Screen.'
|
||||
},
|
||||
'home-renderCategoriesSeparately': {
|
||||
on: 'Sections will only appear when their defined category is selected on the Home Screen. Discovery sections will only load when the Discovery category is selected.',
|
||||
off: 'All sections will be visible when the default Home category is selected. Discovery sections will render at the bottom of the Home and Discovery categories.'
|
||||
}
|
||||
};
|
||||
|
||||
@@ -1184,6 +1196,17 @@
|
||||
mergedConfig.HOME_SETTINGS.minPeopleAppearancesEpisodes = legacyPeopleMin;
|
||||
}
|
||||
|
||||
mergedConfig.HOME_SETTINGS.categories = normalizeHomeCategories(mergedConfig.HOME_SETTINGS.categories);
|
||||
if (mergedConfig.HOME_SETTINGS.renderCategoriesSeparately == null) {
|
||||
mergedConfig.HOME_SETTINGS.renderCategoriesSeparately = false;
|
||||
}
|
||||
if (mergedConfig.HOME_SETTINGS.showCategoryFilters == null) {
|
||||
mergedConfig.HOME_SETTINGS.showCategoryFilters = true;
|
||||
}
|
||||
if (mergedConfig.HOME_SETTINGS.showCreateSectionButtonOnHome == null) {
|
||||
mergedConfig.HOME_SETTINGS.showCreateSectionButtonOnHome = false;
|
||||
}
|
||||
|
||||
// Add ENABLED_NORMAL_SECTIONS and ENABLED_DISCOVERY_SECTIONS
|
||||
// ENABLED_NORMAL_SECTIONS is all enabled sections from HOME_SECTION_GROUPS, SEASONAL_SECTION_GROUPS, and CUSTOM_SECTION_GROUPS that aren't discovery sections
|
||||
// If they have a start/end date the current date must fall within that date range
|
||||
@@ -1948,6 +1971,7 @@
|
||||
{ id: 'general', label: 'General Settings' },
|
||||
{ id: 'spotlight', label: 'Spotlight Settings' },
|
||||
{ id: 'discovery', label: 'Discovery Settings' },
|
||||
{ id: 'categories', label: 'Category Settings' },
|
||||
{ id: 'cache', label: 'Cache Settings' }
|
||||
];
|
||||
return `
|
||||
@@ -1972,6 +1996,7 @@
|
||||
case 'general': return buildGeneralSettingsHTML();
|
||||
case 'spotlight': return se().buildGlobalSpotlightSettingsHTML?.(currentConfig) || '';
|
||||
case 'discovery': return buildDiscoverySettingsHTML();
|
||||
case 'categories': return buildCategorySettingsHTML();
|
||||
case 'cache': return buildCacheSettingsHTML();
|
||||
default: return buildGeneralSettingsHTML();
|
||||
}
|
||||
@@ -2237,6 +2262,9 @@
|
||||
<div class="hsc-settings-span-2">
|
||||
${hscToggleCard('home-ensureThumbsForPopularTVNetworks', homeSettings.ensureThumbsForPopularTVNetworks === true, 'Require Studio Thumbs', D)}
|
||||
</div>
|
||||
<div class="hsc-settings-span-2">
|
||||
${hscToggleCard('home-showCreateSectionButtonOnHome', homeSettings.showCreateSectionButtonOnHome === true, 'Create Section button on Home Screen', D)}
|
||||
</div>
|
||||
<div class="hsc-settings-span-2">
|
||||
${buildTextInput('home-dismissEmptySectionTimer', homeSettings.dismissEmptySectionTimer ?? 0, 'Empty Section Dismiss Timer (ms)', 'number')}
|
||||
<div class="listItemBodyText secondary" style="font-size: 0.85em; margin-top: 0.35em;">
|
||||
@@ -2298,6 +2326,193 @@
|
||||
return '';
|
||||
}
|
||||
|
||||
const CATEGORY_SENTINEL_IDS = new Set(['none', 'discovery', 'pinned']);
|
||||
|
||||
function normalizeHomeCategories(list) {
|
||||
const defaults = window.KefinHomeConfig2?.HOME_SETTINGS?.categories || [];
|
||||
const incoming = Array.isArray(list) ? list : [];
|
||||
const byId = new Map();
|
||||
defaults.forEach((cat) => {
|
||||
if (cat?.id) byId.set(cat.id, { ...cat });
|
||||
});
|
||||
incoming.forEach((cat) => {
|
||||
if (!cat?.id) return;
|
||||
const prev = byId.get(cat.id) || {};
|
||||
byId.set(cat.id, {
|
||||
id: cat.id,
|
||||
name: cat.name || prev.name || cat.id,
|
||||
icon: cat.icon || prev.icon || 'label'
|
||||
});
|
||||
});
|
||||
const ordered = [];
|
||||
const seen = new Set();
|
||||
incoming.forEach((cat) => {
|
||||
if (!cat?.id || seen.has(cat.id)) return;
|
||||
ordered.push(byId.get(cat.id));
|
||||
seen.add(cat.id);
|
||||
});
|
||||
defaults.forEach((cat) => {
|
||||
if (!cat?.id || seen.has(cat.id)) return;
|
||||
ordered.push(byId.get(cat.id));
|
||||
seen.add(cat.id);
|
||||
});
|
||||
return ordered.filter(Boolean);
|
||||
}
|
||||
|
||||
function buildCategoryRowHTML(cat, index, total) {
|
||||
const isSentinel = CATEGORY_SENTINEL_IDS.has(cat.id);
|
||||
return `
|
||||
<div class="listItem kefin-home-category-row" data-index="${index}" data-category-id="${escapeHtml(cat.id)}" style="border: 1px solid rgba(255,255,255,0.1); border-radius: 4px; padding: 0.5em 0.65em; margin-bottom: 0.4em; display: flex; align-items: center; gap: 0.5em;${isSentinel ? ' background: rgba(255,255,255,0.04);' : ''}">
|
||||
<span class="listItemBodyText secondary" style="min-width: 1.5em; text-align: right;">${index + 1}.</span>
|
||||
<input type="text" class="emby-input kefin-home-category-name" value="${escapeHtml(cat.name || '')}" data-index="${index}" aria-label="Category name" style="flex: 1.2; min-width: 0;" />
|
||||
<input type="text" class="emby-input kefin-home-category-icon" value="${escapeHtml(cat.icon || '')}" data-index="${index}" aria-label="Material icon name" placeholder="icon" style="flex: 0.8; min-width: 5em;" />
|
||||
<button type="button" class="emby-button raised kefin-home-category-up" data-index="${index}" title="Move up" style="min-width: auto; padding: 0.35em 0.5em;" ${index === 0 ? 'disabled' : ''}>
|
||||
<span class="material-icons" style="font-size: 1.1em; vertical-align: middle;">arrow_upward</span>
|
||||
</button>
|
||||
<button type="button" class="emby-button raised kefin-home-category-down" data-index="${index}" title="Move down" style="min-width: auto; padding: 0.35em 0.5em;" ${index >= total - 1 ? 'disabled' : ''}>
|
||||
<span class="material-icons" style="font-size: 1.1em; vertical-align: middle;">arrow_downward</span>
|
||||
</button>
|
||||
${isSentinel ? '' : `
|
||||
<button type="button" class="emby-button raised kefin-home-category-remove" data-index="${index}" title="Remove" style="min-width: auto; padding: 0.35em 0.5em;">
|
||||
<span class="material-icons" style="font-size: 1.1em; vertical-align: middle;">delete</span>
|
||||
</button>`}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function buildCategorySettingsHTML() {
|
||||
if (!currentConfig) return '';
|
||||
const homeSettings = currentConfig.HOME_SETTINGS || {};
|
||||
const categories = normalizeHomeCategories(homeSettings.categories);
|
||||
const D = GENERAL_TOGGLE_DESCRIPTIONS;
|
||||
return `
|
||||
<div class="hsc-settings-panel">
|
||||
<div class="hsc-settings-desc listItemBodyText secondary" style="margin-bottom: 1em;">
|
||||
Categories control which Home Screen sections are visible from the fixed category rail. Custom sections can be assigned a category in the section editor.
|
||||
</div>
|
||||
<div class="hsc-settings-grid hsc-settings-grid-2" style="margin-bottom: 1em;">
|
||||
${hscToggleCard('home-showCategoryFilters', homeSettings.showCategoryFilters !== false, 'Show Category Filters', D)}
|
||||
${hscToggleCard('home-renderCategoriesSeparately', homeSettings.renderCategoriesSeparately === true, 'Render Categories Separately', D)}
|
||||
</div>
|
||||
<div class="listItemBodyText" style="font-weight: 500; margin-bottom: 0.5em;">Categories</div>
|
||||
<div class="listItemBodyText secondary" style="font-size: 0.85em; margin-bottom: 0.75em;">
|
||||
Home, Discover, and Pinned are built-in. Their ids cannot change and they cannot be deleted. Custom categories get a stable id when created.
|
||||
</div>
|
||||
<div id="kefin-home-category-list">
|
||||
${categories.map((cat, i) => buildCategoryRowHTML(cat, i, categories.length)).join('')}
|
||||
</div>
|
||||
<div style="display: flex; gap: 0.5em; margin-top: 0.65em; flex-wrap: wrap;">
|
||||
<input type="text" id="kefin-home-category-add-name" class="emby-input" placeholder="Category name…" style="flex: 1; min-width: 140px;" />
|
||||
<input type="text" id="kefin-home-category-add-icon" class="emby-input" placeholder="Material icon (e.g. movie)" style="flex: 0.8; min-width: 120px;" />
|
||||
<button type="button" class="emby-button raised button-submit" id="kefin-home-category-add-btn">Add</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function collectCategoriesFromForm(root) {
|
||||
const rows = Array.from(root.querySelectorAll('.kefin-home-category-row'));
|
||||
return rows.map((row) => ({
|
||||
id: row.dataset.categoryId,
|
||||
name: (row.querySelector('.kefin-home-category-name')?.value || '').trim() || row.dataset.categoryId,
|
||||
icon: (row.querySelector('.kefin-home-category-icon')?.value || '').trim() || 'label'
|
||||
})).filter((cat) => cat.id);
|
||||
}
|
||||
|
||||
function refreshCategoryListUI(root) {
|
||||
const list = root.querySelector('#kefin-home-category-list');
|
||||
if (!list) return;
|
||||
const categories = normalizeHomeCategories(currentConfig.HOME_SETTINGS?.categories);
|
||||
list.innerHTML = categories.map((cat, i) => buildCategoryRowHTML(cat, i, categories.length)).join('');
|
||||
}
|
||||
|
||||
function wireCategorySettingsListeners(root) {
|
||||
if (!root?.querySelector('#kefin-home-category-list')) return;
|
||||
|
||||
const persistCategories = () => {
|
||||
currentConfig.HOME_SETTINGS = {
|
||||
...(currentConfig.HOME_SETTINGS || {}),
|
||||
showCategoryFilters: root.querySelector('#home-showCategoryFilters')?.checked === true,
|
||||
renderCategoriesSeparately: root.querySelector('#home-renderCategoriesSeparately')?.checked === true,
|
||||
categories: normalizeHomeCategories(collectCategoriesFromForm(root))
|
||||
};
|
||||
applyGlobalSettingsFromForm({ save: true });
|
||||
try {
|
||||
window.homeScreen3?.refreshHomeCategoryChrome?.();
|
||||
} catch (_) { /* ignore */ }
|
||||
};
|
||||
|
||||
root.querySelector('#home-showCategoryFilters')?.addEventListener('change', () => {
|
||||
updateHscToggleCardHints(root, GENERAL_TOGGLE_DESCRIPTIONS);
|
||||
persistCategories();
|
||||
});
|
||||
|
||||
root.querySelector('#home-renderCategoriesSeparately')?.addEventListener('change', () => {
|
||||
updateHscToggleCardHints(root, GENERAL_TOGGLE_DESCRIPTIONS);
|
||||
persistCategories();
|
||||
});
|
||||
|
||||
root.addEventListener('change', (e) => {
|
||||
if (e.target.matches?.('.kefin-home-category-name, .kefin-home-category-icon')) {
|
||||
persistCategories();
|
||||
}
|
||||
});
|
||||
|
||||
root.addEventListener('click', (e) => {
|
||||
const up = e.target.closest?.('.kefin-home-category-up');
|
||||
const down = e.target.closest?.('.kefin-home-category-down');
|
||||
const remove = e.target.closest?.('.kefin-home-category-remove');
|
||||
const add = e.target.closest?.('#kefin-home-category-add-btn');
|
||||
if (!up && !down && !remove && !add) return;
|
||||
|
||||
let categories = collectCategoriesFromForm(root);
|
||||
|
||||
if (add) {
|
||||
const name = (root.querySelector('#kefin-home-category-add-name')?.value || '').trim();
|
||||
const icon = (root.querySelector('#kefin-home-category-add-icon')?.value || '').trim() || 'label';
|
||||
if (!name) return;
|
||||
const id = `cat_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`;
|
||||
categories.push({ id, name, icon });
|
||||
const nameInput = root.querySelector('#kefin-home-category-add-name');
|
||||
const iconInput = root.querySelector('#kefin-home-category-add-icon');
|
||||
if (nameInput) nameInput.value = '';
|
||||
if (iconInput) iconInput.value = '';
|
||||
} else {
|
||||
const index = parseInt((up || down || remove).dataset.index, 10);
|
||||
if (!Number.isFinite(index)) return;
|
||||
if (up && index > 0) {
|
||||
[categories[index - 1], categories[index]] = [categories[index], categories[index - 1]];
|
||||
} else if (down && index < categories.length - 1) {
|
||||
[categories[index + 1], categories[index]] = [categories[index], categories[index + 1]];
|
||||
} else if (remove) {
|
||||
const id = categories[index]?.id;
|
||||
if (CATEGORY_SENTINEL_IDS.has(id)) return;
|
||||
categories.splice(index, 1);
|
||||
}
|
||||
}
|
||||
|
||||
currentConfig.HOME_SETTINGS = {
|
||||
...(currentConfig.HOME_SETTINGS || {}),
|
||||
showCategoryFilters: root.querySelector('#home-showCategoryFilters')?.checked === true,
|
||||
renderCategoriesSeparately: root.querySelector('#home-renderCategoriesSeparately')?.checked === true,
|
||||
categories: normalizeHomeCategories(categories)
|
||||
};
|
||||
refreshCategoryListUI(root);
|
||||
applyGlobalSettingsFromForm({ save: true });
|
||||
try {
|
||||
window.homeScreen3?.refreshHomeCategoryChrome?.();
|
||||
} catch (_) { /* ignore */ }
|
||||
});
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert milliseconds to minutes
|
||||
*/
|
||||
@@ -2427,6 +2642,7 @@
|
||||
|| root.querySelector('#libraryCache-mdblistApiKey')
|
||||
|| root.querySelector('#home-minPeopleAppearancesTotal')
|
||||
|| root.querySelector('#home-dismissEmptySectionTimer')
|
||||
|| root.querySelector('#home-showCreateSectionButtonOnHome')
|
||||
|| root.querySelector('#home-loadPeopleEpisodeData')) {
|
||||
const prevLoadPeopleEpisodeData = currentConfig.HOME_SETTINGS?.loadPeopleEpisodeData === true;
|
||||
currentConfig.SEASONAL_THEME_SETTINGS = {
|
||||
@@ -2447,6 +2663,7 @@
|
||||
ensureThumbsForPopularTVNetworks: root.querySelector('#home-ensureThumbsForPopularTVNetworks')?.checked === true,
|
||||
showStaleDataBeforeRefresh: root.querySelector('#home-showStaleDataBeforeRefresh')?.checked === true,
|
||||
dismissEmptySectionTimer: Number.isFinite(dismissEmptyRaw) && dismissEmptyRaw > 0 ? dismissEmptyRaw : 0,
|
||||
showCreateSectionButtonOnHome: root.querySelector('#home-showCreateSectionButtonOnHome')?.checked === true,
|
||||
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),
|
||||
@@ -2926,6 +3143,7 @@
|
||||
window.KefinTweaksUI?.bindToggleCards?.(contentRoot);
|
||||
updateHscToggleCardHints(contentRoot, GENERAL_TOGGLE_DESCRIPTIONS);
|
||||
updateHscToggleCardHints(contentRoot, DISCOVERY_TOGGLE_DESCRIPTIONS);
|
||||
wireCategorySettingsListeners(contentRoot);
|
||||
}
|
||||
|
||||
function ensureGlobalSettingsLiveSync(dialog) {
|
||||
@@ -2967,13 +3185,15 @@
|
||||
});
|
||||
}
|
||||
|
||||
function openNewCustomSectionEditor() {
|
||||
function openNewCustomSectionEditor(options = {}) {
|
||||
getConfig();
|
||||
const defaultTtl = Number(currentConfig?.CACHE?.DEFAULT_TTL);
|
||||
const newSection = {
|
||||
id: `custom-${Date.now()}`,
|
||||
name: 'New Custom Section',
|
||||
enabled: true,
|
||||
order: 100,
|
||||
category: 'none',
|
||||
cardFormat: 'Poster',
|
||||
...(Number.isFinite(defaultTtl) ? { ttl: defaultTtl } : {}),
|
||||
queries: [{
|
||||
@@ -2983,7 +3203,7 @@
|
||||
}
|
||||
}]
|
||||
};
|
||||
openSectionEditor(newSection, 'custom');
|
||||
openSectionEditor(newSection, 'custom', options);
|
||||
}
|
||||
|
||||
function applyOrderListFromForm(dialogRoot) {
|
||||
@@ -5211,6 +5431,10 @@
|
||||
window.KefinHomeScreen.buildEditorPreviewProgressiveSection = buildEditorPreviewProgressiveSection;
|
||||
window.KefinHomeScreen.canOpenSectionEditor = canOpenSectionEditor;
|
||||
window.KefinHomeScreen.openSectionEditorForId = openSectionEditorForId;
|
||||
window.KefinHomeScreen.openNewCustomSection = (options = {}) => openNewCustomSectionEditor({
|
||||
refreshHomeOnSave: true,
|
||||
...options
|
||||
});
|
||||
window.KefinHomeScreen.publishSectionPresentationDefaults = publishSectionPresentationDefaults;
|
||||
window.KefinHomeScreen.findSectionInAllGroups = findSectionInAllGroups;
|
||||
window.KefinHomeScreen.ensureKefinTweaksDefaultSections = ensureKefinTweaksDefaultSections;
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
return flattened;
|
||||
}
|
||||
|
||||
const SECTION_PREF_FIELD_COUNT = 20;
|
||||
const SECTION_PREF_FIELD_COUNT = 21;
|
||||
const DEFAULT_PINNED_LIST_NAME = 'Pinned';
|
||||
const PINNED_LIST_ID_PREFIX = 'pinned-list-';
|
||||
const PINNED_PARENT_ID_PREFIX = 'pinned-parent-';
|
||||
@@ -174,9 +174,27 @@
|
||||
// Field 19: useGaplessCards
|
||||
if (parts[19] === 'true') result.useGaplessCards = true;
|
||||
else if (parts[19] === 'false') result.useGaplessCards = false;
|
||||
// Field 20: renderMode Normal|Spotlight|Random
|
||||
if (parts[20] !== '') {
|
||||
const renderMode = normalizeRenderMode(parts[20]);
|
||||
if (renderMode) result.renderMode = renderMode;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const RENDER_MODES = ['Normal', 'Spotlight', 'Random'];
|
||||
|
||||
function normalizeRenderMode(value) {
|
||||
if (value == null || value === '') return null;
|
||||
const lower = String(value).trim().toLowerCase();
|
||||
return RENDER_MODES.find(mode => mode.toLowerCase() === lower) || null;
|
||||
}
|
||||
|
||||
function resolveRenderMode(section) {
|
||||
return normalizeRenderMode(section?.renderMode)
|
||||
|| (section?.spotlight === true ? 'Spotlight' : 'Normal');
|
||||
}
|
||||
|
||||
function normalizeItemsLayout(value) {
|
||||
if (value === true || value === 'true' || value === 'grid') return 'grid';
|
||||
return 'row';
|
||||
@@ -438,6 +456,11 @@
|
||||
appendPrefField(fields, overrides.borderColor, serverDefaults.borderColor);
|
||||
appendPrefField(fields, overrides.cardTitleColor, serverDefaults.cardTitleColor);
|
||||
appendBoolPrefField(fields, overrides.useGaplessCards, serverDefaults.useGaplessCards);
|
||||
appendPrefField(
|
||||
fields,
|
||||
normalizeRenderMode(overrides.renderMode),
|
||||
normalizeRenderMode(serverDefaults.renderMode) || 'Normal'
|
||||
);
|
||||
return fields.join(';');
|
||||
}
|
||||
|
||||
@@ -921,9 +944,9 @@
|
||||
|
||||
function getServerSectionDefaults(section) {
|
||||
if (!section) return {};
|
||||
const isSpotlight = section.renderMode === 'Spotlight' || section.spotlight === true;
|
||||
const spotlight = section.spotlightConfig || {};
|
||||
return {
|
||||
renderMode: resolveRenderMode(section),
|
||||
order: section.order ?? 0,
|
||||
ttl: section.ttl,
|
||||
cardFormat: section.cardFormat,
|
||||
@@ -970,6 +993,10 @@
|
||||
serverById.get(section.id) || serverById.get(storedId) || section
|
||||
);
|
||||
if (pref.enabled !== undefined) next.enabled = pref.enabled;
|
||||
if (pref.renderMode !== undefined) {
|
||||
next.renderMode = pref.renderMode;
|
||||
next.spotlight = pref.renderMode === 'Spotlight';
|
||||
}
|
||||
if (pref.order !== undefined) next.order = pref.order;
|
||||
if (pref.ttl !== undefined) next.ttl = pref.ttl;
|
||||
if (pref.cardFormat !== undefined) next.cardFormat = pref.cardFormat;
|
||||
@@ -2031,6 +2058,8 @@
|
||||
saveKefinTweaksSectionState,
|
||||
createEmptySectionState,
|
||||
normalizeItemsLayout,
|
||||
normalizeRenderMode,
|
||||
resolveRenderMode,
|
||||
DEFAULT_PINNED_LIST_NAME,
|
||||
PINNED_DEFAULT_ORDER,
|
||||
homeScreenToLegacyArray,
|
||||
|
||||
@@ -131,12 +131,18 @@
|
||||
|
||||
:is([data-modal-id="kefin-homescreen-section-editor"], [data-modal-id="kefin-homescreen-section-create-flow"]) .hsae-basic-group-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.75em;
|
||||
margin-top: 0.85em;
|
||||
margin-bottom: 0.85em;
|
||||
}
|
||||
|
||||
:is([data-modal-id="kefin-homescreen-section-editor"], [data-modal-id="kefin-homescreen-section-create-flow"])
|
||||
.hsae-basic-group-row
|
||||
.hsae-group-new {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
:is([data-modal-id="kefin-homescreen-section-editor"], [data-modal-id="kefin-homescreen-section-create-flow"]) .hsae-field-grid-4 {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
}
|
||||
@@ -149,7 +155,7 @@
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 899px) {
|
||||
:is([data-modal-id="kefin-homescreen-section-editor"], [data-modal-id="kefin-homescreen-section-create-flow"]) .hsae-field-grid-4,
|
||||
:is([data-modal-id="kefin-homescreen-section-editor"], [data-modal-id="kefin-homescreen-section-create-flow"]) .hsae-query-row-sort {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
@@ -792,7 +798,7 @@
|
||||
gap: 0.75em;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
@media (max-width: 899px) {
|
||||
:is([data-modal-id="kefin-homescreen-section-editor"], [data-modal-id="kefin-homescreen-section-create-flow"]) .hsae-appearance-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
@@ -1556,6 +1562,7 @@
|
||||
}
|
||||
|
||||
.hsae-root[data-editor-profile] .hsae-field-grid[data-hsae-profiles],
|
||||
.hsae-root[data-editor-profile] .hsae-basic-group-row[data-hsae-profiles],
|
||||
.hsae-root[data-editor-profile] .hsae-advanced-section-sort[data-hsae-profiles],
|
||||
.hsae-root[data-editor-profile] .hsae-advanced-min-max-age[data-hsae-profiles],
|
||||
.hsae-root[data-editor-profile] .hsae-advanced-limits[data-hsae-profiles] {
|
||||
@@ -1567,6 +1574,7 @@
|
||||
.hsae-root[data-editor-profile="postProcessing"] .hsae-field-grid[data-hsae-profiles~="postProcessing"],
|
||||
.hsae-root[data-editor-profile="normal"] .hsae-field-grid[data-hsae-profiles~="normal"],
|
||||
.hsae-root[data-editor-profile="dateCutoffs"] .hsae-field-grid[data-hsae-profiles~="dateCutoffs"],
|
||||
.hsae-root[data-editor-profile="full"] .hsae-basic-group-row[data-hsae-profiles~="full"],
|
||||
.hsae-root[data-editor-profile="full"] .hsae-advanced-section-sort[data-hsae-profiles~="full"],
|
||||
.hsae-root[data-editor-profile="postProcessing"] .hsae-advanced-section-sort[data-hsae-profiles~="postProcessing"],
|
||||
.hsae-root[data-editor-profile="normal"] .hsae-advanced-section-sort[data-hsae-profiles~="normal"],
|
||||
|
||||
@@ -393,7 +393,14 @@
|
||||
const ADVANCED_TOGGLE_DESCRIPTIONS = {
|
||||
'section-flattenSeries': 'Multiple episodes from the same series will be grouped into one item in the section.',
|
||||
'hsae-limit-before-sort': 'When enabled, the item limit is applied before section-level sort.',
|
||||
'hsae-hide-card-titles': 'Card titles for individual items in the section will not be shown.'
|
||||
'hsae-hide-card-titles': {
|
||||
on: 'Card titles for individual items in the section will not be shown.',
|
||||
off: 'Card titles for individual items in the section will be shown.'
|
||||
},
|
||||
'hsae-use-parent-card': {
|
||||
on: 'Items in this section will link to their Parent item where available. This means that an Episode or Season will link to the Series.',
|
||||
off: 'Items in this section will link to the normal item.'
|
||||
}
|
||||
};
|
||||
|
||||
const RANDOM_QUERY_TOGGLE_DESCRIPTIONS = {
|
||||
@@ -1014,6 +1021,15 @@
|
||||
if (groupNewEl) state.groupNew = groupNewEl.value;
|
||||
if (visibilityEl) state.sectionVisibility = visibilityEl.value;
|
||||
if (orderEl) state.order = orderEl.value;
|
||||
|
||||
const categoryEl = root.querySelector('#section-category');
|
||||
if (categoryEl) {
|
||||
const isDiscovery = state.sectionVisibility === 'discovery';
|
||||
state.category = isDiscovery ? 'discovery' : (categoryEl.value || 'none');
|
||||
state.section = state.section || {};
|
||||
state.section.category = state.category;
|
||||
}
|
||||
|
||||
if (hideWatchedEl) state.hideWatched = hideWatchedEl.checked === true;
|
||||
if (hideNameEl) state.hideName = hideNameEl.checked === true;
|
||||
if (startDateEl) state.startDate = startDateEl.value;
|
||||
@@ -1156,6 +1172,13 @@
|
||||
}
|
||||
}
|
||||
|
||||
const useParentCardEl = root.querySelector('#hsae-use-parent-card');
|
||||
if (useParentCardEl) {
|
||||
state.section = state.section || {};
|
||||
if (useParentCardEl.checked === true) state.section.useParentCard = true;
|
||||
else delete state.section.useParentCard;
|
||||
}
|
||||
|
||||
const cardTitlePositionEl = root.querySelector('#hsae-card-title-position');
|
||||
if (cardTitlePositionEl) {
|
||||
state.section = state.section || {};
|
||||
@@ -1525,11 +1548,33 @@
|
||||
<div class="hsae-card">
|
||||
<div class="hsae-field-grid hsae-field-grid-2 hsae-basic-name-caption">
|
||||
${buildTextInput('section-name', section.name || state.sectionName || '', 'Section Name')}
|
||||
${buildTextInput('section-caption', section.caption || state.caption || '', 'Caption', 'text', 'Displayed below the Section Name.')}
|
||||
${buildTextInput('section-caption', section.caption || state.caption || '', 'Caption', 'text', 'Displayed below the Section Name')}
|
||||
</div>
|
||||
|
||||
<div class="hsae-basic-group-row" data-hsae-profiles="full">
|
||||
<div class="hsae-field-grid hsae-field-grid-2 hsae-basic-group-row" data-hsae-profiles="full">
|
||||
${buildSelect('section-group', groupOptions, selectedGroup, 'Section Group')}
|
||||
${(() => {
|
||||
const categories = (context.config?.HOME_SETTINGS?.categories
|
||||
|| window.KefinTweaksConfig?.homeScreenConfig?.HOME_SETTINGS?.categories
|
||||
|| window.KefinHomeConfig2?.HOME_SETTINGS?.categories
|
||||
|| []).map((c) => ({ value: c.id, label: c.name || c.id }));
|
||||
const isDiscovery = state.sectionVisibility === 'discovery'
|
||||
|| section.discoveryEnabled === true
|
||||
|| section.type === 'discovery';
|
||||
const selectedCategory = isDiscovery ? 'discovery' : (section.category || state.category || 'none');
|
||||
const categorySelect = buildSelect(
|
||||
'section-category',
|
||||
categories.length ? categories : [{ value: 'none', label: 'Home' }],
|
||||
selectedCategory,
|
||||
'Category'
|
||||
);
|
||||
return isDiscovery
|
||||
? categorySelect.replace(
|
||||
'id="section-category" class="fld emby-select emby-select-withcolor"',
|
||||
'id="section-category" class="fld emby-select emby-select-withcolor" disabled'
|
||||
)
|
||||
: categorySelect;
|
||||
})()}
|
||||
<div id="section-group-new-container" class="hsae-group-new" style="display:${selectedGroup === 'New...' ? 'block' : 'none'};">
|
||||
${buildTextInput('section-group-new', state.groupNew || '', 'New Group Name')}
|
||||
</div>
|
||||
@@ -1853,6 +1898,7 @@
|
||||
itemsLayout: draft?.itemsLayout,
|
||||
cardTitleVisibility: draft?.cardTitleVisibility,
|
||||
hideCardTitles: draft?.hideCardTitles,
|
||||
useParentCard: draft?.useParentCard === true,
|
||||
cardTitlePosition: draft?.cardTitlePosition,
|
||||
cardTitleCapitalization: draft?.cardTitleCapitalization,
|
||||
cardTitleFontFamily: draft?.cardTitleFontFamily,
|
||||
@@ -2017,7 +2063,15 @@
|
||||
'hsae-hide-card-titles',
|
||||
hideCardTitles,
|
||||
'Hide Card Titles',
|
||||
ADVANCED_TOGGLE_DESCRIPTIONS['hsae-hide-card-titles']
|
||||
ADVANCED_TOGGLE_DESCRIPTIONS['hsae-hide-card-titles'][hideCardTitles ? 'on' : 'off'],
|
||||
{ hintKey: 'hsae-hide-card-titles' }
|
||||
)}
|
||||
${buildToggleCard(
|
||||
'hsae-use-parent-card',
|
||||
section.useParentCard === true,
|
||||
'Link Parent Item',
|
||||
ADVANCED_TOGGLE_DESCRIPTIONS['hsae-use-parent-card'][section.useParentCard === true ? 'on' : 'off'],
|
||||
{ hintKey: 'hsae-use-parent-card' }
|
||||
)}
|
||||
</div>
|
||||
<div id="hsae-card-title-options-row" style="display:${hideCardTitles ? 'none' : ''};">
|
||||
@@ -2524,6 +2578,8 @@
|
||||
else section.cardTitleVisibility = '';
|
||||
section.hideCardTitles = '';
|
||||
|
||||
section.useParentCard = dialog.querySelector('#hsae-use-parent-card')?.checked === true;
|
||||
|
||||
const cardTitlePosition = dialog.querySelector('#hsae-card-title-position')?.value;
|
||||
if (cardTitlePosition && cardTitlePosition !== 'default') section.cardTitlePosition = cardTitlePosition;
|
||||
else section.cardTitlePosition = '';
|
||||
@@ -2547,6 +2603,14 @@
|
||||
: targetGroupName;
|
||||
}
|
||||
|
||||
const categoryEl = dialog.querySelector('#section-category');
|
||||
if (categoryEl) {
|
||||
const isDiscovery = dialog.querySelector('#hsae-section-visibility')?.value === 'discovery'
|
||||
|| section.discoveryEnabled === true
|
||||
|| section.type === 'discovery';
|
||||
section.category = isDiscovery ? 'discovery' : (categoryEl.value || 'none');
|
||||
}
|
||||
|
||||
return section;
|
||||
}
|
||||
|
||||
@@ -2885,13 +2949,15 @@
|
||||
{ key: 'hideName', checkboxId: 'hsae-hide-section-name', tooltips: HIDE_NAME_TOGGLE_DESCRIPTIONS },
|
||||
{ key: 'hideWatched', checkboxId: 'hsae-hide-watched', tooltips: HIDE_WATCHED_TOGGLE_DESCRIPTIONS },
|
||||
{ key: 'userEnabledByDefault', checkboxId: 'hsae-user-enabled-by-default', tooltips: USER_TOGGLE_TOOLTIPS.userEnabledByDefault },
|
||||
{ key: 'userConfigurable', checkboxId: 'hsae-user-configurable', tooltips: USER_TOGGLE_TOOLTIPS.userConfigurable }
|
||||
{ key: 'userConfigurable', checkboxId: 'hsae-user-configurable', tooltips: USER_TOGGLE_TOOLTIPS.userConfigurable },
|
||||
{ key: 'hsae-hide-card-titles', checkboxId: 'hsae-hide-card-titles', tooltips: ADVANCED_TOGGLE_DESCRIPTIONS['hsae-hide-card-titles'] },
|
||||
{ key: 'hsae-use-parent-card', checkboxId: 'hsae-use-parent-card', tooltips: ADVANCED_TOGGLE_DESCRIPTIONS['hsae-use-parent-card'] }
|
||||
];
|
||||
|
||||
toggles.forEach(({ key, checkboxId, tooltips }) => {
|
||||
const checkbox = root.querySelector(`#${checkboxId}`);
|
||||
const hint = root.querySelector(`[data-hsae-hint="${key}"]`);
|
||||
if (!checkbox || !hint) return;
|
||||
if (!checkbox || !hint || !tooltips) return;
|
||||
const enabled = checkbox.checked === true;
|
||||
hint.textContent = tooltips[enabled ? 'on' : 'off'];
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ interface HomeScreenSection {
|
||||
caption?: string;
|
||||
enabled: boolean;
|
||||
order: number;
|
||||
category?: string; // Stable category id (custom sections); runtime may force discovery/pinned
|
||||
|
||||
// Display Options
|
||||
cardFormat?: "Poster" | "Thumb" | "Backdrop" | "Random";
|
||||
@@ -31,6 +32,7 @@ interface HomeScreenSection {
|
||||
cardTitleFontSize?: "small" | "normal" | "large";
|
||||
userConfigurable?: boolean; // Whether users can toggle/reorder this section
|
||||
useGaplessCards?: boolean; // When true, remove gaps between cards (row or grid)
|
||||
useParentCard?: boolean; // When true, use the parent item will be rendered as the card image and link
|
||||
|
||||
// Loads items from an external URL and loads matches from that list from your Jellyfin library
|
||||
// Currently supports: mdblist.com only
|
||||
@@ -145,7 +147,7 @@ interface SpotlightConfig {
|
||||
// Used for the customPrefs.kefinTweaks.homeScreen property
|
||||
// All values are arrays of semi-colon separated strings
|
||||
interface KefinHomeScreen {
|
||||
sections: Array<string>; // id;enabled;order;ttl;cardFormat;animationEnabled;hideName;hideCardTitles;cardTitlePosition;borderStyle;spotlightLayout;spotlightSize;spotlightTileCount;itemsLayout(row|grid; legacy true→grid);cardTitleCapitalization;cardTitleFontFamily;cardTitleFontSize;borderColor;cardTitleColor;useGaplessCards
|
||||
sections: Array<string>; // id;enabled;order;ttl;cardFormat;animationEnabled;hideName;hideCardTitles;cardTitlePosition;borderStyle;spotlightLayout;spotlightSize;spotlightTileCount;itemsLayout(row|grid; legacy true→grid);cardTitleCapitalization;cardTitleFontFamily;cardTitleFontSize;borderColor;cardTitleColor;useGaplessCards;renderMode(Normal|Spotlight|Random)
|
||||
pinnedLists: Array<string>; // name;id1;id2;id3;...
|
||||
pinnedParents: Array<string>; // name;id;type
|
||||
}
|
||||
@@ -191,4 +193,6 @@ interface KefinHomeScreenSectionUserConfig {
|
||||
cardTitleCapitalization?: string;
|
||||
cardTitleFontFamily?: string;
|
||||
cardTitleFontSize?: string;
|
||||
/** User override of the section's render mode. */
|
||||
renderMode?: "Normal" | "Spotlight" | "Random";
|
||||
}
|
||||
+41
-32
@@ -2049,16 +2049,24 @@
|
||||
|
||||
.emby-scroller-container[data-card-format="Logo"]:not(.visualCardBox) .cardScalable,
|
||||
.emby-scroller-container[data-card-format="Logo"]:not(.visualCardBox) .cardPadder,
|
||||
.emby-scroller-container[data-card-format="Logo"]:not(.visualCardBox) .cardBox,
|
||||
.emby-scroller-container[data-card-format="Clear Art"]:not(.visualCardBox) .cardScalable,
|
||||
.emby-scroller-container[data-card-format="Clear Art"]:not(.visualCardBox) .cardPadder,
|
||||
.emby-scroller-container[data-card-format="Clear Art"]:not(.visualCardBox) .cardBox,
|
||||
.emby-scroller-container[data-card-format="Disc"]:not(.visualCardBox) .cardScalable,
|
||||
.emby-scroller-container[data-card-format="Disc"]:not(.visualCardBox) .cardPadder,
|
||||
.emby-scroller-container[data-card-format="Disc"]:not(.visualCardBox) .cardBox,
|
||||
.emby-scroller-container[data-section-id="liveTv-channels"]:not(.visualCardBox) .cardScalable,
|
||||
.emby-scroller-container[data-section-id="liveTv-channels"]:not(.visualCardBox) .cardPadder,
|
||||
.emby-scroller-container[data-section-id="liveTv-channels"]:not(.visualCardBox) .cardBox,
|
||||
html.sleekfin-main-ui .emby-scroller-container[data-card-format="Logo"]:not(.visualCardBox) .cardPadder,
|
||||
html.sleekfin-main-ui .emby-scroller-container[data-card-format="Clear Art"]:not(.visualCardBox) .cardPadder,
|
||||
html.sleekfin-main-ui .emby-scroller-container[data-card-format="Disc"]:not(.visualCardBox) .cardPadder,
|
||||
html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels"]:not(.visualCardBox) .cardPadder {
|
||||
html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels"]:not(.visualCardBox) .cardPadder,
|
||||
html.sleekfin-main-ui .emby-scroller-container[data-card-format="Logo"]:not(.visualCardBox) .cardBox,
|
||||
html.sleekfin-main-ui .emby-scroller-container[data-card-format="Clear Art"]:not(.visualCardBox) .cardBox,
|
||||
html.sleekfin-main-ui .emby-scroller-container[data-card-format="Disc"]:not(.visualCardBox) .cardBox,
|
||||
html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels"]:not(.visualCardBox) .cardBox {
|
||||
background: transparent !important;
|
||||
box-shadow: none !important;
|
||||
border: none !important;
|
||||
@@ -2192,88 +2200,89 @@ html.sleekfin-main-ui .emby-scroller-container[data-section-id="liveTv-channels"
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/* .spotlight-section .spotlight-item:has(.spotlight-background-layer-center) .spotlight-background-layer {
|
||||
width: 37.5%;
|
||||
.spotlight-section .spotlight-item:has(.spotlight-background-layer-center) .spotlight-background-layer {
|
||||
width: 37.5%;
|
||||
}
|
||||
|
||||
.emby-scroller-container:has([data-direction="right"]:disabled):has([data-direction="left"]:disabled) button.show-all-button {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.emby-scroller-container .emby-scrollbuttons:has([data-direction="right"]:disabled):has([data-direction="left"]:disabled) {
|
||||
display: none;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.spotlight-truncated-list:has(.spotlight-truncated-list-tooltip) {
|
||||
overflow: visible;
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
.emby-scroller-container:has(.itemsContainer[data-layout="grid"]) .emby-scrollbuttons {
|
||||
display: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.emby-scroller-container:has(.itemsContainer[data-layout="grid"]) .show-all-button {
|
||||
display: initial !important;
|
||||
display: initial !important;
|
||||
}
|
||||
|
||||
:has(.spotlight-section[data-layout="Borderless"]) .homeSectionsContainer > *:not(.spotlight-section) {
|
||||
z-index: 1;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.spotlight-section[data-size="full"] .spotlight-truncated-list.itemDetailsGroup:has(>.detailsGroupItem),
|
||||
.spotlight-section[data-size="full"] .spotlight-truncated-list.sectionTabs:has(>.emby-tab-button) {
|
||||
font-size: large;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: start;
|
||||
.spotlight-section[data-size="full"] .spotlight-truncated-list.itemDetailsGroup:has(> .detailsGroupItem),
|
||||
.spotlight-section[data-size="full"] .spotlight-truncated-list.sectionTabs:has(> .emby-tab-button) {
|
||||
font-size: large;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
align-self: start;
|
||||
}
|
||||
|
||||
.spotlight-section .itemDetailsGroup:has(.detailsGroupItem:not(.hide)), .spotlight-section .sectionTabs:not(.spotlight-section-title):has(.emby-tab-button:not(.hide)) {
|
||||
margin: 0px !important;
|
||||
padding: 0px !important;
|
||||
width: initial !important;
|
||||
min-width: initial !important;
|
||||
.spotlight-section .itemDetailsGroup:has(.detailsGroupItem:not(.hide)),
|
||||
.spotlight-section .sectionTabs:not(.spotlight-section-title):has(.emby-tab-button:not(.hide)) {
|
||||
margin: 0px !important;
|
||||
padding: 0px !important;
|
||||
width: initial !important;
|
||||
min-width: initial !important;
|
||||
}
|
||||
|
||||
.spotlight-section:not(.hide) span.emby-tab-button.emby-tab-button-active:not(:has(a)) {
|
||||
cursor: default;
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.emby-scroller-container:has(.itemsContainer[data-layout="grid"]) button.show-all-button::after {
|
||||
content: 'Collapse';
|
||||
content: "Collapse";
|
||||
}
|
||||
|
||||
.layout-mobile .emby-scroller-container:has(.itemsContainer[data-layout="grid"]) button.show-all-button::after {
|
||||
content: 'keyboard_arrow_up';
|
||||
content: "keyboard_arrow_up";
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section .spotlight-section-title.sectionTabs:has(.emby-tab-button:not(.hide)) {
|
||||
justify-self: center;
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
justify-self: center;
|
||||
display: flex !important;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section .spotlight-section-title.sectionTabs:has(.emby-tab-button:not(.hide)) > * {
|
||||
display: block !important;
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.layout-mobile .spotlight-section[data-hide-section-name="true"] .spotlight-section-title,
|
||||
.layout-mobile .spotlight-section[data-hide-section-name="true"] .spotlight-section-title.sectionTabs,
|
||||
.spotlight-section[data-hide-section-name="true"] .spotlight-section-title.sectionTabs {
|
||||
display: none !important;
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.emby-scroller-container:has(.emby-scrollbuttons-button:not(.hide)) button.show-all-button {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.layout-mobile .emby-scroller-container:has(.overflowPortraitCard:nth-child(4)) button.show-all-button,
|
||||
.layout-mobile .emby-scroller-container:has(.overflowBackdropCard:nth-child(3)) button.show-all-button {
|
||||
display: flex;
|
||||
display: flex;
|
||||
}
|
||||
.emby-scroller-container:has([data-custom-card="true"]) .section-refresh-button {
|
||||
display: none;
|
||||
} */
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* Section-level card title presentation */
|
||||
.verticalSection .cardText,
|
||||
|
||||
+45
-22
@@ -551,6 +551,13 @@
|
||||
element.removeAttribute('data-section-type');
|
||||
}
|
||||
|
||||
const resolvedCategory = resolveSectionCategory(sectionConfig);
|
||||
if (resolvedCategory) {
|
||||
element.setAttribute('data-category', resolvedCategory);
|
||||
} else {
|
||||
element.removeAttribute('data-category');
|
||||
}
|
||||
|
||||
if (isCustomSection) element.dataset.customSection = 'true';
|
||||
else delete element.dataset.customSection;
|
||||
|
||||
@@ -558,6 +565,20 @@
|
||||
else delete element.dataset.discoverySection;
|
||||
}
|
||||
|
||||
function resolveSectionCategory(sectionConfig) {
|
||||
if (!sectionConfig) return 'none';
|
||||
const id = String(sectionConfig.id || '');
|
||||
if (id.startsWith('pinned-list-') || id.startsWith('pinned-parent-')) return 'pinned';
|
||||
const sectionType = String(sectionConfig.type || '').toLowerCase();
|
||||
const isDiscovery = sectionConfig.discoveryEnabled === true
|
||||
|| sectionConfig.discoverySection === true
|
||||
|| sectionType === 'discovery'
|
||||
|| sectionType === 'custom-discovery'
|
||||
|| !!sectionConfig.discoveryType;
|
||||
if (isDiscovery) return 'discovery';
|
||||
return sectionConfig.category || 'none';
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the scroll buttons for a scrollable container
|
||||
* @param {HTMLElement} scroller - The scroller element
|
||||
@@ -4691,31 +4712,33 @@
|
||||
bannerContainer.appendChild(navContainer);
|
||||
}
|
||||
|
||||
// Pause button (bottom right)
|
||||
// Pause button (bottom right) — only when nav chrome exists (showNavButtons)
|
||||
if (autoPlay && items.length > 1) {
|
||||
const navButtonsContainer = bannerContainer.querySelector('.spotlight-nav-container .spotlight-nav-buttons-container');
|
||||
const pauseButton = document.createElement('button');
|
||||
pauseButton.className = 'spotlight-pause-button spotlight-nav-button emby-button';
|
||||
const pauseIcon = document.createElement('span');
|
||||
pauseIcon.className = 'material-icons';
|
||||
pauseIcon.textContent = 'pause';
|
||||
pauseButton.appendChild(pauseIcon);
|
||||
pauseButton.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
isPaused = !isPaused;
|
||||
if (isPaused) {
|
||||
if (autoPlayTimer) {
|
||||
clearTimeout(autoPlayTimer);
|
||||
autoPlayTimer = null;
|
||||
if (navButtonsContainer) {
|
||||
const pauseButton = document.createElement('button');
|
||||
pauseButton.className = 'spotlight-pause-button spotlight-nav-button emby-button';
|
||||
const pauseIcon = document.createElement('span');
|
||||
pauseIcon.className = 'material-icons';
|
||||
pauseIcon.textContent = 'pause';
|
||||
pauseButton.appendChild(pauseIcon);
|
||||
pauseButton.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
isPaused = !isPaused;
|
||||
if (isPaused) {
|
||||
if (autoPlayTimer) {
|
||||
clearTimeout(autoPlayTimer);
|
||||
autoPlayTimer = null;
|
||||
}
|
||||
pauseIcon.textContent = 'play_arrow';
|
||||
} else {
|
||||
startAutoPlay();
|
||||
pauseIcon.textContent = 'pause';
|
||||
}
|
||||
pauseIcon.textContent = 'play_arrow';
|
||||
} else {
|
||||
startAutoPlay();
|
||||
pauseIcon.textContent = 'pause';
|
||||
}
|
||||
});
|
||||
|
||||
navButtonsContainer.insertBefore(pauseButton, navButtonsContainer.firstChild);
|
||||
});
|
||||
|
||||
navButtonsContainer.insertBefore(pauseButton, navButtonsContainer.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
// Slide state container (dots or numeric "N / X")
|
||||
|
||||
+125
-1
@@ -67,6 +67,27 @@ div#discovery-loading-indicator:not(.show)::after {
|
||||
opacity: 1 !important;
|
||||
}
|
||||
|
||||
/* Hide Discover More / loading affordance outside Home + Discover categories */
|
||||
#indexPage .homeSectionsContainer[data-category]:not([data-category="none"]):not([data-category="discovery"]) #discovery-loading-indicator,
|
||||
.homePage .homeSectionsContainer[data-category]:not([data-category="none"]):not([data-category="discovery"]) #discovery-loading-indicator,
|
||||
.libraryPage
|
||||
.homeSectionsContainer[data-category]:not([data-category="none"]):not([data-category="discovery"])
|
||||
#discovery-loading-indicator,
|
||||
#indexPage .homeSectionsContainer[data-category]:not([data-category="none"]):not([data-category="discovery"]) .discovery-loading-indicator,
|
||||
.homePage .homeSectionsContainer[data-category]:not([data-category="none"]):not([data-category="discovery"]) .discovery-loading-indicator,
|
||||
.libraryPage
|
||||
.homeSectionsContainer[data-category]:not([data-category="none"]):not([data-category="discovery"])
|
||||
.discovery-loading-indicator {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
min-height: 0 !important;
|
||||
height: 0 !important;
|
||||
overflow: hidden !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
opacity: 0 !important;
|
||||
}
|
||||
|
||||
#indexPage[data-discovery-ready] #discovery-loading-indicator:not(.show) .spinner {
|
||||
visibility: hidden;
|
||||
opacity: 0;
|
||||
@@ -358,7 +379,7 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
|
||||
text-align: center;
|
||||
margin: 3em auto;
|
||||
display: block;
|
||||
animation: fadeInDelayed 0.1s linear 1s forwards;
|
||||
animation: fadeInDelayed 0.1s linear 3s forwards;
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
@@ -384,3 +405,106 @@ body[data-seasonal-animations="true"]:has(.homePage:not(.hide)) .snowverlay {
|
||||
width: 30vw !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Home screen chrome: create section + category rail */
|
||||
.kefin-home-chrome {
|
||||
position: fixed;
|
||||
z-index: 12;
|
||||
pointer-events: none;
|
||||
opacity: 0.18;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
.kefin-home-chrome:hover,
|
||||
.kefin-home-chrome:focus-within {
|
||||
opacity: 1;
|
||||
}
|
||||
.kefin-home-chrome[hidden],
|
||||
#kefin-home-category-rail[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
.kefin-home-chrome button,
|
||||
#kefin-home-category-rail button {
|
||||
pointer-events: auto;
|
||||
}
|
||||
.kefin-home-create-section {
|
||||
left: max(0.75rem, env(safe-area-inset-left));
|
||||
bottom: max(0.75rem, env(safe-area-inset-bottom));
|
||||
}
|
||||
.kefin-home-create-section-btn .material-icons {
|
||||
font-size: 1.75rem;
|
||||
}
|
||||
|
||||
/* Outer sectionTabs wrapper — no native header layout */
|
||||
#kefin-home-category-rail.sectionTabs {
|
||||
position: static;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
/* Inner visual bar — keep our layout over skinHeader / MuiAppBar positioning */
|
||||
#kefin-home-category-rail .kefin-home-category-rail {
|
||||
position: fixed;
|
||||
z-index: 12;
|
||||
top: max(4.5rem, calc(env(safe-area-inset-top) + 4.5rem));
|
||||
left: max(0.5rem, env(safe-area-inset-left));
|
||||
right: auto;
|
||||
bottom: auto;
|
||||
width: auto;
|
||||
height: auto;
|
||||
max-width: none;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
border-radius: 999px;
|
||||
box-shadow: 1px 1px 2px 0 #fff !important;
|
||||
transform: none;
|
||||
pointer-events: none;
|
||||
}
|
||||
#kefin-home-category-rail .kefin-home-category-rail:hover,
|
||||
#kefin-home-category-rail .kefin-home-category-rail:focus-within {
|
||||
opacity: 1;
|
||||
}
|
||||
.kefin-home-category-btn {
|
||||
opacity: 0.75;
|
||||
}
|
||||
.kefin-home-category-btn.is-active {
|
||||
opacity: 1;
|
||||
color: var(--theme-primary-color, #00a4dc);
|
||||
background: rgba(0, 164, 220, 0.18);
|
||||
}
|
||||
.kefin-home-category-btn .material-icons {
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
@media (max-width: 899px) {
|
||||
#kefin-home-category-rail .kefin-home-category-rail {
|
||||
top: auto;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
flex-direction: row;
|
||||
justify-content: space-evenly;
|
||||
align-items: center;
|
||||
gap: 0.15rem;
|
||||
padding: 0.4rem 0.5rem calc(0.4rem + env(safe-area-inset-bottom, 0px)) !important;
|
||||
border-radius: 0;
|
||||
opacity: 1;
|
||||
pointer-events: auto;
|
||||
margin: 0 !important;
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
.kefin-home-create-section {
|
||||
bottom: calc(3.25rem + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
.homePage:not(.hide) {
|
||||
padding-bottom: calc(3.5rem + env(safe-area-inset-bottom, 0px));
|
||||
}
|
||||
}
|
||||
|
||||
.kefin-section-more-toggle {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
body:not(:has(.homePage:not(.hide))) div#kefin-home-category-rail {
|
||||
display: none;
|
||||
}
|
||||
+378
-32
@@ -75,7 +75,8 @@
|
||||
kefinContinueWatching: false,
|
||||
kefinLatestMedia: false,
|
||||
jellyfinOrders: {},
|
||||
homePaintedEventFired: false
|
||||
homePaintedEventFired: false,
|
||||
isRenderingHome: false
|
||||
};
|
||||
|
||||
async function fetchDisplayPreferences() {
|
||||
@@ -427,11 +428,14 @@
|
||||
* Main Entry Point
|
||||
*/
|
||||
async function enhanceHomeScreen() {
|
||||
LOG('Login detected, continuing with home screen initialization');
|
||||
LOG('Initializing Home Screen v3...');
|
||||
performanceTimer.loadTimeStart = performance.now();
|
||||
performanceMetrics.initialRenderStart = performance.now();
|
||||
|
||||
mountCreateSectionButton();
|
||||
mountCategoryRail();
|
||||
syncHomeScreenChromeVisibility();
|
||||
|
||||
// Reset cache/metrics for a new initialization cycle
|
||||
sectionCache.renderedSections = [];
|
||||
sectionCache.fragmentCache = null;
|
||||
@@ -476,7 +480,7 @@
|
||||
}
|
||||
|
||||
// Check if another page container exists with an already rendered KefinTweaks home screen
|
||||
/* const otherPageContainer = document.querySelector('.pageContainer:not(.hide) .homeSectionsContainer[data-sections-rendered="true"]');
|
||||
const otherPageContainer = document.querySelector('.pageContainer:not(.hide) .homeSectionsContainer[data-sections-rendered="true"]');
|
||||
if (otherPageContainer) {
|
||||
// Copy the existing home sections with [data-section-id] from that container into our target container instead of re-rendering it
|
||||
const otherPageSections = otherPageContainer.querySelectorAll('[data-section-id]');
|
||||
@@ -487,7 +491,7 @@
|
||||
container.appendChild(homeScreenFragment);
|
||||
LOG('Cloned existing home sections from a previous container');
|
||||
return;
|
||||
} */
|
||||
}
|
||||
|
||||
let performanceStartTime = performance.now();
|
||||
|
||||
@@ -530,6 +534,8 @@
|
||||
filteredHomeSections = filteredHomeSections.filter(section => section.enabled !== false);
|
||||
}
|
||||
|
||||
updatePinnedCategoryAvailability(filteredHomeSections, homeScreen);
|
||||
|
||||
performanceEndTime = performance.now();
|
||||
performanceDuration = performanceEndTime - performanceStartTime;
|
||||
LOG(`Home Screen v3 Load user home screen sections initialization time: ${performanceDuration.toFixed(2)}ms`);
|
||||
@@ -606,6 +612,12 @@
|
||||
LOG('Initializing Discovery Sections...');
|
||||
setupDiscoveryInteraction(container);
|
||||
|
||||
try {
|
||||
refreshHomeCategoryChrome();
|
||||
} catch (err) {
|
||||
ERR('Failed to refresh home category chrome after home render:', err);
|
||||
}
|
||||
|
||||
/* // If Jellyfin hasn't rendered after 5 seconds, assume it won't and clean up the observer
|
||||
setTimeout(() => {
|
||||
if (!sectionCache.jellyfinHasRendered && observer) {
|
||||
@@ -1147,9 +1159,6 @@
|
||||
return;
|
||||
}
|
||||
|
||||
// Skip if we've already rendered into this container for this cycle and we're not explicitly re-using the cache
|
||||
if (container.dataset.sectionsRendered && !useCache) return;
|
||||
|
||||
// Check if children with [data-section-id] are present
|
||||
const children = container.children;
|
||||
const hasSections = Array.from(children).some(child => child.dataset && child.dataset.sectionId);
|
||||
@@ -1159,6 +1168,8 @@
|
||||
}
|
||||
|
||||
container.dataset.sectionsRendered = 'true';
|
||||
applyActiveHomeCategory(homeChromeActiveCategory || 'none');
|
||||
syncHomeScreenChromeVisibility();
|
||||
|
||||
const sectionsToRender = [];
|
||||
|
||||
@@ -1233,34 +1244,47 @@
|
||||
const homeConfig = await window.KefinHomeScreen.getConfig();
|
||||
const showStaleDataBeforeRefresh = homeConfig?.HOME_SETTINGS?.showStaleDataBeforeRefresh === true;
|
||||
|
||||
const targetContainer = container;
|
||||
const targetContainer = container;
|
||||
|
||||
const renderStartTime = performance.now();
|
||||
await window.cardBuilder.renderProgressiveSections(targetContainer, sectionsToRender, {
|
||||
waitForContainerClass: 'homeSectionsContainer',
|
||||
enhanceOnVisible: true,
|
||||
showStaleDataBeforeRefresh
|
||||
});
|
||||
const renderEndTime = performance.now();
|
||||
const renderDuration = renderEndTime - renderStartTime;
|
||||
LOG(`Home Screen v3 Render progressive sections initialization time: ${renderDuration.toFixed(2)}ms`);
|
||||
|
||||
// If MediaBar plugin is in use, call LayoutSync.update() to update the layout
|
||||
if (typeof LayoutSync !== 'undefined' && LayoutSync && typeof LayoutSync.update === 'function') {
|
||||
LayoutSync.update();
|
||||
const kefinSections = Array.from(children).some(child => child.dataset && child.dataset.sectionId);
|
||||
if (kefinSections) {
|
||||
LOG('Sections already rendered, skipping');
|
||||
return;
|
||||
}
|
||||
|
||||
// Allow deferred library-cache crawls to start (movies/series/people scheduleBootstrap)
|
||||
if (!state.homePaintedEventFired) {
|
||||
state.homePaintedEventFired = true;
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent('kefinTweaksHomePainted'));
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
if (!state.isRenderingHome && !kefinSections) {
|
||||
const renderStartTime = performance.now();
|
||||
state.isRenderingHome = true;
|
||||
|
||||
// Check if target container is homeSectionsContainer
|
||||
if (!targetContainer.classList.contains('homeSectionsContainer')) {
|
||||
targetContainer.dataset.sectionsPrerendered = 'true';
|
||||
await window.cardBuilder.renderProgressiveSections(targetContainer, sectionsToRender, {
|
||||
waitForContainerClass: 'homeSectionsContainer',
|
||||
enhanceOnVisible: true,
|
||||
showStaleDataBeforeRefresh
|
||||
});
|
||||
|
||||
state.isRenderingHome = false;
|
||||
|
||||
const renderEndTime = performance.now();
|
||||
const renderDuration = renderEndTime - renderStartTime;
|
||||
LOG(`Home Screen v3 Render progressive sections initialization time: ${renderDuration.toFixed(2)}ms`);
|
||||
|
||||
// If MediaBar plugin is in use, call LayoutSync.update() to update the layout
|
||||
if (typeof LayoutSync !== 'undefined' && LayoutSync && typeof LayoutSync.update === 'function') {
|
||||
LayoutSync.update();
|
||||
}
|
||||
|
||||
// Allow deferred library-cache crawls to start (movies/series/people scheduleBootstrap)
|
||||
if (!state.homePaintedEventFired) {
|
||||
state.homePaintedEventFired = true;
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent('kefinTweaksHomePainted'));
|
||||
} catch (_) { /* ignore */ }
|
||||
}
|
||||
|
||||
// Check if target container is homeSectionsContainer
|
||||
if (!targetContainer.classList.contains('homeSectionsContainer')) {
|
||||
targetContainer.dataset.sectionsPrerendered = 'true';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1301,6 +1325,10 @@
|
||||
* Ensures discovery buffer is populated (pre-fetches next group)
|
||||
*/
|
||||
async function ensureDiscoveryBuffer() {
|
||||
if (!isDiscoveryAllowedForActiveCategory()) {
|
||||
LOG('Discovery buffer skipped: active category does not allow discovery');
|
||||
return state.discoveryBuffer;
|
||||
}
|
||||
if (state.ensuringDiscoveryBuffer) {
|
||||
LOG('Already ensuring discovery buffer...');
|
||||
|
||||
@@ -1411,6 +1439,11 @@
|
||||
const container = document.querySelector('.libraryPage:not(.hide) .homeSectionsContainer');
|
||||
if (!container) return;
|
||||
|
||||
if (!isDiscoveryAllowedForActiveCategory()) {
|
||||
LOG('Discovery render skipped: active category does not allow discovery');
|
||||
return;
|
||||
}
|
||||
|
||||
// Single-flight: set synchronously before any await
|
||||
if (state.isRenderingDiscovery) return;
|
||||
if (state.discoverySectionsRemain === false) return;
|
||||
@@ -1808,15 +1841,327 @@
|
||||
await enhanceHomeScreen();
|
||||
}
|
||||
|
||||
const HOME_CHROME_CREATE_ID = 'kefin-home-create-section-btn';
|
||||
const HOME_CHROME_CATEGORY_RAIL_ID = 'kefin-home-category-rail';
|
||||
const HOME_CHROME_CATEGORY_STYLE_ID = 'kefin-home-category-filter-styles';
|
||||
const HOME_CHROME_CLASSIC_RAIL_CLASSES = 'skinHeader skinHeader-withBackground emby-tab-button';
|
||||
let homeChromeActiveCategory = 'none';
|
||||
let homeChromeCreateMounted = false;
|
||||
let homeChromeCategoryMounted = false;
|
||||
let homeChromeHasPinnedContent = false;
|
||||
let homeChromeVisibilityObserver = null;
|
||||
let homeChromeMuiAppBarObserver = null;
|
||||
|
||||
function getHomeSettings() {
|
||||
return window.KefinTweaksConfig?.homeScreenConfig?.HOME_SETTINGS
|
||||
|| window.KefinHomeConfig2?.HOME_SETTINGS
|
||||
|| {};
|
||||
}
|
||||
|
||||
function getConfiguredCategories() {
|
||||
const defaults = window.KefinHomeConfig2?.HOME_SETTINGS?.categories || [];
|
||||
const configured = getHomeSettings().categories;
|
||||
if (Array.isArray(configured) && configured.length) return configured;
|
||||
return defaults;
|
||||
}
|
||||
|
||||
function sectionIsPinnedCategory(section) {
|
||||
const id = String(section?.id || '');
|
||||
return id.startsWith('pinned-list-')
|
||||
|| id.startsWith('pinned-parent-')
|
||||
|| section?.category === 'pinned';
|
||||
}
|
||||
|
||||
function updatePinnedCategoryAvailability(sections, homeScreen) {
|
||||
const userHomeConfig = window.KefinUserHomeScreenConfig;
|
||||
const fromUserPins = (userHomeConfig?.buildPinnedSectionConfigs?.(homeScreen) || []).length > 0;
|
||||
const fromSections = (sections || []).some(sectionIsPinnedCategory);
|
||||
homeChromeHasPinnedContent = fromUserPins || fromSections;
|
||||
}
|
||||
|
||||
function getRailCategories() {
|
||||
return getConfiguredCategories().filter((cat) => {
|
||||
if (cat?.id === 'pinned') return homeChromeHasPinnedContent;
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function isModernUIChrome() {
|
||||
return document.querySelector('.MuiBox-root') !== null || localStorage.getItem('layout')?.length === 0;
|
||||
}
|
||||
|
||||
function getCategoryRailNativeClasses() {
|
||||
const base = 'kefin-home-chrome kefin-home-category-rail';
|
||||
if (isModernUIChrome()) {
|
||||
let appBar = document.querySelector('.MuiAppBar-root');
|
||||
|
||||
if (appBar?.className) {
|
||||
return `${base} ${appBar.className}`;
|
||||
}
|
||||
}
|
||||
return `${base} ${HOME_CHROME_CLASSIC_RAIL_CLASSES}`;
|
||||
}
|
||||
|
||||
function ensureMuiAppBarClassObserver() {
|
||||
if (!isModernUIChrome() || homeChromeMuiAppBarObserver) return;
|
||||
if (document.querySelector('.MuiAppBar-root')) return;
|
||||
homeChromeMuiAppBarObserver = new MutationObserver(() => {
|
||||
if (!document.querySelector('.MuiAppBar-root')) return;
|
||||
try { homeChromeMuiAppBarObserver?.disconnect(); } catch (_) { /* ignore */ }
|
||||
homeChromeMuiAppBarObserver = null;
|
||||
const inner = document.querySelector(`#${HOME_CHROME_CATEGORY_RAIL_ID} .kefin-home-category-rail`);
|
||||
if (inner) inner.className = getCategoryRailNativeClasses();
|
||||
});
|
||||
homeChromeMuiAppBarObserver.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
function isHomePageVisible() {
|
||||
return !!document.querySelector('.homePage:not(.hide)');
|
||||
}
|
||||
|
||||
function syncHomeScreenChromeVisibility() {
|
||||
const onHome = isHomePageVisible();
|
||||
const createBtn = document.getElementById(HOME_CHROME_CREATE_ID);
|
||||
if (createBtn) {
|
||||
createBtn.hidden = !onHome;
|
||||
createBtn.setAttribute('aria-hidden', onHome ? 'false' : 'true');
|
||||
}
|
||||
const showFilters = getHomeSettings().showCategoryFilters !== false;
|
||||
const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
|
||||
if (rail) {
|
||||
const visible = onHome && showFilters;
|
||||
rail.hidden = !visible;
|
||||
rail.setAttribute('aria-hidden', visible ? 'false' : 'true');
|
||||
}
|
||||
if (onHome) {
|
||||
applyActiveHomeCategory(homeChromeActiveCategory || 'none');
|
||||
}
|
||||
}
|
||||
|
||||
function ensureHomeChromeVisibilityObserver() {
|
||||
if (homeChromeVisibilityObserver) return;
|
||||
homeChromeVisibilityObserver = new MutationObserver(() => syncHomeScreenChromeVisibility());
|
||||
homeChromeVisibilityObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
attributes: true,
|
||||
attributeFilter: ['class']
|
||||
});
|
||||
}
|
||||
|
||||
async function mountCreateSectionButton() {
|
||||
if (homeChromeCreateMounted || document.getElementById(HOME_CHROME_CREATE_ID)) {
|
||||
homeChromeCreateMounted = true;
|
||||
return;
|
||||
}
|
||||
if (getHomeSettings().showCreateSectionButtonOnHome !== true) return;
|
||||
try {
|
||||
const isAdmin = !!(await window.apiHelper?.isAdmin?.());
|
||||
if (!isAdmin) return;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
const wrap = document.createElement('div');
|
||||
wrap.id = HOME_CHROME_CREATE_ID;
|
||||
wrap.className = 'kefin-home-chrome kefin-home-create-section';
|
||||
wrap.hidden = !isHomePageVisible();
|
||||
wrap.innerHTML = `
|
||||
<button type="button" class="paper-icon-button-light emby-button kefin-home-create-section-btn" title="Create Section" aria-label="Create Section">
|
||||
<span class="material-icons" aria-hidden="true">add_circle</span>
|
||||
</button>
|
||||
`;
|
||||
wrap.querySelector('button')?.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
try {
|
||||
window.KefinHomeScreen?.openNewCustomSection?.({ refreshHomeOnSave: true });
|
||||
} catch (err) {
|
||||
ERR('Failed to open create section from home:', err);
|
||||
}
|
||||
});
|
||||
document.body.appendChild(wrap);
|
||||
homeChromeCreateMounted = true;
|
||||
}
|
||||
|
||||
function buildCategoryFilterStyles(categories) {
|
||||
const rules = [];
|
||||
rules.push(`
|
||||
.homeSectionsContainer[data-render-categories-separately="true"][data-category="none"] > [data-section-id]:not([data-category="none"]) {
|
||||
display: none !important;
|
||||
}
|
||||
`);
|
||||
(categories || []).forEach((cat) => {
|
||||
const id = String(cat?.id || '').trim();
|
||||
if (!id || id === 'none') return;
|
||||
const safe = CSS.escape(id);
|
||||
rules.push(`
|
||||
.homeSectionsContainer[data-category="${safe}"] > [data-section-id]:not([data-category="${safe}"]) {
|
||||
display: none !important;
|
||||
}
|
||||
`);
|
||||
});
|
||||
return rules.join('\n');
|
||||
}
|
||||
|
||||
function ensureCategoryFilterStyles() {
|
||||
let styleEl = document.getElementById(HOME_CHROME_CATEGORY_STYLE_ID);
|
||||
if (!styleEl) {
|
||||
styleEl = document.createElement('style');
|
||||
styleEl.id = HOME_CHROME_CATEGORY_STYLE_ID;
|
||||
document.head.appendChild(styleEl);
|
||||
}
|
||||
styleEl.textContent = buildCategoryFilterStyles(getConfiguredCategories());
|
||||
}
|
||||
|
||||
function applyActiveHomeCategory(categoryId) {
|
||||
const categories = getRailCategories();
|
||||
const next = categories.some((c) => c.id === categoryId) ? categoryId : 'none';
|
||||
const prev = homeChromeActiveCategory || 'none';
|
||||
homeChromeActiveCategory = next;
|
||||
const separate = getHomeSettings().renderCategoriesSeparately === true;
|
||||
|
||||
document.querySelectorAll('.homeSectionsContainer').forEach((container) => {
|
||||
container.dataset.category = next;
|
||||
container.dataset.renderCategoriesSeparately = separate ? 'true' : 'false';
|
||||
});
|
||||
|
||||
const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
|
||||
if (rail) {
|
||||
rail.querySelectorAll('.kefin-home-category-btn').forEach((btn) => {
|
||||
const active = btn.dataset.categoryId === next;
|
||||
btn.classList.toggle('is-active', active);
|
||||
btn.setAttribute('aria-pressed', active ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
if (next === 'discovery' && prev !== 'discovery' && isDiscoveryAllowedForActiveCategory()) {
|
||||
const container = document.querySelector('.libraryPage:not(.hide) .homeSectionsContainer, .homePage:not(.hide) .homeSectionsContainer');
|
||||
if (container) {
|
||||
const hasDiscovery = !!(
|
||||
container.querySelector('[data-discovery-section="true"], [data-category="discovery"]')
|
||||
);
|
||||
if (!hasDiscovery) {
|
||||
try {
|
||||
renderNextDiscoveryGroup();
|
||||
} catch (err) {
|
||||
ERR('Failed to bootstrap discovery group for Discover category:', err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function isDiscoveryAllowedForActiveCategory() {
|
||||
const separate = getHomeSettings().renderCategoriesSeparately === true;
|
||||
const active = homeChromeActiveCategory || 'none';
|
||||
if (separate) return active === 'discovery';
|
||||
return active === 'none' || active === 'discovery';
|
||||
}
|
||||
|
||||
function mountCategoryRail() {
|
||||
if (getHomeSettings().showCategoryFilters === false) {
|
||||
const existing = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
|
||||
if (existing) existing.remove();
|
||||
homeChromeCategoryMounted = false;
|
||||
ensureCategoryFilterStyles();
|
||||
homeChromeActiveCategory = 'none';
|
||||
applyActiveHomeCategory('none');
|
||||
return;
|
||||
}
|
||||
|
||||
if (homeChromeCategoryMounted && document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID)) {
|
||||
ensureCategoryFilterStyles();
|
||||
applyActiveHomeCategory(homeChromeActiveCategory || 'none');
|
||||
return;
|
||||
}
|
||||
|
||||
const categories = getRailCategories();
|
||||
ensureCategoryFilterStyles();
|
||||
|
||||
const desiredCategory = categories.some((c) => c.id === homeChromeActiveCategory)
|
||||
? homeChromeActiveCategory
|
||||
: 'none';
|
||||
|
||||
const outer = document.createElement('div');
|
||||
outer.id = HOME_CHROME_CATEGORY_RAIL_ID;
|
||||
outer.className = 'sectionTabs';
|
||||
outer.hidden = !isHomePageVisible();
|
||||
outer.setAttribute('role', 'toolbar');
|
||||
outer.setAttribute('aria-label', 'Home categories');
|
||||
outer.setAttribute('aria-hidden', outer.hidden ? 'true' : 'false');
|
||||
|
||||
const inner = document.createElement('div');
|
||||
inner.className = getCategoryRailNativeClasses();
|
||||
inner.innerHTML = categories.map((cat) => {
|
||||
const id = escapeHtmlChrome(cat.id);
|
||||
const name = escapeHtmlChrome(cat.name || cat.id);
|
||||
const icon = escapeHtmlChrome(cat.icon || 'label');
|
||||
const active = desiredCategory === cat.id;
|
||||
return `
|
||||
<button type="button"
|
||||
class="paper-icon-button-light emby-button kefin-home-category-btn${active ? ' is-active' : ''}"
|
||||
data-category-id="${id}"
|
||||
title="${name}"
|
||||
aria-label="${name}"
|
||||
aria-pressed="${active ? 'true' : 'false'}">
|
||||
<span class="material-icons" aria-hidden="true">${icon}</span>
|
||||
</button>
|
||||
`;
|
||||
}).join('');
|
||||
|
||||
outer.appendChild(inner);
|
||||
outer.addEventListener('click', (e) => {
|
||||
const btn = e.target.closest('.kefin-home-category-btn');
|
||||
if (!btn) return;
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
applyActiveHomeCategory(btn.dataset.categoryId || 'none');
|
||||
});
|
||||
|
||||
document.body.appendChild(outer);
|
||||
homeChromeCategoryMounted = true;
|
||||
ensureMuiAppBarClassObserver();
|
||||
applyActiveHomeCategory(desiredCategory);
|
||||
}
|
||||
|
||||
function refreshHomeCategoryChrome() {
|
||||
ensureCategoryFilterStyles();
|
||||
const rail = document.getElementById(HOME_CHROME_CATEGORY_RAIL_ID);
|
||||
if (rail) rail.remove();
|
||||
homeChromeCategoryMounted = false;
|
||||
mountCategoryRail();
|
||||
syncHomeScreenChromeVisibility();
|
||||
}
|
||||
|
||||
function escapeHtmlChrome(value) {
|
||||
return String(value ?? '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
async function initHomeScreenChrome() {
|
||||
ensureHomeChromeVisibilityObserver();
|
||||
await mountCreateSectionButton();
|
||||
mountCategoryRail();
|
||||
syncHomeScreenChromeVisibility();
|
||||
}
|
||||
|
||||
window.homeScreen3 = {
|
||||
init: enhanceHomeScreen,
|
||||
refreshHomeSections,
|
||||
resolveDiscoverySection: buildDiscoverySectionInstance,
|
||||
sectionHelper: () => window.sectionHelper
|
||||
sectionHelper: () => window.sectionHelper,
|
||||
getActiveHomeCategory: () => homeChromeActiveCategory || 'none',
|
||||
isDiscoveryAllowedForActiveCategory,
|
||||
refreshHomeCategoryChrome
|
||||
};
|
||||
|
||||
//enhanceHomeScreen();
|
||||
connectHomeSectionsReadyObserver();
|
||||
//initHomeScreenChrome();
|
||||
|
||||
if (window.KefinTweaksUtils && typeof window.KefinTweaksUtils.onViewPage === 'function') {
|
||||
window.KefinTweaksUtils.onViewPage((view, element, hash) => {
|
||||
@@ -1834,6 +2179,7 @@
|
||||
try {
|
||||
manageBodyClasses();
|
||||
enhanceHomeScreen();
|
||||
syncHomeScreenChromeVisibility();
|
||||
} catch (err) { ERR('Home screen page change handler failed:', err); }
|
||||
}, { pages: ['home', 'home.html'] });
|
||||
}
|
||||
|
||||
@@ -2227,6 +2227,9 @@
|
||||
ensureThumbsForPopularTVNetworks: false,
|
||||
showStaleDataBeforeRefresh: true,
|
||||
dismissEmptySectionTimer: 0,
|
||||
showCreateSectionButtonOnHome: false,
|
||||
showCategoryFilters: true,
|
||||
renderCategoriesSeparately: false,
|
||||
minPeopleAppearancesTotal: 10,
|
||||
minPeopleAppearancesMovies: 10,
|
||||
minPeopleAppearancesSeries: 10,
|
||||
@@ -2236,18 +2239,18 @@
|
||||
categories: [
|
||||
{
|
||||
id: 'none',
|
||||
name: 'None',
|
||||
name: 'Home',
|
||||
icon: 'home',
|
||||
},
|
||||
{
|
||||
id: 'discovery',
|
||||
id: 'discovery',
|
||||
name: 'Discover',
|
||||
icon: 'explore',
|
||||
},
|
||||
{
|
||||
id: 'pinned',
|
||||
name: 'Pinned',
|
||||
icon: 'pin',
|
||||
icon: 'push_pin',
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -345,6 +345,7 @@
|
||||
function buildPrefOverrides(cfg) {
|
||||
const spotlight = cfg.spotlightConfig || {};
|
||||
return {
|
||||
renderMode: resolveRenderMode(cfg),
|
||||
order: cfg.order,
|
||||
ttl: cfg.ttl,
|
||||
cardFormat: cfg.cardFormat,
|
||||
@@ -394,6 +395,9 @@
|
||||
|
||||
function applyServerDefaultsToConfig(cfg, defaults) {
|
||||
if (!cfg || !defaults) return;
|
||||
const renderMode = defaults.renderMode || 'Normal';
|
||||
cfg.renderMode = renderMode;
|
||||
cfg.spotlight = renderMode === 'Spotlight';
|
||||
cfg.order = defaults.order;
|
||||
cfg.ttl = defaults.ttl;
|
||||
cfg.cardFormat = defaults.cardFormat;
|
||||
@@ -751,10 +755,38 @@
|
||||
];
|
||||
}
|
||||
|
||||
function resolveRenderMode(cfg) {
|
||||
const api = getConfigApi();
|
||||
if (api?.resolveRenderMode) return api.resolveRenderMode(cfg);
|
||||
return cfg?.renderMode || (cfg?.spotlight === true ? 'Spotlight' : 'Normal');
|
||||
}
|
||||
|
||||
function isSpotlightSection(cfg) {
|
||||
return cfg?.renderMode === 'Spotlight' || cfg?.spotlight === true;
|
||||
}
|
||||
|
||||
// 'Random' picks a mode at render time, so a re-render must keep whatever is on screen.
|
||||
function shouldRenderAsSpotlight(entry) {
|
||||
if (resolveRenderMode(entry?.effectiveConfig) === 'Random') {
|
||||
return entry?.element?.classList?.contains('spotlight-section') === true;
|
||||
}
|
||||
return isSpotlightSection(entry?.effectiveConfig);
|
||||
}
|
||||
|
||||
// A section that was never spotlight has no spotlightConfig, so the popover controls and
|
||||
// the renderer would otherwise fall back to different defaults.
|
||||
function buildSpotlightConfigSeed(existingConfig, serverDefaults = {}) {
|
||||
const existing = existingConfig || {};
|
||||
const admin = window.KefinHomeScreen?.getConfig?.()?.SPOTLIGHT_SETTINGS || {};
|
||||
return {
|
||||
...admin,
|
||||
...existing,
|
||||
spotlightLayout: existing.spotlightLayout ?? admin.spotlightLayout ?? serverDefaults.spotlightLayout ?? 'Border',
|
||||
spotlightSize: existing.spotlightSize ?? admin.spotlightSize ?? serverDefaults.spotlightSize ?? 'normal',
|
||||
tileCount: existing.tileCount ?? admin.tileCount ?? serverDefaults.spotlightTileCount ?? 1
|
||||
};
|
||||
}
|
||||
|
||||
function applyPresentationToElement(entry) {
|
||||
const cfg = entry.effectiveConfig;
|
||||
const el = entry.element;
|
||||
@@ -827,7 +859,7 @@
|
||||
delete el.dataset.fontSize;
|
||||
}
|
||||
|
||||
if (isSpotlightSection(cfg)) {
|
||||
if (shouldRenderAsSpotlight(entry)) {
|
||||
const spotlight = cfg.spotlightConfig || {};
|
||||
if (spotlight.spotlightLayout) {
|
||||
el.dataset.layout = spotlight.spotlightLayout;
|
||||
@@ -846,9 +878,16 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Structural classes/attributes owned by each render mode's builder. When a section
|
||||
// switches modes the freshly built root must keep its own set instead of inheriting
|
||||
// the previous mode's.
|
||||
const SPOTLIGHT_MODE_CLASSES = ['spotlight-section', 'padded-left'];
|
||||
const NORMAL_MODE_CLASSES = ['verticalSection', 'emby-scroller-container', 'custom-scroller-container'];
|
||||
const SPOTLIGHT_MODE_ATTRS = ['data-layout', 'data-size', 'data-tile-count'];
|
||||
|
||||
function replaceSectionElement(entry, newContent) {
|
||||
const cfg = entry.effectiveConfig;
|
||||
const isSpotlight = isSpotlightSection(cfg);
|
||||
const isSpotlight = newContent.classList.contains('spotlight-section');
|
||||
let preservedLayout = null;
|
||||
let preservedGapless = null;
|
||||
if (!isSpotlight) {
|
||||
@@ -862,9 +901,21 @@
|
||||
|| window.cardBuilder?.resolveUseGaplessCards?.(cfg) === true;
|
||||
}
|
||||
|
||||
const wasSpotlight = entry.element.classList.contains('spotlight-section');
|
||||
const modeChanged = wasSpotlight !== isSpotlight;
|
||||
|
||||
newContent.style.cssText = entry.element.style.cssText;
|
||||
newContent.className = entry.element.className;
|
||||
if (modeChanged) {
|
||||
const staleClasses = wasSpotlight ? SPOTLIGHT_MODE_CLASSES : NORMAL_MODE_CLASSES;
|
||||
entry.element.classList.forEach(cls => {
|
||||
if (!staleClasses.includes(cls)) newContent.classList.add(cls);
|
||||
});
|
||||
} else {
|
||||
newContent.className = entry.element.className;
|
||||
}
|
||||
Array.from(entry.element.attributes).forEach(attr => {
|
||||
if (attr.name === 'class') return;
|
||||
if (modeChanged && !isSpotlight && SPOTLIGHT_MODE_ATTRS.includes(attr.name)) return;
|
||||
newContent.setAttribute(attr.name, attr.value);
|
||||
});
|
||||
if (cfg.order != null) {
|
||||
@@ -910,7 +961,7 @@
|
||||
}
|
||||
|
||||
let newContent = null;
|
||||
if (isSpotlightSection(cfg) && window.cardBuilder?.renderSpotlightSection) {
|
||||
if (shouldRenderAsSpotlight(entry) && window.cardBuilder?.renderSpotlightSection) {
|
||||
const activeSlide = entry.element?.querySelector('.spotlight-item[data-active]');
|
||||
const initialIndex = parseInt(activeSlide?.getAttribute('data-index'), 10);
|
||||
newContent = window.cardBuilder.renderSpotlightSection(items, cfg.name, {
|
||||
@@ -1129,6 +1180,7 @@
|
||||
showCloseButton: false,
|
||||
closeOnBackdrop: true,
|
||||
closeOnEscape: true,
|
||||
fixedSize: false,
|
||||
onOpen: (modalInstance) => {
|
||||
styleAnchoredPopoverDialog(modalInstance, anchorBtn);
|
||||
const root = modalInstance.dialogContent;
|
||||
@@ -1314,6 +1366,7 @@
|
||||
showCloseButton: false,
|
||||
closeOnBackdrop: true,
|
||||
closeOnEscape: true,
|
||||
fixedSize: false,
|
||||
onOpen: (modalInstance) => {
|
||||
styleAnchoredPopoverDialog(modalInstance, anchorBtn);
|
||||
const root = modalInstance.dialogContent;
|
||||
@@ -1959,20 +2012,16 @@
|
||||
}
|
||||
|
||||
function resolvePopoverMount(anchorButton, sectionElement) {
|
||||
return anchorButton?.closest(
|
||||
'.sectionTitleContainer, .spotlight-section-title-container'
|
||||
) || sectionElement;
|
||||
return document.body;
|
||||
}
|
||||
|
||||
function preparePopoverMount(anchorButton, sectionElement) {
|
||||
const mountElement = resolvePopoverMount(anchorButton, sectionElement);
|
||||
mountElement.classList.add('kefin-section-has-configure-popover');
|
||||
sectionElement?.classList.add('kefin-section-configure-popover-open');
|
||||
return mountElement;
|
||||
}
|
||||
|
||||
function cleanupPopoverMount(mountElement, sectionElement) {
|
||||
mountElement?.classList.remove('kefin-section-has-configure-popover');
|
||||
sectionElement?.classList.remove('kefin-section-configure-popover-open');
|
||||
}
|
||||
|
||||
@@ -2139,54 +2188,51 @@
|
||||
}
|
||||
|
||||
function positionConfigurePopover(popover, anchorButton, mountElement) {
|
||||
const sectionElement = mountElement.closest('[data-section-id]')
|
||||
|| activePopover?.entry?.element
|
||||
const sectionElement = activePopover?.entry?.element
|
||||
|| anchorButton?.closest?.('[data-section-id]')
|
||||
|| mountElement;
|
||||
const mountRect = mountElement.getBoundingClientRect();
|
||||
const sectionRect = sectionElement.getBoundingClientRect();
|
||||
const sectionRect = sectionElement?.getBoundingClientRect?.() || anchorButton.getBoundingClientRect();
|
||||
const buttonRect = anchorButton.getBoundingClientRect();
|
||||
const popoverWidth = popover.offsetWidth || popover.getBoundingClientRect().width;
|
||||
const popoverWidth = popover.offsetWidth || popover.getBoundingClientRect().width || 280;
|
||||
const margin = 8;
|
||||
const gap = 8;
|
||||
|
||||
// Center on the configure button in viewport space first.
|
||||
let leftViewport = buttonRect.left + (buttonRect.width / 2) - (popoverWidth / 2);
|
||||
|
||||
// Keep inside the section when possible; always keep inside the viewport.
|
||||
const minLeft = Math.max(sectionRect.left + margin, margin);
|
||||
const maxLeft = Math.min(sectionRect.right - popoverWidth - margin, window.innerWidth - popoverWidth - margin);
|
||||
|
||||
if (maxLeft >= minLeft) {
|
||||
leftViewport = Math.min(Math.max(leftViewport, minLeft), maxLeft);
|
||||
} else {
|
||||
// Section narrower than popover (common when section name is hidden):
|
||||
// pin to the section's left edge, then clamp to the viewport.
|
||||
leftViewport = Math.min(
|
||||
Math.max(sectionRect.left + margin, margin),
|
||||
Math.max(margin, window.innerWidth - popoverWidth - margin)
|
||||
);
|
||||
}
|
||||
|
||||
popover.style.left = `${leftViewport - mountRect.left}px`;
|
||||
|
||||
// Spotlights: always open below so the popover stays inside the banner
|
||||
// (overflow/mask on .spotlight-banner-container would clip anything above).
|
||||
// Normal sections: decide once from expanded height; never flip after open.
|
||||
let placeBelow;
|
||||
if (activePopover?.anchorMode === 'above' || activePopover?.anchorMode === 'below') {
|
||||
placeBelow = activePopover.anchorMode === 'below';
|
||||
} else {
|
||||
const isSpotlight = !!mountElement.closest('.spotlight-section')
|
||||
|| activePopover?.entry?.element?.classList.contains('spotlight-section');
|
||||
const isSpotlight = !!sectionElement?.classList?.contains('spotlight-section')
|
||||
|| !!anchorButton?.closest?.('.spotlight-section');
|
||||
const expandedHeight = getPopoverPlacementHeight(popover, true);
|
||||
const spaceAbove = mountRect.top - margin;
|
||||
const spaceAbove = buttonRect.top - margin;
|
||||
placeBelow = isSpotlight || spaceAbove < expandedHeight + gap;
|
||||
if (activePopover) activePopover.anchorMode = placeBelow ? 'below' : 'above';
|
||||
}
|
||||
|
||||
popover.classList.toggle('is-below', placeBelow);
|
||||
popover.style.top = '';
|
||||
popover.style.bottom = '';
|
||||
popover.style.position = 'fixed';
|
||||
popover.style.left = `${leftViewport}px`;
|
||||
if (placeBelow) {
|
||||
popover.style.top = `${buttonRect.bottom + gap}px`;
|
||||
popover.style.bottom = 'auto';
|
||||
} else {
|
||||
const height = getPopoverPlacementHeight(popover, popover.classList.contains('is-expanded'));
|
||||
popover.style.top = `${Math.max(margin, buttonRect.top - gap - height)}px`;
|
||||
popover.style.bottom = 'auto';
|
||||
}
|
||||
}
|
||||
|
||||
function openConfigurePopover(sectionConfig, sectionElement, anchorButton) {
|
||||
@@ -2263,6 +2309,13 @@
|
||||
<div class="kefin-section-configure-title-container">
|
||||
<div class="kefin-section-configure-title-row">
|
||||
<h3>${escapeHtml(sectionConfig.name)}</h3>
|
||||
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-restore-defaults-btn${sectionDirty ? '' : ' is-disabled'}" title="Restore defaults" aria-label="Restore defaults" ${sectionDirty ? '' : 'disabled'} aria-disabled="${sectionDirty ? 'false' : 'true'}">
|
||||
<span class="material-icons" aria-hidden="true">settings_backup_restore</span>
|
||||
</button>
|
||||
${isPinnedParent ? `
|
||||
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-unpin-button" title="Unpin" aria-label="Unpin from Home Screen">
|
||||
<span class="material-icons" aria-hidden="true">push_pin</span>
|
||||
</button>` : ''}
|
||||
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-publish-btn" title="Apply to all users" aria-label="Apply to all users" hidden>
|
||||
<span class="material-icons" aria-hidden="true">publish</span>
|
||||
</button>
|
||||
@@ -2287,6 +2340,10 @@
|
||||
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-visibility-btn${cfg.enabled !== false ? '' : ' is-hidden-visibility'}" title="${cfg.enabled !== false ? 'Hide section' : 'Show section'}" aria-label="${cfg.enabled !== false ? 'Hide section' : 'Show section'}" aria-pressed="${cfg.enabled !== false}">
|
||||
<span class="material-icons" aria-hidden="true">${cfg.enabled !== false ? 'visibility' : 'visibility_off'}</span>
|
||||
</button>
|
||||
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-view-carousel-btn${isSpotlight ? ' is-active' : ''}" title="${isSpotlight ? 'Spotlight mode' : 'Normal mode'}" aria-label="Toggle Spotlight" aria-pressed="${isSpotlight}">
|
||||
<span class="material-icons" aria-hidden="true">view_carousel</span>
|
||||
</button>
|
||||
<span class="kefin-section-configure-divider" aria-hidden="true"></span>
|
||||
${!isSpotlight ? `
|
||||
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-format-btn" title="Card Format" aria-label="Card Format">
|
||||
<span class="material-icons" aria-hidden="true">image</span>
|
||||
@@ -2306,13 +2363,6 @@
|
||||
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-spotlight-tiles-btn" title="${escapeHtml(spotlightTilesLabel)}" aria-label="${escapeHtml(spotlightTilesLabel)}" data-tile-count="${spotlightTileCount}">
|
||||
<span class="material-icons" aria-hidden="true">${spotlightTilesIcon}</span>
|
||||
</button>`}
|
||||
${isPinnedParent ? `
|
||||
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-unpin-button" title="Unpin" aria-label="Unpin from Home Screen">
|
||||
<span class="material-icons" aria-hidden="true">push_pin</span>
|
||||
</button>` : ''}
|
||||
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-restore-defaults-btn${sectionDirty ? '' : ' is-disabled'}" title="Restore defaults" aria-label="Restore defaults" ${sectionDirty ? '' : 'disabled'} aria-disabled="${sectionDirty ? 'false' : 'true'}">
|
||||
<span class="material-icons" aria-hidden="true">settings_backup_restore</span>
|
||||
</button>
|
||||
<button type="button" is="paper-icon-button-light" class="paper-icon-button-light emby-button kefin-section-more-toggle" title="More options" aria-label="More options" aria-expanded="false">
|
||||
<span class="material-icons" aria-hidden="true">more_horiz</span>
|
||||
</button>
|
||||
@@ -2589,6 +2639,25 @@
|
||||
openRestoreSectionDefaultsConfirm(() => restoreSectionToServerDefaults(entry, serverDefaults));
|
||||
});
|
||||
|
||||
popover.querySelector('.kefin-section-view-carousel-btn')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const nextSpotlight = !isSpotlightSection(entry.effectiveConfig);
|
||||
entry.effectiveConfig.renderMode = nextSpotlight ? 'Spotlight' : 'Normal';
|
||||
entry.effectiveConfig.spotlight = nextSpotlight;
|
||||
if (nextSpotlight) {
|
||||
entry.effectiveConfig.spotlightConfig = buildSpotlightConfigSeed(
|
||||
entry.effectiveConfig.spotlightConfig,
|
||||
serverDefaults
|
||||
);
|
||||
}
|
||||
persistAndApply(entry, serverDefaults, () => {
|
||||
closePopover();
|
||||
rerenderSection(entry);
|
||||
const anchor = getConfigureAnchorButton(entry);
|
||||
if (anchor) openConfigurePopover(entry.effectiveConfig, entry.element, anchor);
|
||||
});
|
||||
});
|
||||
|
||||
popover.querySelector('.kefin-section-more-toggle')?.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
const willExpand = !popover.classList.contains('is-expanded');
|
||||
@@ -2724,9 +2793,7 @@
|
||||
overflow: visible;
|
||||
}
|
||||
.kefin-section-configure-popover {
|
||||
position: absolute;
|
||||
bottom: calc(100% - 15px);
|
||||
top: auto;
|
||||
position: fixed;
|
||||
z-index: 12000;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
@@ -2738,8 +2805,6 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
.kefin-section-configure-popover.is-below {
|
||||
top: 55px;
|
||||
bottom: auto;
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
.kefin-section-configure-popover.is-below .kefin-section-configure-title-container {
|
||||
@@ -2809,12 +2874,22 @@
|
||||
.kefin-section-configure-primary-row .paper-icon-button-light .material-icons {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.kefin-section-configure-primary-row .kefin-section-restore-defaults-btn {
|
||||
margin-left: auto;
|
||||
.kefin-section-configure-divider {
|
||||
width: 1px;
|
||||
align-self: stretch;
|
||||
margin: 0.35em 0.25em;
|
||||
background: rgba(255, 255, 255, 0.2);
|
||||
flex: 0 0 1px;
|
||||
}
|
||||
.kefin-section-visibility-btn.is-hidden-visibility {
|
||||
opacity: 0.45;
|
||||
}
|
||||
.kefin-section-view-carousel-btn:not(.is-active) {
|
||||
opacity: 0.4;
|
||||
}
|
||||
.kefin-section-view-carousel-btn.is-active {
|
||||
opacity: 1;
|
||||
}
|
||||
.kefin-section-restore-defaults-btn.is-disabled,
|
||||
.kefin-section-restore-defaults-btn:disabled {
|
||||
opacity: 0.4;
|
||||
@@ -3066,7 +3141,9 @@
|
||||
margin: 0 0 0.5rem 0.5rem;
|
||||
}
|
||||
.kefin-section-configure-title-row .kefin-section-open-editor-btn,
|
||||
.kefin-section-configure-title-row .kefin-section-publish-btn {
|
||||
.kefin-section-configure-title-row .kefin-section-publish-btn,
|
||||
.kefin-section-configure-title-row .kefin-section-restore-defaults-btn,
|
||||
.kefin-section-configure-title-row .kefin-section-unpin-button {
|
||||
flex: 0 0 auto;
|
||||
opacity: 0.85;
|
||||
padding: 0;
|
||||
@@ -3075,7 +3152,9 @@
|
||||
height: 2.25rem;
|
||||
}
|
||||
.kefin-section-configure-title-row .kefin-section-open-editor-btn .material-icons,
|
||||
.kefin-section-configure-title-row .kefin-section-publish-btn .material-icons {
|
||||
.kefin-section-configure-title-row .kefin-section-publish-btn .material-icons,
|
||||
.kefin-section-configure-title-row .kefin-section-restore-defaults-btn .material-icons,
|
||||
.kefin-section-configure-title-row .kefin-section-unpin-button .material-icons {
|
||||
font-size: 1.25rem;
|
||||
}
|
||||
.kefin-section-configure-title-row .kefin-section-open-editor-btn[hidden],
|
||||
@@ -3083,7 +3162,9 @@
|
||||
display: none !important;
|
||||
}
|
||||
.kefin-section-configure-title-row .kefin-section-open-editor-btn:hover,
|
||||
.kefin-section-configure-title-row .kefin-section-publish-btn:hover {
|
||||
.kefin-section-configure-title-row .kefin-section-publish-btn:hover,
|
||||
.kefin-section-configure-title-row .kefin-section-restore-defaults-btn:hover,
|
||||
.kefin-section-configure-title-row .kefin-section-unpin-button:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
`;
|
||||
|
||||
Reference in new issue
Block a user